diff --git a/.gitignore b/.gitignore index 118d11be55..85bd82b7ff 100644 --- a/.gitignore +++ b/.gitignore @@ -300,6 +300,7 @@ Kopie von* /src/Presentation/SmartStore.Web/App_Data/ImportProfiles/ /src/Presentation/SmartStore.Web/App_Data/Indexing/ /src/Presentation/SmartStore.Web/App_Data/Lucene/ +/src/Presentation/SmartStore.Web/App_Data/EfCache/ /src/Presentation/SmartStore.Web/App_Data/Settings.txt /src/Presentation/SmartStore.Web/App_Data/InstalledPlugins.txt /src/Presentation/SmartStore.Web/App_Data/Licenses.lic diff --git a/README.md b/README.md index 95b669b8f9..f8e84aa9bb 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,60 @@ -## Overview +

+ + Smartstore.NET + + + +
+
+ +

+ Smartstore.NET +

+

Ready. Sell. Grow.

+ + +

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

- SmartStore.NET Demoshop + 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 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. -**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. +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. -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. +
+

+ Smartstore.NET Demoshop +

+
-**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. +**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 ;-) +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) +* **Documentation:** [Smartstore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home) + +

 

+ ## Highlights @@ -35,6 +71,8 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC ### Features +* NEW: CMS Page Builder +* NEW: CMS Menu Builder * Multi-Store support * Unlimited number of products and categories * Product Bundles @@ -55,40 +93,28 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC * CMS (Blog, Forum, custom pages & HTML content etc.) * and many more... + + ## Project Status -SmartStore.NET V3.1.5 has been released on 25 May, 2018. The highlights are: - -* **Wallet \***: Enables full or partial order payment via credit account. Includes REST-Api. -* **[Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) template engine**: very flexible templating for e-mails and campaigns with autocompletion and syntax highlighting. -* **Cash Rounding**: define money rounding rules on currency and payment method level. -* **Modern, responsive backend**: migrated backend to Bootstrap 4, overhauled and improved the user interface. -* **RTL**: comprehensive RTL (Right-to-left) and bidi(rectional) support. -* Search engine friendly topic URLs -* Compliance with **EU-GDPR** requirements -* **Enhanced MegaMenu \***: virtual dropdowns for surplus top-level categories and brands. -* **Honeypot** bot detection for registration and contact forms. -* **Amazon Pay**: - * Supports merchants registered in the USA and Japan - * External authentication via *Login with Amazon* button in shop frontend - * Several improvements through the new *Login and pay with Amazon* services -* **Image processing**: new processing and caching strategy! Thumbnails are not created synchronously during the main request anymore, instead a new middleware route defers processing until an image is requested by any client. -* **TinyImage \***: scores ultra-high image compression rates (up to 80 %!) and enables WebP support. -* **UrlRewriter \***: define URL redirection rules in the backend using *mod_rewrite* notation. -* **Address formatting** templates by country -* **Language packs**: downloader & auto-importer for packages available online. -* ...and a lot more new features, enhancements and fixes - -\* Commercial plugin + +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. +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 @@ -96,7 +122,7 @@ We have set up a live online demo for you so you are able to test SmartStore.NET * 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. +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 @@ -107,6 +133,7 @@ NOTE: SmartStore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2 * Full Trust + ## License -SmartStore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt). +Smartstore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt). diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets index b5914fd87d..080c78d497 100644 --- a/SmartStoreNET.Tasks.Targets +++ b/SmartStoreNET.Tasks.Targets @@ -39,7 +39,7 @@ x86 $(BUILD_NUMBER) - 3.1.5 + 3.2.2 $(StageFolder) .$(Version) diff --git a/assets/SmartStoreNET.png b/assets/SmartStoreNET.png deleted file mode 100644 index 1fc5ca5409..0000000000 Binary files a/assets/SmartStoreNET.png and /dev/null differ diff --git a/assets/smartstore-icon.png b/assets/smartstore-icon.png new file mode 100644 index 0000000000..e6414db195 Binary files /dev/null and b/assets/smartstore-icon.png differ diff --git a/assets/smartstore-text.png b/assets/smartstore-text.png new file mode 100644 index 0000000000..25bf9558fb Binary files /dev/null and b/assets/smartstore-text.png differ diff --git a/build.bat b/build.bat index ef0004b549..94a6c72519 100644 --- a/build.bat +++ b/build.bat @@ -1,37 +1,45 @@ +SETLOCAL ENABLEDELAYEDEXPANSION + for /f "usebackq tokens=1* delims=: " %%i in (`lib\vswhere\vswhere -latest -requires Microsoft.Component.MSBuild`) do ( - if /i "%%i"=="installationPath" set InstallDir=%%j + if /i "%%i"=="installationPath" ( + set InstallDir=%%j + echo !InstallDir! + if exist "!InstallDir!\MSBuild\15.0\Bin\MSBuild.exe" ( + echo "Using MSBuild from Visual Studio 2017" + set msbuild="!InstallDir!\MSBuild\15.0\Bin\MSBuild.exe" + goto build + ) + if exist "!InstallDir!\MSBuild\Current\Bin\MSBuild.exe" ( + echo "Using MSBuild from Visual Studio 2019" + set msbuild="!InstallDir!\MSBuild\Current\Bin\MSBuild.exe" + goto build + ) + ) ) FOR %%b in ( - "%InstallDir%\Common7\Tools\VsMSBuildCmd.bat" - "%VS140COMNTOOLS%\Common7\Tools\vsvars32.bat" + "%VS150COMNTOOLS%\vsvars32.bat" + "%VS140COMNTOOLS%\vsvars32.bat" + "%VS120COMNTOOLS%\vsvars32.bat" + "%VS110COMNTOOLS%\vsvars32.bat" ) do ( if exist %%b ( - call %%b - goto findmsbuild + echo "Using MSBuild from %%b" + call %%b + set msbuild="msbuild" + goto build ) ) echo "Unable to detect suitable environment. Build may not succeed." -:findmsbuild - -SETLOCAL ENABLEDELAYEDEXPANSION - -if exist "%InstallDir%\MSBuild\15.0\Bin\MSBuild.exe" ( - if not defined MsBuildPath ( - SET "MsBuildPath=%InstallDir%\MSBuild\15.0\Bin\MsBuild.exe" - goto build - ) -) - - -echo "Unable to detect suitable MsBuild version (15.0). Build may not succeed." - :build + cd /d %~dp0 echo "Restoring NuGet packages" lib\nuget\nuget.exe restore "src\SmartStoreNET.sln" -call "!MsBuildPath!" SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %* +%msbuild% SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %* + +:end diff --git a/changelog.md b/changelog.md index c63fce1b9e..81357cca18 100644 --- a/changelog.md +++ b/changelog.md @@ -1,67 +1,155 @@ -# Release Notes +# Release Notes + +## SmartStore.NET 3.2.2 + +### New Features +* External category links: alternative links for categories in the main menu and in category listings. +* Page Builder: multistore support for stories. +* #1639 Shipping.ByTotal: Add option to calculate the shipping rate based on the net total instead of the gross total. +* #1537 Implement "Ratenzahlung powered by PayPal" provider. +* New Heidelpay payment plugin. +* Shop-Connector: mapping of product SKUs that differ between client shops. +* MegaMenu supports configuration of menu items + + +### Improvements +* Amazon Pay: PSD2. Strong customer authentication (SCA) upgrade. +* Updated *MaxMind.GeoIP* library and country database +* Removed HTML encoding for meta tags and document title. +* #1662 Quantity Units: added field for name pluralization. +* (SEO) combined SSL and canonical host 301 redirections to prevent successive redirects. +* (SEO) Activated GZIP compression for all dynamic and static responses. +* Set max. file upload size to 2 GB (globally) +* (Dev) Added a sample block for the *Page Builder* in DevTools plugin. +* Various other minor improvements. +* PayPal PLUS: paying by instalments can be selected in payment wall. +* Payone: send more information to API (e.g. shipping address) to stay as frictionless as possible for PSD2. +* PageBuilder: Added keyboard shortcuts +* #1582 Added structured data to reviews +* BMEcat: added support for import of specification attributes + +### Bugfixes +* Fixed wrong XML sitemap index numbering. +* #1646 Setting ManufacturerItemsToDisplayOnHomepage to 1 will not show any manufacturer. +* #1647 All manufacturers page: link of last character missing in alphabetical letter list. +* #1664 RTL: CMS menu issues in backend. +* #1677 Page Builder: moving and resizing blocks via drag & drop didn't work correctly in RTL. +* Do not adjust inventory twice when an order is cancelled and deleted. +* #1500 Fallback to fixed VAT rate instead of rate by region for countries that are not subject to vat. +* Fixed "The entity wrapper stored in the proxy does not reference the same proxy" when copying an export profile. +* Various other minor fixes. +* BMEcat: set TaxCategoryId & MainPictureId for newly imported products + + + +## SmartStore.NET 3.2.1 + +### Improvements +* Implemented file range support for CachedFileResult +* Sitemap should only include products visible for current customer, which is the search engine system customer +* Fixed uniform spelling for query string parameter "returnUrl" +* Fixed search engine issue (itemtype, "offers" required) on product detail page +* Summernote: solved html formatting issues thanks to "Beautifier" library +* Media middleware: simplified ETag generation & handling + +### Bugfixes + +* Mega Menu: Product rotator wasn't working +* Tiny Image: Fixed periodic IOException +* URL Rewriter: performance optimization +* Fixed more menu for main catalog navigation +* Fixed DateTime nor expressed as UTC in AzureFile & LocalFile +* Content Slider: Fixed slide display bug on surface and other touch devices +* Content Slider: Randomize slides > Value was stored in database but toggle button has always shown "off" +* Page Builder: Fixed slider bug for list blocks (brand, category, products) in Firefox +* Page Builder: fixes blurry background images in IOS +* Output Cache: action widgets didn't render when they were part of a widgetzone within a donut child action +* Multishop resolution failed under certain circumstances +* Fixed menu issue "collection was modified, enumeration operation may not execute" +* Fixed "The parameters dictionary contains a null entry for parameter 'catId' of non-nullable type 'System.Int32'" +* Fixed discount coupon sometimes wasn't applied +* Fixed "Value cannot be null. Parameter name: key" in order list +* Fixed topic link resolving by system name in multistore environment +* Async event consumers were not guaranteed to be awaited + ## SmartStore.NET 3.2 ### Highlights -* (Perf) #1202 Search: update to Lucene.NET 4.8 -* (Perf) Huge performance increase in discount resolution and calculation +* (NEW) **CMS 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) **CMS 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. ### New Features * **EmailReminder**: * Reminders for open shopping carts * Reminders for product reviews * Reminders to visit the shop after a long absence -* #1144 Enable multi server search index -* Made Topic ACL enabled -* Implemented paging & filtering for Topic grid -* Topics: added **IsPublished**, **Short Title** (link text) and **Intro** (teaser) properties. -* New storefront catalog options: **ShowSubCategoriesInSubPages**, **ShowDescriptionInSubPages** & **IncludeFeaturedProductsInSubPages** (Subpage = List index > 1 or any active filter). -* New security option: Use invisible reCAPTCHA +* **Scheduling** + * Task execution history + * Web farms: tasks can run decidedly on each machine of a web farm + * #1144 MegaSearch: multi server search index +* **Topics & Pages** + * Access control lists + * Grid paging & filtering + * New properties: added **IsPublished**, **Short Title** (link text) and **Intro** (teaser) properties. + * #1542: added options to set body CSS class and Html Id. +* **MegaSearch**: + * Supports searching for forum posts. + * #1172 Option to display related search terms on search page. + * Command to continue a previously aborted search index rebuild. +* **Import/export of**: + * Product tags + * Tier prices + * Product attribute options + * Product attribute combinations (update only) +* **Forum**: + * Added option to display a captcha on forum pages when creating or replying to a topic. + * #417 Restrict forum groups to specific customer roles. + * Added published property to forum topic and post. + * Added voting for forum posts. + * Several performance improvements. * **BeezUp**: * #1459 Add option to only submit one category name per product * Allow to specify export categories per product +* **Santander**: + * Added payment method for instalment purchase. + * Added financing calculator. +* Updated icon library to **Font Awesome 5 Free** with the possibility to activate pro version in the frontend. +* New storefront catalog options: **ShowSubCategoriesInSubPages**, **ShowDescriptionInSubPages** & **IncludeFeaturedProductsInSubPages** (Subpage = List index > 1 or any active filter). +* New security option: Use **invisible reCAPTCHA** * Wallet: Allow customer to choose whether refund should be submitted to his wallet. * Added option to display preview pictures in product lists * Added option to add multiple file versions to product download section * Added options for alternating price display (in badges) -* **Forum**: - * Added option to display a captcha on forum pages when creating or replying to a topic. - * #417 Restrict forum groups to specific customer roles. - * Added published property to forum topic and post. - * Added voting for forum posts. - * Several performance improvements. -* **MegaSearch**: - * Supports searching for forum posts. - * #1172 Option to display related search terms on search page. - * Command to continue a previously aborted search index rebuild. -* **Import/export of**: - * Product tags. - * Tier prices. - * Product attribute options. - * Product attribute combinations (update only). * Customer avatar: Letter with colored background if no avatar image was uploaded. * Viveum: Supports payment via "Virtual Account Brands" (e.g. PayPal). * Added options for alternating price display (in badges). * #1515 Poll: Add result tab with a list of answers and customers for a poll * BMEcat: Added export and import of product tags. -* Santander instalment purchase. -* #1542 Pages: Added options to set body CSS class and Html Id +* Web-API: Added endpoints for ProductSpecificationAttribute. +* Trusted Shops: Added new Trustbadge display for mobile devices +* (Dev) Added *sm:EfCommandTimeout* setting to web.config (was 30 sec. fixed, now configurable) ### Improvements * (Perf) Significantly increased query performance for products with a lot of category assignments (> 10). +* (Perf) Increased app startup speed by up to 20%. * (Perf) Ultra-fast file-based XML sitemap generation for extremely large catalogs (> 1M) * At least 10x faster * Generated files are saved on the hard disk now: a rebuild after an app restart is no longer necessary. * No exclusive locks during rebuilds anymore: if an (outdated) file already exists, it is returned instantly. +* (Dev) Enhanced pub/sub: new `IConsumer` marker interface for classes that contain one or more event message handlers. The generic `IConsumer` implements `IConsumer` but has been marked as `Obsolete`. * Better protection against XSS attacks +* Updated to Bootstrap version 4.1.3 * **Debitoor**: * Partially update customer instead of full update to avoid all fields being overwritten. * #1540 Place company name in front of customer name (according to address format of the particular country). * #1565 Add a button to manually create a draft invoice. * #1479 Show in messages the delivery time at the time of purchase -* #1184 Sort Current shopping carts & Current wishlists by ShoppingCartItem.CreatedOn. +* #1184 Sort current shopping carts & current wishlists by `ShoppingCartItem.CreatedOn`. * #1106 BMECat: import & export support for product keywords -* #1499 Added hint to forms indicating that fields with an asterisk (*) +* #1499 Added hint to forms indicating that fields with an asterisk (*) are required * Added filter for newsletter subscriber export by working language * Refactored download section * Enhanced EntityPicker to pick from customers, manufacturers & categories @@ -70,30 +158,35 @@ * Do not filter cookie using resources if cookie usage has not yet been consented to. * #1563 QueuedMessagesClearTask: add a setting for the age of the mails to be deleted. * #1569 Added a setting to show login note if no prices are displayed due to customer group permissions. -* PayPal PLUS: now up to 10 more third party payment methods are allowed by PayPal. +* PayPal PLUS: Up to 10 more third party payment methods are allowed by PayPal now. * #1560 Tell-a-Friend and anonymous user: render alert box and tell guest to login to use this function -* #1571 Compare products only shows one specification attribute option -* #1539 Allow signing in with both e-mail and username +* #1571 Compare products now shows all specification attribute options +* #1539 Signing in is now allowed with e-mail and username +* Trusted Shops: Trustbadge won't be displayed in Popups & Iframes anymore +* #1461 Admin Grid: filter dialog will be displayed entirely even when grid has no data to display +* Lots of minor enhancements for both frontend and admin theme +* Many other minor improvements ### Bugfixes -* In a multi-store environment, multiple topics with the same system name cannot be resolved reliably. +* In a multi-store environment, multiple topics with the same system name can now be resolved reliably. * **GMC**: * Export the product images if no attribute images are defined * Do not export the first image twice for additional images * Export image URL of full size image (not default size) for additional images - * Custom labels were not exported + * Custom labels are now being exported * Media middleware: 0-byte files should be treated as missing. -* Megamenu alpha/omega blends do not toggle correctly on touch devices +* Megamenu alpha/omega blends do now toggle correctly on touch devices * Summernote HTML editor exceeds parent container width when CodeMirror is activated * Only display a zero search hits warning if at least one filter is activated * #1436 Do not display delivery time in customer order completed messages * "ArgumentNullException: The value must not be NULL" if a topic is password protected * Tax by region: Fixes after inserting a tax rate country column shows "Unavailable" * #1014 Switching to default language keeps specific URL alias of current page -* Shipping by total: Fixes when inserting a record the country was not saved -* #1460 Editing of the customer title is missing on customer and address pages in the backend +* Shipping by total: When inserting a record the country now will be saved +* #1460 Editing of the customer title was missing on customer and address pages in the backend * #1447 Checkout button payment methods (Amazon, PayPal Express) won't work in conjunction with mandatory checkout attributes -* When creating a topic, the widget zone input shows System.String[] +* Fixed Autofac circularity error +* When creating a topic, the widget zone input has shown System.String[] * Switching the language always redirected to the home page if SEO friendly URLs was deactivated. * File upload of a checkout attribute was not stored on cart page. * Redirecting within checkout may have displayed an incorrect URL in the browser. @@ -102,25 +195,27 @@ * In a multi-store, message templates may have loaded the wrong disclaimer and conditions-of-use text. * NullReferenceException in manufacturer list when there is no manufacturer. * Wrong order of featured products on category page. -* #1504 Cart item price calculation wrong if attribute combinations with text types are involved. -* #1485 Dropdown list for product sorting does not work with Internet Explorer 11. -* #1468 Twitter authentication not working anymore. +* #1504 Cart item price calculation was wrong if attribute combinations with text types were involved. +* #1485 Dropdown list for product sorting didn't not work with Internet Explorer 11. +* #1468 Twitter authentication wasn't working anymore. * Newsletter subscription didn't work when customer privacy setting DisplayGdprConsentOnForms was turned off * Fixed social media image detection * Fixed redirection of bots when several languages were active * Region cannot be selected in checkout when entering a billing or shipping address * Fixed invalid conversion of "System.Int32" to "SmartStore.Core.Domain.Tax.VatNumberStatus" when placing an order * MegaMenu: Improved item rendering for third tier elements -* Product display order on category and manufacturer pages sometimes wrong when using linq search. +* Product display order on category and manufacturer pages was sometimes wrong when using linq search. * Debitoor: Wrong invoice total if a subtotal discount has been applied. * Import: Fixes invalid conversion "System.Double" to "SmartStore.Core.Domain.Catalog.QuantityControlType". * Topics: Fixes "Cannot insert duplicate key row in object 'dbo.UrlRecord' with unique index 'IX_UrlRecord_Slug'". * #1566 Santander: eliminate the 1 cent rounding difference at amountTotalNet. * Fixed redirection to the homepage for pages which are loaded while the application is restarted. * #1570 Filter option "Only deactivated customers" filters deleted instead of deactivated customers. -* UPS rates were way too high due to incorrect API request * #1475 select boxes must be wrapped on mobile devices if data-select-url is set -* Fixed the redirection to the homepage for pages which are loaded while the application is restarted +* Fixed the redirection to the homepage for pages which were loaded while the application was restarted +* Fixes product feeds expect a different base price formatting. +* #1369 Shopping cart shows "Discount code applied", although it is not applied due to a lower tier price. +* Many other fixes... ## SmartStore.NET 3.1.5 @@ -379,7 +474,7 @@ * Added config setting *sm:PdfEngineBaseUrl*. There are cases where the PDF converter exits with a network error, when it is unable to load automatically resolved URLs. * (Dev) Added *Retry* utility class * #1176 Admin > Product Search: It ain't possible to search for parts of a product name - + ### Bugfixes * #1145: Fixed HTTP 404 after switching language * Fixed null reference exception in product lists if sorting is not allowed @@ -667,7 +762,8 @@ * #495 Implement option to search product detail description by default ### Improvements -* (Perf) Implemented static caches for URL aliases and localized properties. Increases app startup and request speed by up to 30%. +* (Perf) Implemented static caches for URL aliases and localized properties. Increases +* and request speed by up to 30%. * (Perf) Significantly reduced number of database reads during product list rendering. Increases request speed by up to 10%. * (Perf) Implemented 2nd level cache for infrequently changed entities. Increases request speed by up to 10%. * TaskScheduler: Rewritten from scratch to be suitable for Web Farms @@ -1287,7 +1383,7 @@ * [SMNET-202] - SmartTabSelection mit verschachtelten Tabs fehlerhaft nach Reload einer Seite ### Improvement - + * [SMNET-13] - Attributwerte: der Text "Aufpreis" muss um "Minderpreis" erweitert werden. * [SMNET-15] - Umgestaltung der Darstellung der Staffelpreise (Popover ab dem fünften Element) * [SMNET-30] - MessageTemplates teilweise auf Englisch diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll index 2d948e052f..17bf4e2ad9 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 79225de4c7..b58492422f 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 2016")] +[assembly: AssemblyCopyright("Copyright © SmartStore AG 2019")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs index d9e092d65c..719cd1d6ff 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.1.5.0")] +[assembly: AssemblyVersion("3.2.2.0")] -[assembly: AssemblyFileVersion("3.1.5.0")] -[assembly: AssemblyInformationalVersion("3.1.5.0")] +[assembly: AssemblyFileVersion("3.2.2.0")] +[assembly: AssemblyInformationalVersion("3.2.2.0")] diff --git a/src/Libraries/SmartStore.Core/Async/AsyncLock.cs b/src/Libraries/SmartStore.Core/Async/AsyncLock.cs deleted file mode 100644 index b93da92afc..0000000000 --- a/src/Libraries/SmartStore.Core/Async/AsyncLock.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using SmartStore.Utilities; - -namespace SmartStore.Core.Async -{ - public sealed class AsyncLock : DisposableObject - { - private static readonly TimeSpan _noTimeout = TimeSpan.FromMilliseconds(-1); - private static readonly ConcurrentDictionary _keyLocks = new ConcurrentDictionary(); - - private readonly SemaphoreSlim _semaphore; - private readonly int _initialCount; - - public AsyncLock() - : this(1, int.MaxValue) - { - } - - public AsyncLock(int initialCount) - : this(initialCount, int.MaxValue) - { - } - - public AsyncLock(int initialCount, int maxCount) - { - _semaphore = new SemaphoreSlim(initialCount, maxCount); - _initialCount = initialCount; - } - - public static AsyncLock Acquire(object key) - { - Guard.NotNull(key, nameof(key)); - - var asyncLock = _keyLocks.GetOrAdd(key, x => new AsyncLock()); - - // Perf: once we have more than 100 lock objects in the storage, remove the idle ones. - if (_keyLocks.Count > 100) - { - // never remove the current one and the locked ones - var idles = _keyLocks.Where(kvp => kvp.Key != key && !kvp.Value.IsLockHeld).ToArray(); - foreach (var kvp in idles) - { - var value = kvp.Value; - // double check for held lock - if (!value.IsLockHeld) - { - _keyLocks.TryRemove(kvp.Key, out value); - } - } - } - - return asyncLock; - } - - public bool IsLockHeld - { - get { return _semaphore.CurrentCount < _initialCount; } - } - - public int Release() - { - return _semaphore.Release(); - } - - public int Release(int releaseCount) - { - return _semaphore.Release(releaseCount); - } - - public Task LockAsync() - { - return LockAsync(_noTimeout, CancellationToken.None); - } - - public Task LockAsync(TimeSpan timeout) - { - return LockAsync(timeout, CancellationToken.None); - } - - public Task LockAsync(CancellationToken cancellationToken) - { - return LockAsync(_noTimeout, cancellationToken); - } - - public Task LockAsync(TimeSpan timeout, CancellationToken cancellationToken) - { - var wait = _semaphore.WaitAsync(timeout, cancellationToken); - - return wait.IsCompleted - ? Task.FromResult(new ActionDisposable(() => _semaphore.Release()) as IDisposable) - : wait.ContinueWith((t, s) => new ActionDisposable(() => ((SemaphoreSlim)s).Release()) as IDisposable, - _semaphore, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - - protected override void OnDispose(bool disposing) - { - if (disposing) - _semaphore.Dispose(); - } - } -} diff --git a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs index 1fce2cc436..2751259eef 100644 --- a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs +++ b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,113 +9,166 @@ namespace SmartStore.Core.Async { - public static class AsyncRunner + using EventQueue = ConcurrentQueue>; + + public class AsyncRunner : IDisposable { - private static readonly BackgroundWorkHost _host = new BackgroundWorkHost(); + #region Instance members - /// - /// Gets the global cancellation token which signals the application shutdown - /// - public static CancellationToken AppShutdownCancellationToken + private ExclusiveSynchronizationContext CurrentContext; + private SynchronizationContext OldContext; + private int TaskCount; + + private AsyncRunner() { - get { return _host.ShutdownCancellationTokenSource.Token; } + OldContext = SynchronizationContext.Current; + CurrentContext = new ExclusiveSynchronizationContext(OldContext); + SynchronizationContext.SetSynchronizationContext(CurrentContext); + } + + public void Dispose() + { + try + { + CurrentContext.BeginMessageLoop(); + } + catch (Exception e) + { + throw e; + } + finally + { + 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 method to execute - public static void RunSync(Func func) + /// Task execute + public void Run(Task task, Action continuation = null) { - var oldContext = SynchronizationContext.Current; - var synch = new ExclusiveSynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(synch); - synch.Post(async _ => + CurrentContext.Post(async _ => { try { - await func(); + Increment(); + await task; + + continuation?.Invoke(task); } catch (Exception e) { - synch.InnerException = e; - throw; + CurrentContext.InnerException = e; } finally { - synch.EndMessageLoop(); + Decrement(); } }, null); - synch.BeginMessageLoop(); - - SynchronizationContext.SetSynchronizationContext(oldContext); } /// /// Executes an async Task method which has a TResult return type synchronously /// /// Return Type - /// Task method to execute - /// - public static TResult RunSync(Func> func) + /// Task to execute + public TResult Run(Task task, Action> continuation = null) { - var oldContext = SynchronizationContext.Current; - var synch = new ExclusiveSynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(synch); - TResult ret = default(TResult); - synch.Post(async _ => + var result = default(TResult); + + CurrentContext.Post(async _ => { try { - ret = await func(); + Increment(); + result = await task; + + continuation?.Invoke(task); } catch (Exception e) { - synch.InnerException = e; - throw; + CurrentContext.InnerException = e; } finally { - synch.EndMessageLoop(); + Decrement(); } }, null); - synch.BeginMessageLoop(); - SynchronizationContext.SetSynchronizationContext(oldContext); - return ret; - } - public static Task Run(Action action) - { - return Run(action, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + return result; } - public static Task Run(Action action, CancellationToken cancellationToken) + #endregion + + private static readonly BackgroundWorkHost _host = new BackgroundWorkHost(); + + /// + /// Gets the global cancellation token which signals the application shutdown + /// + public static CancellationToken AppShutdownCancellationToken { - return Run(action, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); + get { return _host.ShutdownCancellationTokenSource.Token; } } - public static Task Run(Action action, TaskCreationOptions options) + public static AsyncRunner Create() { - return Run(action, CancellationToken.None, options, TaskScheduler.Default); + return new AsyncRunner(); } - public static Task Run(Action action, CancellationToken cancellationToken, TaskCreationOptions options) + /// + /// 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) { - return Run(action, cancellationToken, options, TaskScheduler.Default); + using (var runner = new AsyncRunner()) + { + runner.Run(func(), continuation); + } } - public static Task Run(Action action, TaskScheduler scheduler) + /// + /// 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) { - return Run(action, CancellationToken.None, TaskCreationOptions.None, scheduler); + var result = default(TResult); + + using (var runner = new AsyncRunner()) + { + result = runner.Run(func(), continuation); + } + + return result; } - public static Task Run(Action action, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler) + public static Task Run( + Action action, + CancellationToken cancellationToken = default(CancellationToken), + TaskCreationOptions options = TaskCreationOptions.LongRunning, + TaskScheduler scheduler = null) { Guard.NotNull(action, nameof(action)); - Guard.NotNull(scheduler, nameof(scheduler)); var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; - options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool) var t = Task.Factory.StartNew(() => { var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; @@ -122,21 +176,24 @@ public static Task Run(Action action, Cancell { action(accessor.GetLifetimeScope(null), ct); } - }, ct, options, scheduler); + }, ct, options, scheduler ?? TaskScheduler.Default); _host.Register(t, ct); return t; } - public static Task Run(Action action, object state, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler) + 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)); - Guard.NotNull(scheduler, nameof(scheduler)); var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; - options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool) var t = Task.Factory.StartNew((o) => { @@ -145,47 +202,22 @@ public static Task Run(Action action, { action(accessor.GetLifetimeScope(null), ct, o); } - }, state, ct, options, scheduler); + }, state, ct, options, scheduler ?? TaskScheduler.Default); _host.Register(t, ct); return t; } - - - public static Task Run(Func function) - { - return Run(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - public static Task Run(Func function, CancellationToken cancellationToken) - { - return Run(function, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - public static Task Run(Func function, TaskCreationOptions options) - { - return Run(function, CancellationToken.None, options, TaskScheduler.Default); - } - - public static Task Run(Func function, CancellationToken cancellationToken, TaskCreationOptions options) - { - return Run(function, cancellationToken, options, TaskScheduler.Default); - } - - public static Task Run(Func function, TaskScheduler scheduler) - { - return Run(function, CancellationToken.None, TaskCreationOptions.None, scheduler); - } - - public static Task Run(Func function, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler) + public static Task Run( + Func function, + CancellationToken cancellationToken = default(CancellationToken), + TaskCreationOptions options = TaskCreationOptions.LongRunning, + TaskScheduler scheduler = null) { Guard.NotNull(function, nameof(function)); - Guard.NotNull(scheduler, nameof(scheduler)); var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; - options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool) var t = Task.Factory.StartNew(() => { @@ -194,21 +226,24 @@ public static Task Run(Func Run(Func function, object state, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler) + 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)); - Guard.NotNull(scheduler, nameof(scheduler)); var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token; - options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool) var t = Task.Factory.StartNew((o) => { @@ -217,19 +252,61 @@ 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; - readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false); - readonly Queue> _items = new Queue>(); + 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; } @@ -240,10 +317,7 @@ public override void Send(SendOrPostCallback d, object state) public override void Post(SendOrPostCallback d, object state) { - lock (_items) - { - _items.Enqueue(Tuple.Create(d, state)); - } + _items.Enqueue(Tuple.Create(d, state)); _workItemsWaiting.Set(); } @@ -256,26 +330,19 @@ public void BeginMessageLoop() { while (!_done) { - Tuple task = null; - lock (_items) + if (!_items.TryDequeue(out var task)) { - if (_items.Count > 0) - { - task = _items.Dequeue(); - } + _workItemsWaiting.WaitOne(); } - if (task != null) + else { task.Item1(task.Item2); - if (InnerException != null) // the method threw an exeption + + if (InnerException != null) // method threw an exeption { throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException); } } - else - { - _workItemsWaiting.WaitOne(); - } } } @@ -291,7 +358,7 @@ internal class BackgroundWorkHost : IRegisteredObject { private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource(); private int _numRunningWorkItems; - + public BackgroundWorkHost() { HostingEnvironment.RegisterObject(this); @@ -337,7 +404,7 @@ public void Register(Task work, CancellationToken cancellationToken) } _numRunningWorkItems++; } - + work.ContinueWith( WorkItemComplete, CancellationToken.None, @@ -353,7 +420,7 @@ private void WorkItemComplete(Task work) lock (this) { num = --_numRunningWorkItems; - isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested; + isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested; } if (num == 0 && isCancellationRequested) { diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScope.cs b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs new file mode 100644 index 0000000000..b705ab10b6 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SmartStore.Core.Caching +{ + public class CacheScope + { + private HashSet _dependencies; + + public CacheScope(string key) + { + Key = key; + } + + public string Key { get; private set; } + + public void AddDependency(string key) + { + if (key == null) + { + return; + } + + if (_dependencies == null) + { + _dependencies = new HashSet(); + } + + _dependencies.Add(key); + } + + public IEnumerable Dependencies + { + get + { + return _dependencies ?? Enumerable.Empty(); + } + } + } +} diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs new file mode 100644 index 0000000000..b04e0a7d45 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Utilities; + +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; + } + } +} diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs index c28fc3d0ff..855349106f 100644 --- a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs +++ b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs @@ -14,8 +14,9 @@ public interface ICacheManager /// /// 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); + T Get(string key, bool independent = false); /// /// Gets a cache item associated with the specified key or adds the item @@ -25,8 +26,9 @@ public interface ICacheManager /// 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. /// Cached item value - T Get(string key, Func acquirer, TimeSpan? duration = null); + T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false); /// /// Gets a cache item associated with the specified key or - if it doesn't exist in the cache - @@ -36,16 +38,18 @@ public interface ICacheManager /// 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. /// Cached item value - Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null); + Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = 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); + ISet GetHashSet(string key, Func> acquirer = null); /// /// Adds a cache item with the specified key @@ -53,7 +57,10 @@ public interface ICacheManager /// Key /// Value /// Absolute expiration time - void Put(string key, object value, TimeSpan? duration = null); + /// + /// 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 diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs new file mode 100644 index 0000000000..a640e6b200 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs @@ -0,0 +1,34 @@ +using System; + +namespace SmartStore.Core.Caching +{ + 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); + + /// + /// 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); + } +} diff --git a/src/Libraries/SmartStore.Core/Caching/ISet.cs b/src/Libraries/SmartStore.Core/Caching/ISet.cs index 59decb5d52..d0bff72bd7 100644 --- a/src/Libraries/SmartStore.Core/Caching/ISet.cs +++ b/src/Libraries/SmartStore.Core/Caching/ISet.cs @@ -6,6 +6,7 @@ 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); diff --git a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs index 89a2c707e6..a00224784d 100644 --- a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs +++ b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs @@ -4,27 +4,37 @@ using System.Threading.Tasks; using System.Collections.Concurrent; using System.Threading; -using SmartStore.Core.Async; -using System.Collections; using System.Collections.Generic; using SmartStore.Utilities; using System.Text.RegularExpressions; +using System.Diagnostics; +using SmartStore.Utilities.Threading; +using SmartStore.Core.Infrastructure.DependencyManagement; 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 MemoryCache _cache; + private readonly Work _scopeAccessor; + private MemoryCache _cache; private readonly ConcurrentDictionary _keyLocks = new ConcurrentDictionary(); - public MemoryCacheManager() + public MemoryCacheManager(Work scopeAccessor) { - _cache = new MemoryCache("SmartStore"); + _scopeAccessor = scopeAccessor; + _cache = CreateCache(); + } + + private MemoryCache CreateCache() + { + return new MemoryCache("SmartStore"); } public bool IsDistributedCache @@ -32,7 +42,7 @@ public bool IsDistributedCache get { return false; } } - private bool TryGet(string key, out T value) + private bool TryGet(string key, bool independent, out T value) { value = default(T); @@ -40,6 +50,12 @@ private bool TryGet(string key, out T value) if (obj != null) { + // Make the parent scope's entry depend on this + if (!independent) + { + _scopeAccessor.Value.PropagateKey(key); + } + if (obj.Equals(FakeNull)) { return true; @@ -52,65 +68,74 @@ private bool TryGet(string key, out T value) return false; } - public T Get(string key) + public T Get(string key, bool independent = false) { - T value; - TryGet(key, out value); - + TryGet(key, independent, out T value); return value; } - public T Get(string key, Func acquirer, TimeSpan? duration = null) + public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false) { - T value; - - if (TryGet(key, out value)) + if (TryGet(key, independent, out T value)) { return value; } - lock (String.Intern(key)) + if (_scopeAccessor.Value.HasScope(key)) { - // atomic operation must be outer locked - if (!TryGet(key, out value)) + throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key)); + } + + // Get the (semaphore) locker specific to this key + using (KeyedLock.Lock("cache:" + key, TimeSpan.FromMinutes(1))) + { + // Atomic operation must be outer locked + if (!TryGet(key, independent, out value)) { - value = acquirer(); - Put(key, value, duration); - return value; + using (_scopeAccessor.Value.BeginScope(key)) + { + value = acquirer(); + Put(key, value, duration, _scopeAccessor.Value.Current.Dependencies); + return value; + } } } return value; } - public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null) + public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false) { - T value; - - if (TryGet(key, out value)) + if (TryGet(key, independent, out T value)) { return value; } - // get the async (semaphore) locker specific to this key - var keyLock = AsyncLock.Acquire(key); + if (_scopeAccessor.Value.HasScope(key)) + { + throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key)); + } - using (await keyLock.LockAsync()) + // Get the async (semaphore) locker specific to this key + using (await KeyedLock.LockAsync("cache:" + key, TimeSpan.FromMinutes(1))) { - if (!TryGet(key, out value)) + if (!TryGet(key, independent, out value)) { - value = await acquirer().ConfigureAwait(false); - Put(key, value, duration); - return value; + using (_scopeAccessor.Value.BeginScope(key)) + { + value = await acquirer(); + Put(key, value, duration, _scopeAccessor.Value.Current.Dependencies); + return value; + } } } return value; } - public void Put(string key, object value, TimeSpan? duration = null) + public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null) { - _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration)); + _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration, dependencies)); } public bool Contains(string key) @@ -135,39 +160,53 @@ public IEnumerable Keys(string pattern) } var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase); - return keys.Where(x => wildcard.IsMatch(x)); + return keys.Where(x => wildcard.IsMatch(x)).ToArray(); } public int RemoveByPattern(string pattern) { - var keysToRemove = Keys(pattern); - int count = 0; - lock (_cache) { + var keysToRemove = Keys(pattern); + int count = 0; + // lock atomic operation foreach (string key in keysToRemove) { _cache.Remove(key); count++; } - } - return count; + return count; + } } public void Clear() { - RemoveByPattern("*"); - } + // 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) + public virtual ISet GetHashSet(string key, Func> acquirer = null) { - var set = Get(key, () => new MemorySet(this)); - return set; + 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) + private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration, IEnumerable dependencies) { var absoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration; @@ -175,16 +214,35 @@ private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration) { 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) { diff --git a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs index 51254daa26..c88cacd703 100644 --- a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs +++ b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs @@ -16,6 +16,11 @@ public MemorySet(ICacheManager cache) _cache = cache; } + public void AddRange(IEnumerable items) + { + base.UnionWith(items); + } + public bool Move(string destinationKey, string item) { if (this.Contains(item)) diff --git a/src/Libraries/SmartStore.Core/Caching/NullCache.cs b/src/Libraries/SmartStore.Core/Caching/NullCache.cs index ae8d19b285..e75abfd91d 100644 --- a/src/Libraries/SmartStore.Core/Caching/NullCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/NullCache.cs @@ -23,12 +23,12 @@ public bool IsDistributedCache get { return false; } } - public T Get(string key) + public T Get(string key, bool independent = false) { return default(T); } - public T Get(string key, Func acquirer, TimeSpan? duration = null) + public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false) { if (acquirer == null) { @@ -37,7 +37,7 @@ public T Get(string key, Func acquirer, TimeSpan? duration = null) return acquirer(); } - public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null) + public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false) { if (acquirer == null) { @@ -47,12 +47,12 @@ public Task GetAsync(string key, Func> acquirer, TimeSpan? duratio } - public ISet GetHashSet(string key) + public ISet GetHashSet(string key, Func> acquirer = null) { return new MemorySet(this); } - public void Put(string key, object value, TimeSpan? duration = null) + public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null) { } diff --git a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs index 3929c80b79..86d5b00249 100644 --- a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs +++ b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs @@ -4,6 +4,8 @@ using System.Collections.Concurrent; using System.Linq; using Newtonsoft.Json; +using SmartStore.Utilities; +using System.Threading; namespace SmartStore.Collections { @@ -14,40 +16,65 @@ namespace SmartStore.Collections /// The type of value. [JsonConverter(typeof(ConcurrentMultiMapConverter))] [Serializable] - public class ConcurrentMultimap : IEnumerable>> + public class ConcurrentMultimap : IEnumerable>> { - private readonly ConcurrentDictionary> _items; - private static readonly Func, IProducerConsumerCollection> _bagCreator = (col) => new ConcurrentBag(col); + private readonly ConcurrentDictionary> _dict; + private readonly Func, ICollection> _collectionCreator; public ConcurrentMultimap() - : this(null, null) + : this(null, null, null) { } public ConcurrentMultimap(IEqualityComparer comparer) - : this(null, comparer) + : this(null, comparer, null) { } - public ConcurrentMultimap(IEnumerable>> items) - : this(items, null) + public ConcurrentMultimap(IEnumerable>> items) + : this(items, null, null) { // for serialization } - public ConcurrentMultimap(IEnumerable>> items, IEqualityComparer comparer) + public ConcurrentMultimap( + IEnumerable>> items, + IEqualityComparer comparer) + : this(items, comparer, null) { - _items = new ConcurrentDictionary>(comparer ?? EqualityComparer.Default); + // 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; - if (items != null) + foreach (var item in items) { - foreach (var kvp in items) - { - _items.TryAdd(kvp.Key, _bagCreator(kvp.Value ?? Enumerable.Empty())); - } + 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. /// @@ -55,7 +82,7 @@ public int Count { get { - return _items.Keys.Count; + return _dict.Keys.Count; } } @@ -66,29 +93,33 @@ public int TotalValueCount { get { - return this._items.Values.Sum(x => x.Count); + 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 IProducerConsumerCollection this[TKey key] + public virtual SyncedCollection this[TKey key] { get { - IProducerConsumerCollection value; - if (!_items.TryGetValue(key, out value)) - { - value = _bagCreator(Enumerable.Empty()); - if (!_items.TryAdd(key, value)) - { - value = null; - } - } - - return value; + GetOrCreateValues(key, null, out var col); + return col; } } @@ -97,15 +128,15 @@ public virtual IProducerConsumerCollection this[TKey key] /// public virtual ICollection Keys { - get { return _items.Keys; } + get { return _dict.Keys; } } /// - /// Gets the collection of collections of values. + /// Gets all value collections. /// - public virtual ICollection> Values + public virtual ICollection> Values { - get { return _items.Values; } + get { return _dict.Values; } } /// @@ -115,7 +146,10 @@ public virtual ICollection> Values /// The value. public virtual void TryAdd(TKey key, TValue value) { - this[key].TryAdd(value); + if (!GetOrCreateValues(key, new[] { value }, out var col)) + { + col.Add(value); + } } /// @@ -127,18 +161,63 @@ public virtual void TryAddRange(TKey key, IEnumerable values) { Guard.NotNull(values, nameof(values)); - values.Each(x => this[key].TryAdd(x)); + if (!GetOrCreateValues(key, values, out var col)) + { + col.AddRange(values); + } } /// - /// Attempts to remove all values for the specified key. + /// 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) + public virtual bool TryRemoveAll(TKey key, out SyncedCollection collection) { - IProducerConsumerCollection collection; - return _items.TryRemove(key, out collection); + return _dict.TryRemove(key, out collection); } /// @@ -146,7 +225,7 @@ public virtual bool TryRemoveAll(TKey key) /// public virtual void Clear() { - _items.Clear(); + _dict.Clear(); } /// @@ -156,7 +235,7 @@ public virtual void Clear() /// True if the multimap has one or more values for the specified key, otherwise false. public virtual bool ContainsKey(TKey key) { - return _items.ContainsKey(key); + return _dict.ContainsKey(key); } /// @@ -167,7 +246,7 @@ public virtual bool ContainsKey(TKey key) /// True if the multimap contains such a value; otherwise, false. public virtual bool ContainsValue(TKey key, TValue value) { - return _items.ContainsKey(key) && _items[key].Contains(value); + return _dict.ContainsKey(key) && _dict[key].Contains(value); } /// @@ -183,12 +262,26 @@ IEnumerator IEnumerable.GetEnumerator() /// Returns an enumerator that iterates through the multimap. /// /// An object that can be used to iterate through the multimap. - public virtual IEnumerator>> GetEnumerator() + public virtual IEnumerator>> GetEnumerator() { - foreach (var pair in _items) + 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) diff --git a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs b/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs deleted file mode 100644 index 114c4e24bf..0000000000 --- a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace SmartStore.Collections -{ - public class MostRecentlyUsedList : IEnumerable - { - private readonly int _maxSize; - private readonly List _mru; - - public MostRecentlyUsedList(int maxSize) - { - _maxSize = maxSize; - _mru = new List(); - } - - public MostRecentlyUsedList(IEnumerable collection, int maxSize) - { - _maxSize = maxSize; - _mru = collection.ToList(); - - Normalize(); - } - - public MostRecentlyUsedList(string collection, int maxSize) - { - _maxSize = maxSize; - _mru = collection.SplitSafe(Delimiter).Cast().ToList(); - - Normalize(); - } - - public MostRecentlyUsedList(IEnumerable collection, T newItem, int maxSize) - { - _maxSize = maxSize; - _mru = collection.ToList(); - - Add(newItem); - } - - public MostRecentlyUsedList(string collection, T newItem, int maxSize) - { - _maxSize = maxSize; - _mru = collection.SplitSafe(Delimiter).Cast().Distinct().ToList(); - - Add(newItem); - } - - public static string Delimiter { get { return ";"; } } - - private void Normalize() - { - if (_maxSize >= 0) - { - while (_mru.Count > _maxSize) - _mru.RemoveAt(_mru.Count - 1); - } - } - - public IEnumerator GetEnumerator() - { - return _mru.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _mru.GetEnumerator(); - } - - public override string ToString() - { - return string.Join(Delimiter, _mru); - } - - public T this[int key] - { - get - { - return _mru[key]; - } - set - { - _mru[key] = value; - } - } - - public int Count { get { return _mru.Count; } } - - public void Add(T item) - { - int i = _mru.IndexOf(item); - if (i > -1) - _mru.RemoveAt(i); - - _mru.Insert(0, item); - - Normalize(); - } - } -} diff --git a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs index 6c8f85fc78..738ffab5fa 100644 --- a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs +++ b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Concurrent; using System.Linq; using System.Linq.Expressions; using Newtonsoft.Json; @@ -17,11 +16,12 @@ namespace SmartStore.Collections [Serializable] public class Multimap : IEnumerable>> { - private readonly IDictionary> _items; + private readonly IDictionary> _dict; private readonly Func, ICollection> _collectionCreator; private readonly bool _isReadonly = false; - private 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() : this(EqualityComparer.Default) @@ -30,8 +30,8 @@ public Multimap() public Multimap(IEqualityComparer comparer) { - _items = new Dictionary>(comparer ?? EqualityComparer.Default); - _collectionCreator = _defaultCollectionCreator; + _dict = new Dictionary>(comparer ?? EqualityComparer.Default); + _collectionCreator = DefaultCollectionCreator; } public Multimap(Func, ICollection> collectionCreator) @@ -49,7 +49,7 @@ internal Multimap(IDictionary> dictionary, Func> dictionary, bool isRea { Guard.NotNull(dictionary, nameof(dictionary)); - _items = dictionary; + _dict = dictionary; if (isReadonly && dictionary != null) { @@ -81,21 +81,20 @@ public Multimap(IEnumerable>> items, IEqu // for serialization Guard.NotNull(items, nameof(items)); - _items = new Dictionary>(comparer ?? EqualityComparer.Default); - _collectionCreator = _defaultCollectionCreator; + _dict = new Dictionary>(comparer ?? EqualityComparer.Default); if (items != null) { foreach (var kvp in items) { - _items[kvp.Key] = CreateCollection(kvp.Value); + _dict[kvp.Key] = CreateCollection(kvp.Value); } } } protected virtual ICollection CreateCollection(IEnumerable values) { - return (_collectionCreator ?? _defaultCollectionCreator)(values ?? Enumerable.Empty()); + return (_collectionCreator ?? DefaultCollectionCreator)(values ?? Enumerable.Empty()); } /// @@ -105,7 +104,7 @@ public int Count { get { - return this._items.Keys.Count; + return this._dict.Keys.Count; } } @@ -116,7 +115,7 @@ public int TotalValueCount { get { - return this._items.Values.Sum(x => x.Count); + return this._dict.Values.Sum(x => x.Count); } } @@ -128,15 +127,15 @@ public virtual ICollection this[TKey key] { get { - if (!_items.ContainsKey(key)) + if (!_dict.ContainsKey(key)) { if (!_isReadonly) - _items[key] = CreateCollection(null); + _dict[key] = CreateCollection(null); else return null; } - return _items[key]; + return _dict[key]; } } @@ -145,15 +144,15 @@ public virtual ICollection this[TKey key] /// public virtual ICollection Keys { - get { return _items.Keys; } + get { return _dict.Keys; } } - /// - /// Gets the collection of collections of values. - /// + /// + /// Gets all value collections. + /// public virtual ICollection> Values { - get { return _items.Values; } + get { return _dict.Values; } } public IEnumerable Find(TKey key, Expression> predicate) @@ -161,9 +160,9 @@ public IEnumerable Find(TKey key, Expression> predica Guard.NotNull(key, nameof(key)); Guard.NotNull(predicate, nameof(predicate)); - if (_items.ContainsKey(key)) + if (_dict.ContainsKey(key)) { - return _items[key].Where(predicate.Compile()); + return _dict[key].Where(predicate.Compile()); } return Enumerable.Empty(); @@ -188,7 +187,10 @@ public virtual void Add(TKey key, TValue value) /// The values. public virtual void AddRange(TKey key, IEnumerable values) { - CheckNotReadonly(); + if (values == null || !values.Any()) + return; + + CheckNotReadonly(); this[key].AddRange(values); } @@ -203,12 +205,12 @@ public virtual bool Remove(TKey key, TValue value) { CheckNotReadonly(); - if (!_items.ContainsKey(key)) + if (!_dict.ContainsKey(key)) return false; - bool result = _items[key].Remove(value); - if (_items[key].Count == 0) - _items.Remove(key); + bool result = _dict[key].Remove(value); + if (_dict[key].Count == 0) + _dict.Remove(key); return result; } @@ -221,7 +223,7 @@ public virtual bool Remove(TKey key, TValue value) public virtual bool RemoveAll(TKey key) { CheckNotReadonly(); - return _items.Remove(key); + return _dict.Remove(key); } /// @@ -230,7 +232,7 @@ public virtual bool RemoveAll(TKey key) public virtual void Clear() { CheckNotReadonly(); - _items.Clear(); + _dict.Clear(); } /// @@ -240,7 +242,7 @@ public virtual void Clear() /// True if the multimap has one or more values for the specified key, otherwise false. public virtual bool ContainsKey(TKey key) { - return _items.ContainsKey(key); + return _dict.ContainsKey(key); } /// @@ -251,7 +253,7 @@ public virtual bool ContainsKey(TKey key) /// True if the multimap contains such a value; otherwise, false. public virtual bool ContainsValue(TKey key, TValue value) { - return _items.ContainsKey(key) && _items[key].Contains(value); + return _dict.ContainsKey(key) && _dict[key].Contains(value); } /// @@ -269,7 +271,7 @@ IEnumerator IEnumerable.GetEnumerator() /// An object that can be used to iterate through the multimap. public virtual IEnumerator>> GetEnumerator() { - foreach (KeyValuePair> pair in _items) + foreach (KeyValuePair> pair in _dict) yield return pair; } diff --git a/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs new file mode 100644 index 0000000000..d8100effd8 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +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 + } +} diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs index 19cf141de9..00034d0778 100644 --- a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs +++ b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs @@ -227,7 +227,17 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s if (GetPropValue("Metadata", value) is IDictionary dict && dict.Count > 0) { writer.WritePropertyName("Metadata"); - serializer.Serialize(writer, dict); + + var typeNameHandling = serializer.TypeNameHandling; + try + { + serializer.TypeNameHandling = TypeNameHandling.All; + serializer.Serialize(writer, dict); + } + finally + { + serializer.TypeNameHandling = typeNameHandling; + } } // Children diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs index 8dd7bee9b2..8264db12e1 100644 --- a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs +++ b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs @@ -40,13 +40,10 @@ public object Id { var map = GetIdNodeMap(); - if (_id != value) + if (_id != null && map.ContainsKey(_id)) { - if (_id != null && map.ContainsKey(_id)) - { - // Remove old id from map - map.Remove(_id); - } + // Remove old id from map + map.Remove(_id); } if (value != null) @@ -142,7 +139,12 @@ public TMetadata GetMetadata(string key, bool recursive = true) break; } - return (TMetadata)metadata; + if (metadata != null) + { + return (TMetadata)metadata; + } + + return default(TMetadata); } private bool TryGetMetadataForNode(TreeNodeBase node, string key, out object metadata) diff --git a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs new file mode 100644 index 0000000000..db54bf8936 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs @@ -0,0 +1,106 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +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) + { + 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); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs index dea60d7a62..85419958d3 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs @@ -93,8 +93,7 @@ public static FastActivator[] GetActivators(Type type) private static FastActivator[] GetActivatorsCore(Type type) { - FastActivator[] activators; - if (!_activatorsCache.TryGetValue(type, out activators)) + if (!_activatorsCache.TryGetValue(type, out FastActivator[] activators)) { var candidates = GetCandidateConstructors(type); activators = candidates.Select(c => new FastActivator(c)).ToArray(); diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs index d88dbc43ae..ba43358e1a 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs @@ -136,21 +136,6 @@ public static object Invoke(object obj, string methodName, Type[] parameterTypes return invoker.Invoke(obj, parameters ?? Array.Empty()); } - /// - /// Creates and caches a fast method invoker. - /// - /// Method info instance to create an invoker for. - /// The fast method invoker. - public static FastInvoker GetInvoker(MethodInfo methodInfo) - { - Guard.NotNull(methodInfo, nameof(methodInfo)); - - return GetInvoker( - methodInfo.DeclaringType, - methodInfo.Name, - methodInfo.GetParameters().Select(x => x.ParameterType).ToArray()); - } - /// /// Creates and caches a fast method invoker. /// @@ -192,6 +177,26 @@ public static FastInvoker GetInvoker(Type type, string methodName, params Type[] 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 @@ -208,20 +213,80 @@ private static MethodInfo FindMatchingMethod(Type type, string methodName, Type[ #endregion - class MethodKey : Tuple + 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 { - public MethodKey(Type type, string methodName, int parametersHash) - : base(type, methodName, parametersHash) + 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 Type Type { get { return base.Item1; } } - public string MethodName { get { return base.Item2; } } - public int ParameterHash { get { return base.Item3; } } - public static MethodKey Create(Type type, string methodName, IEnumerable parameterTypes) + public override int GetHashCode() { - var hashCombiner = HashCodeCombiner.Start().Add(parameterTypes); - return new MethodKey(type, methodName, hashCombiner.CombinedHash); + return _method.GetHashCode(); } } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs index 5687317362..ae60b58488 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs @@ -125,7 +125,7 @@ public override object ConvertTo(CultureInfo culture, string format, object valu if (value is IEnumerable enumerable) { - // we don't use string.Join() because it doesn't support invariant culture + // 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)); diff --git a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs index 1e467280a9..00d96fe9ba 100644 --- a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs +++ b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs @@ -11,6 +11,7 @@ namespace SmartStore.Core.Configuration /// into single properties. /// [AttributeUsage(AttributeTargets.Class)] + [Obsolete] public class JsonPersistAttribute : Attribute { } diff --git a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs index 93a7552c50..7d3b433e8e 100644 --- a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs +++ b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace SmartStore.Core.Data { @@ -89,7 +90,12 @@ public int Commit() return _ctx.SaveChanges(); } - public void Dispose() + public async Task CommitAsync() + { + return await _ctx.SaveChangesAsync(); + } + + public void Dispose() { _ctx.AutoDetectChangesEnabled = _autoDetectChangesEnabled; _ctx.ProxyCreationEnabled = _proxyCreationEnabled; diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs index 0700430685..39126f48f8 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs @@ -8,7 +8,6 @@ namespace SmartStore.Core.Data.Hooks { public class DefaultDbHookHandler : IDbHookHandler { - private readonly IEnumerable> _hooks; private readonly IList> _saveHooks; private readonly Multimap _hooksRequestCache = new Multimap(); @@ -27,7 +26,6 @@ public class DefaultDbHookHandler : IDbHookHandler public DefaultDbHookHandler(IEnumerable> hooks) { - _hooks = hooks; _saveHooks = hooks.Where(x => x.Metadata.IsLoadHook == false).ToList(); } @@ -35,7 +33,7 @@ public ILogger Logger { get; set; - } + } = NullLogger.Instance; public bool HasImportantSaveHooks() { diff --git a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs index a3d54c9f75..5041bebcdf 100644 --- a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Linq.Expressions; using System.Text; +using System.Threading.Tasks; namespace SmartStore.Core.Data { @@ -95,7 +96,57 @@ public static int DeleteAll(this IRepository rs, Expression> return count; } - public static IQueryable Get( + /// + /// 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, diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs index 5a895b03a8..d74e7c0c73 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs @@ -42,6 +42,12 @@ public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDele [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. + /// + [DataMember] + public string ExternalLink { get; set; } + /// /// Gets or sets a text displayed in a badge next to the category within menus /// diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs index 14ba3736a4..c07c9fe69e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs @@ -10,7 +10,8 @@ public interface ICategoryNode : ILocalizedEntity, ISlugSupported, IAclSupported { int ParentCategoryId { get; } string Name { get; } - string Alias { get; } + string ExternalLink { get; } + string Alias { get; } int? PictureId { get; } bool Published { get; } int DisplayOrder { get; } @@ -25,7 +26,8 @@ public class CategoryNode : ICategoryNode public int Id { get; set; } public int ParentCategoryId { get; set; } public string Name { get; set; } - public string Alias { get; set; } + public string ExternalLink { get; set; } + public string Alias { get; set; } public int? PictureId { get; set; } public bool Published { get; set; } public int DisplayOrder { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs index 354720c7ea..60373a2d15 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs @@ -1,18 +1,10 @@ -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.Catalog +namespace SmartStore.Core.Domain.Catalog { - public partial class ProductBundleItemData + public partial class ProductBundleItemData { public ProductBundleItemData(ProductBundleItem item) { - Item = item; // can be null... test with IsValid + Item = item; // Can be null. } public ProductBundleItem Item { get; private set; } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs index 5cb9acaa5f..ee5adfe594 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs @@ -33,15 +33,17 @@ public partial class ProductCategory : BaseEntity /// [DataMember] public int DisplayOrder { get; set; } - + /// /// Gets the category /// + [DataMember] public virtual Category Category { get; set; } /// /// Gets the product /// + [DataMember] public virtual Product Product { get; set; } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs index 087a1f9351..73edaec790 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs @@ -37,11 +37,13 @@ public partial class ProductManufacturer : BaseEntity /// /// Gets or sets the manufacturer /// + [DataMember] public virtual Manufacturer Manufacturer { get; set; } /// /// Gets or sets the product /// + [DataMember] public virtual Product Product { get; set; } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs index 132e767131..eb8595b3af 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs @@ -46,6 +46,7 @@ public partial class ProductSpecificationAttribute : BaseEntity /// /// Gets or sets the specification attribute option /// + [DataMember] public virtual SpecificationAttributeOption SpecificationAttributeOption { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Cms/MenuItemRecord.cs b/src/Libraries/SmartStore.Core/Domain/Cms/MenuItemRecord.cs new file mode 100644 index 0000000000..9aab96cc35 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Cms/MenuItemRecord.cs @@ -0,0 +1,117 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using SmartStore.Core.Domain.Localization; + +namespace SmartStore.Core.Domain.Cms +{ + /// + /// Represents a menu item. + /// + [Table("MenuItemRecord")] + public class MenuItemRecord : BaseEntity, ILocalizedEntity + { + /// + /// Gets or sets the menu identifier. + /// + [Required] + public int MenuId { get; set; } + + /// + /// Gets the menu. + /// + [ForeignKey("MenuId")] + public virtual MenuRecord Menu { get; set; } + + /// + /// Gets or sets the parent menu item identifier. 0 if the item has no parent. + /// + [Index("IX_MenuItem_ParentItemId")] + public int ParentItemId { get; set; } + + /// + /// Gets or sets the provider name. + /// + [StringLength(100)] + public string ProviderName { get; set; } + + /// + /// Gets or sets the model. + /// + [MaxLength] + public string Model { get; set; } + + /// + /// Gets or sets the title. + /// + [StringLength(400)] + public string Title { get; set; } + + /// + /// Gets or sets the short description. It is used for the link title attribute. + /// + [StringLength(400)] + public string ShortDescription { get; set; } + + /// + /// Gets or sets permission names. + /// + [MaxLength] + public string PermissionNames { get; set; } + + /// + /// Gets or sets a value indicating whether the menu item is published. + /// + [Index("IX_MenuItem_Published")] + public bool Published { get; set; } = true; + + /// + /// Gets or sets the display order. + /// + [Index("IX_MenuItem_DisplayOrder")] + public int DisplayOrder { get; set; } + + /// + /// Gets or sets a value indicating whether the menu item has a divider or a group header. + /// + public bool BeginGroup { get; set; } + + /// + /// If selected and this menu item has children, the menu will initially appear expanded. + /// + public bool ShowExpanded { get; set; } + + /// + /// Gets or sets the no-follow link attribute. + /// + public bool NoFollow { get; set; } + + /// + /// Gets or sets the blank target link attribute. + /// + public bool NewWindow { get; set; } + + /// + /// Gets or sets fontawesome icon class. + /// + [StringLength(100)] + public string Icon { get; set; } + + /// + /// Gets or sets fontawesome icon style. + /// + [StringLength(10)] + public string Style { get; set; } + + /// + /// Gets or sets HTML id attribute. + /// + [StringLength(100)] + public string HtmlId { get; set; } + + /// + /// Gets or sets the CSS class. + /// + [StringLength(100)] + public string CssClass { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs b/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs new file mode 100644 index 0000000000..c8ebedf1c1 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Security; +using SmartStore.Core.Domain.Stores; + +namespace SmartStore.Core.Domain.Cms +{ + /// + /// Represents a menu. + /// + [Table("MenuRecord")] + public class MenuRecord : BaseEntity, ILocalizedEntity, IStoreMappingSupported, IAclSupported + { + private ICollection _items; + + /// + /// /// Gets or sets the menu items. + /// + public virtual ICollection Items + { + get { return _items ?? (_items = new HashSet()); } + protected set { _items = value; } + } + + /// + /// Gets or sets the system name. It identifies the menu. + /// + [Required, StringLength(400), Index("IX_Menu_SystemName_IsSystemMenu", Order = 0)] + public string SystemName { get; set; } + + /// + /// Gets or sets the value indicating whether this menu is deleteable by a user. + /// + [Index("IX_Menu_SystemName_IsSystemMenu", Order = 1)] + public bool IsSystemMenu { get; set; } + + /// + /// Gets or sets the menu template name. + /// + [StringLength(400)] + public string Template { get; set; } + + /// + /// Gets or sets the widget zone name. + /// + [StringLength(4000)] + public string WidgetZone { get; set; } + + /// + /// Gets or sets the title. + /// + [StringLength(400)] + public string Title { get; set; } + + /// + /// Gets or sets a value indicating whether the menu is published. + /// + [Index("IX_Menu_Published")] + public bool Published { get; set; } = true; + + /// + /// Gets or sets the order for widget registration. + /// + public int DisplayOrder { get; set; } + + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores. + /// + [Index("IX_Menu_LimitedToStores")] + public bool LimitedToStores { get; set; } + + /// + /// Gets or sets a value indicating whether the entity is subject to ACL. + /// + [Index("IX_Menu_SubjectToAcl")] + public bool SubjectToAcl { get; set; } + + /// + /// Helper function to get the comma-separated WidgetZone property as an enumerable of strings. + /// + /// Widget zones. + public IEnumerable GetWidgetZones() + { + if (WidgetZone.IsEmpty()) + { + return Enumerable.Empty(); + } + + return WidgetZone.Trim().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()); + } + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs index 46980652d0..1a45382602 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs @@ -4,50 +4,34 @@ namespace SmartStore.Core.Domain.Common { public class SocialSettings : ISettings { - public SocialSettings() - { - ShowSocialLinksInFooter = true; - FacebookLink = "#"; - GooglePlusLink = "#"; - TwitterLink = "#"; - PinterestLink = "#"; - //YoutubeLink = "#"; - //InstagramLink = "#"; - } - - /// + /// /// Gets or sets the value that determines whether social links should be show in the footer /// - public bool ShowSocialLinksInFooter { get; set; } + public bool ShowSocialLinksInFooter { get; set; } = true; /// /// Gets or sets the facebook link /// - public string FacebookLink { get; set; } - - /// - /// Gets or sets the google plus link - /// - public string GooglePlusLink { get; set; } + public string FacebookLink { get; set; } = "#"; /// /// Gets or sets the twitter link /// - public string TwitterLink { get; set; } + public string TwitterLink { get; set; } = "#"; /// /// Gets or sets the pinterest link /// - public string PinterestLink { get; set; } + public string PinterestLink { get; set; } = "#"; /// /// Gets or sets the youtube link /// - public string YoutubeLink { get; set; } + public string YoutubeLink { get; set; } = "#"; - /// - /// Gets or sets the instagram link - /// - public string InstagramLink { get; set; } - } + /// + /// Gets or sets the instagram link + /// + public string InstagramLink { get; set; } = "#"; + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs b/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs index f10f0543c3..4e671a5384 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs @@ -1,6 +1,5 @@ -using System; -using SmartStore.Core.Domain.Localization; using System.Runtime.Serialization; +using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Directory { @@ -16,6 +15,12 @@ public partial class QuantityUnit : BaseEntity, ILocalizedEntity [DataMember] public string Name { get; set; } + /// + /// Gets or sets the name plural. + /// + [DataMember] + public string NamePlural { get; set; } + /// /// Gets or sets the description /// @@ -38,7 +43,6 @@ public partial class QuantityUnit : BaseEntity, ILocalizedEntity /// 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/Seo/SeoSettings.cs b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs index 7160bcf30c..9d1d5f5bce 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs @@ -81,7 +81,7 @@ public SeoSettings() public string SeoNameCharConversion { get; set; } public bool CanonicalUrlsEnabled { get; set; } - public CanonicalHostNameRule CanonicalHostNameRule { get; set; } + public CanonicalHostNameRule CanonicalHostNameRule { get; set; } /// /// Slugs (sename) reserved for some other needs diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs index 0fb734efbd..d9325aa60a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs @@ -97,7 +97,7 @@ public partial class Store : BaseEntity /// /// Gets the security mode for the store /// - public HttpSecurityMode GetSecurityMode(bool? useSsl = null) + public virtual HttpSecurityMode GetSecurityMode(bool? useSsl = null) { if (useSsl ?? SslEnabled) { diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs b/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs index 3273ab21da..1185f4b399 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs @@ -7,42 +7,44 @@ namespace SmartStore.Core.Domain.Stores public static class StoreExtensions { /// - /// Parse comma-separated Hosts + /// Indicates whether a store contains a specified host /// /// Store - /// Comma-separated hosts - public static string[] ParseHostValues(this Store store) + /// Host + /// true - contains, false - no + public static bool ContainsHostValue(this Store store, string host) { - if (store == null) - throw new ArgumentNullException("store"); + Guard.NotNull(store, nameof(store)); - var parsedValues = new List(); - if (!string.IsNullOrEmpty(store.Hosts)) + if (string.IsNullOrEmpty(host)) { - var hosts = store.Hosts.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - parsedValues.AddRange(hosts.Select(host => host.Trim()).Where(tmp => !string.IsNullOrEmpty(tmp))); - } + return false; + } - return parsedValues.ToArray(); + var contains = store.ParseHostValues() + .FirstOrDefault(x => x.Equals(host, StringComparison.InvariantCultureIgnoreCase)) != null; + return contains; } /// - /// Indicates whether a store contains a specified host + /// Parse comma-separated hosts /// /// Store - /// Host - /// true - contains, false - no - public static bool ContainsHostValue(this Store store, string host) + /// Comma-separated hosts + public static string[] ParseHostValues(this Store store) { - if (store == null) - throw new ArgumentNullException("store"); + Guard.NotNull(store, nameof(store)); - if (String.IsNullOrEmpty(host)) - return false; + if (string.IsNullOrWhiteSpace(store.Hosts)) + { + return Array.Empty(); + } - var contains = store.ParseHostValues() - .FirstOrDefault(x => x.Equals(host, StringComparison.InvariantCultureIgnoreCase)) != null; - return contains; + 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/Tasks/ScheduleTask.cs b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs index c6d22dd2e0..f516fc53da 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs @@ -88,12 +88,25 @@ protected set public ScheduleTask Clone() { - return (ScheduleTask)this.MemberwiseClone(); + var task = new ScheduleTask + { + Name = Name, + Alias = Alias, + CronExpression = CronExpression, + Type = Type, + Enabled = Enabled, + StopOnError = StopOnError, + NextRunUtc = NextRunUtc, + IsHidden = IsHidden, + RunPerMachine = RunPerMachine + }; + + return task; } object ICloneable.Clone() { - return this.MemberwiseClone(); + return this.Clone(); } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs index 1d79ba018b..28fd36b269 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs @@ -12,7 +12,8 @@ public TaxSettings() DisplayTaxSuffix = true; PricesIncludeTax = true; EuVatAllowVatExemption = true; - ShowLegalHintsInFooter = true; + ShippingPriceIncludesTax = true; + ShowLegalHintsInFooter = true; ShowLegalHintsInProductDetails = true; ShowLegalHintsInProductList = true; AuxiliaryServicesTaxingType = AuxiliaryServicesTaxType.SpecifiedTaxCategory; diff --git a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs index 15fc63095f..5492f4631f 100644 --- a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs +++ b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs @@ -83,20 +83,19 @@ public void SendEmail(SmtpContext context, EmailMessage message) } } - public Task SendEmailAsync(SmtpContext context, EmailMessage message) + public async Task SendEmailAsync(SmtpContext context, EmailMessage message) { Guard.NotNull(context, nameof(context)); Guard.NotNull(message, nameof(message)); - var client = context.ToSmtpClient(); - ApplySettings(client); - var msg = this.BuildMailMessage(message); - - return client.SendMailAsync(msg).ContinueWith(t => + using (var msg = this.BuildMailMessage(message)) { - client.Dispose(); - msg.Dispose(); - }); + using (var client = context.ToSmtpClient()) + { + ApplySettings(client); + await client.SendMailAsync(msg); + } + } } private void ApplySettings(SmtpClient client) diff --git a/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs b/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs new file mode 100644 index 0000000000..18b72d94da --- /dev/null +++ b/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs @@ -0,0 +1,13 @@ +using System; +using System.Reflection; + +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())) + { + } + } +} diff --git a/src/Libraries/SmartStore.Core/Events/AsyncConsumerAttribute.cs b/src/Libraries/SmartStore.Core/Events/AsyncConsumerAttribute.cs deleted file mode 100644 index 4a33b18f31..0000000000 --- a/src/Libraries/SmartStore.Core/Events/AsyncConsumerAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace SmartStore.Core.Events -{ - [AttributeUsage(AttributeTargets.Class, Inherited=true, AllowMultiple=false)] - public class AsyncConsumerAttribute : Attribute - { - } -} diff --git a/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs b/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs new file mode 100644 index 0000000000..d52607845a --- /dev/null +++ b/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs @@ -0,0 +1,23 @@ +using System; + +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; + } + + public TMessage Message + { + get; + private set; + } + } +} diff --git a/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs b/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs new file mode 100644 index 0000000000..f990c1e61c --- /dev/null +++ b/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs @@ -0,0 +1,50 @@ +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() + { + } + + 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 Type MessageType { get; set; } + public Type ContainerType { 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; } + + 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 new file mode 100644 index 0000000000..9eb50ecb00 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Autofac; +using SmartStore.ComponentModel; +using SmartStore.Core.Async; +using SmartStore.Core.Logging; + +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 + } +} diff --git a/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs b/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs index 1ae4f063fb..0c233e9da1 100644 --- a/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs +++ b/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs @@ -5,8 +5,8 @@ namespace SmartStore.Core.Events { public class EventConsumerMetadata { - public bool ExecuteAsync { get; set; } 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 new file mode 100644 index 0000000000..a5076473c9 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Events/FireForgetAttribute.cs @@ -0,0 +1,20 @@ +using System; + +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 + { + } +} diff --git a/src/Libraries/SmartStore.Core/Events/IConsumer.cs b/src/Libraries/SmartStore.Core/Events/IConsumer.cs index 5fee38c56e..da2ff66d89 100644 --- a/src/Libraries/SmartStore.Core/Events/IConsumer.cs +++ b/src/Libraries/SmartStore.Core/Events/IConsumer.cs @@ -1,8 +1,45 @@ - +using System; + namespace SmartStore.Core.Events { - public interface IConsumer - { - void HandleEvent(T message); - } + /// + /// 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); + } } diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerFactory.cs b/src/Libraries/SmartStore.Core/Events/IConsumerFactory.cs deleted file mode 100644 index a55c9fce79..0000000000 --- a/src/Libraries/SmartStore.Core/Events/IConsumerFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace SmartStore.Core.Events -{ - - public interface IConsumerFactory - { - /// - /// Resolves all event consumers of type - /// - /// - /// Specifies whether only async consumers, sync consumers or both should be resolved - /// (null = all, true = only async, false = only sync) - /// - /// The consumer instances - IEnumerable> GetConsumers(bool? resolveAsyncs = null); - - bool HasAsyncConsumer { get; } - } - -} diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs b/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs new file mode 100644 index 0000000000..85036e7528 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs @@ -0,0 +1,12 @@ +using System; + +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; + } +} diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs b/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs new file mode 100644 index 0000000000..4339d6fb4f --- /dev/null +++ b/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace SmartStore.Core.Events +{ + /// + /// 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 new file mode 100644 index 0000000000..7a7be293d7 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Events/IConsumerResolver.cs @@ -0,0 +1,16 @@ +using System; +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); + } +} diff --git a/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs b/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs index 54fb88e357..21f0f6ebe6 100644 --- a/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs +++ b/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs @@ -1,8 +1,9 @@ - +using System; + namespace SmartStore.Core.Events { - public interface IEventPublisher - { - void Publish(T eventMessage); - } + 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 d0bff29ca8..07d7ae6e11 100644 --- a/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs +++ b/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs @@ -10,7 +10,7 @@ public static IEventPublisher Instance get { return s_instance; } } - public void Publish(T eventMessage) + 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 8fec538fbe..5bd474e782 100644 --- a/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web; -using System.Web.Routing; +using SmartStore.Collections; namespace SmartStore { @@ -26,7 +23,22 @@ public static void AddRange(this ICollection initial, IEnumerable other } } - public static bool IsNullOrEmpty(this ICollection source) + 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; + } + + return new SyncedCollection(source, syncRoot); + } + + public static bool IsNullOrEmpty(this ICollection source) { return source == null || source.Count == 0; } diff --git a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs index 35fa7b73e2..58992963be 100644 --- a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs @@ -213,16 +213,21 @@ public static byte[] ToByteArray(this Stream stream) if (stream == null) throw new ArgumentNullException(nameof(stream)); - if (stream is MemoryStream) + if (stream is MemoryStream mem) { - return ((MemoryStream)stream).ToArray(); + if (mem.TryGetBuffer(out var buffer)) + { + return buffer.Array; + } + + return mem.ToArray(); } else { using (var streamReader = new MemoryStream()) { stream.CopyTo(streamReader); - return streamReader.ToArray(); + return streamReader.GetBuffer(); } } } @@ -232,9 +237,14 @@ public static async Task ToByteArrayAsync(this Stream stream) if (stream == null) throw new ArgumentNullException(nameof(stream)); - if (stream is MemoryStream) + if (stream is MemoryStream mem) { - return ((MemoryStream)stream).ToArray(); + if (mem.TryGetBuffer(out var buffer)) + { + return buffer.Array; + } + + return mem.ToArray(); } else { diff --git a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs index a711c68aa7..cb529dcace 100644 --- a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs @@ -423,6 +423,36 @@ public static int Remove(this IList list, Func selector) return count; } - #endregion - } + #endregion + + #region Stack + + public static bool TryPeek(this Stack stack, out T value) + { + value = default(T); + + if (stack.Count > 0) + { + value = stack.Peek(); + return true; + } + + return false; + } + + public static bool TryPop(this Stack stack, out T value) + { + value = default(T); + + if (stack.Count > 0) + { + value = stack.Pop(); + return true; + } + + return false; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs index 1d6bb939dc..59851619db 100644 --- a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs @@ -11,6 +11,7 @@ using SmartStore.Core.Infrastructure; using SmartStore.Core; using System.Web.Mvc; +using SmartStore.Core.Fakes; namespace SmartStore { @@ -28,6 +29,47 @@ public static class HttpExtensions new Tuple("X-Url-Scheme", "https") }; + /// + /// Tries to get the instance without throwing exceptions + /// + /// The instance or null. + 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 /// @@ -242,7 +284,7 @@ where key.StartsWith(pattern, StringComparison.OrdinalIgnoreCase) return keys.ToArray(); } - public static ControllerContext GetMasterControllerContext(this ControllerContext controllerContext) + public static ControllerContext GetRootControllerContext(this ControllerContext controllerContext) { Guard.NotNull(controllerContext, nameof(controllerContext)); @@ -255,6 +297,21 @@ public static ControllerContext GetMasterControllerContext(this ControllerContex return ctx; } - } + public static bool IsBareBonePage(this ControllerContext controllerContext) + { + var ctx = controllerContext.GetRootControllerContext(); + + if (ctx is ViewContext viewContext) + { + // IsPopUp or Framed + if (viewContext.ViewBag.IsPopup == true || viewContext.ViewBag.Framed == true) + { + return true; + } + } + + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs index 5b6c628c10..2104c9c3ee 100644 --- a/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SmartStore { diff --git a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs index e57d2ca275..845d7a9a79 100644 --- a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs @@ -34,10 +34,10 @@ public static string GetAreaName(this RouteBase route) return null; } - /// - /// Generates an identifier for the given route in the form "[{area}.]{controller}.{action}" - /// - public static string GenerateRouteIdentifier(this RouteData routeData) + /// + /// 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"); diff --git a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs index 98f55abf2d..211fb926af 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs @@ -611,30 +611,29 @@ 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. + // Do not use separator.IsEmpty() here because whitespace like " " is a valid separator. // an empty separator "" returns array with value. if (separator == null) { - separator = "|"; - - if (value.IndexOf(separator) < 0) + for (var i = 0; i < value.Length; i++) { - if (value.IndexOf(';') > -1) - { - separator = ";"; - } - else if (value.IndexOf(',') > -1) + var c = value[i]; + if (c == ';' || c == ',' || c == '|') { - separator = ","; + return value.Split(new char[] { c }, StringSplitOptions.RemoveEmptyEntries); } - else if (value.IndexOf(Environment.NewLine) > -1) + if (c == '\r' && (i + 1) < value.Length & value[i + 1] == '\n') { - separator = Environment.NewLine; + return value.GetLines(false, true).ToArray(); } } - } - return value.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries); + return new string[] { value }; + } + else + { + return value.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries); + } } /// Splits a string into two strings @@ -1087,11 +1086,8 @@ public static string ReplaceCsvChars(this string s) [DebuggerStepThrough] public static string HighlightKeywords(this string input, string keywords, string preMatch = "", string postMatch = "") { - if (preMatch == null) - throw new ArgumentNullException(nameof(preMatch)); - - if (postMatch == null) - throw new ArgumentNullException(nameof(postMatch)); + Guard.NotNull(preMatch, nameof(preMatch)); + Guard.NotNull(postMatch, nameof(postMatch)); if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(keywords)) { @@ -1107,7 +1103,7 @@ public static string HighlightKeywords(this string input, string keywords, strin if (!string.IsNullOrWhiteSpace(pattern)) { var rg = new Regex(pattern, RegexOptions.IgnoreCase); - input = rg.Replace(input, m => preMatch + m.Value + postMatch); + input = rg.Replace(input, m => preMatch + m.Value.EmptyNull().HtmlEncode() + postMatch); } return input; diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs index 4a024630cc..68302e9a61 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs @@ -66,6 +66,8 @@ public FakeHttpContext(string relativeUrl, _serverVariables = serverVariables; _items = new Dictionary(); + + Handler = new FakeHttpHandler(); } public override IHttpHandler Handler { get; set; } diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpHandler.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpHandler.cs new file mode 100644 index 0000000000..ea0839801a --- /dev/null +++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpHandler.cs @@ -0,0 +1,15 @@ +using System; +using System.Web; + +namespace SmartStore.Core.Fakes +{ + public class FakeHttpHandler : IHttpHandler + { + public bool IsReusable => true; + + public void ProcessRequest(HttpContext context) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs index 5238183265..ee5ecc7928 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs @@ -115,8 +115,8 @@ public override string ApplicationPath { get { - //we know that relative paths always start with ~/ - //ApplicationPath should start with / + // We know that relative paths always start with ~/ + // ApplicationPath should start with / if (_relativeUrl != null && _relativeUrl.StartsWith("~/")) return _relativeUrl.Remove(0, 1); return null; @@ -137,10 +137,11 @@ public override string UserHostAddress } public override string RawUrl => this.ApplicationPath; - public override bool IsSecureConnection => false; + 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 { diff --git a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs index 617aa3f5c3..9591f17d81 100644 --- a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs +++ b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs @@ -517,7 +517,7 @@ public long Size public DateTime LastUpdated { - get { return _fileInfo.LastWriteTime; } + get { return _fileInfo.LastWriteTimeUtc; } } public string Extension @@ -595,7 +595,7 @@ public string Name public DateTime LastUpdated { - get { return _directoryInfo.LastWriteTime; } + get { return _directoryInfo.LastWriteTimeUtc; } } public long Size diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs index 6f1bb5f3a2..e4177560ad 100644 --- a/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs +++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs @@ -33,6 +33,7 @@ public interface IVirtualFolder Stream OpenFile(string relativePath); void CreateTextFile(string relativePath, string content); + Task CreateTextFileAsync(string relativePath, string content); Stream CreateFile(string relativePath); void CreateDirectory(string relativePath); @@ -40,6 +41,7 @@ public interface IVirtualFolder void DeleteDirectory(string relativePath); string ReadFile(string relativePath); + Task ReadFileAsync(string relativePath); void CopyFile(string relativePath, Stream destination); DateTime GetFileLastWriteTimeUtc(string relativePath); diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs index 8b892dedea..6355143bc6 100644 --- a/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs +++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using SmartStore.Core.Logging; namespace SmartStore.Core.IO @@ -117,6 +118,19 @@ public void CreateTextFile(string relativePath, string 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); @@ -170,6 +184,26 @@ public virtual string ReadFile(string relativePath) } } + 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)); diff --git a/src/Libraries/SmartStore.Core/IPageable.cs b/src/Libraries/SmartStore.Core/IPageable.cs index 6689becf09..ce270c8e4e 100644 --- a/src/Libraries/SmartStore.Core/IPageable.cs +++ b/src/Libraries/SmartStore.Core/IPageable.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace SmartStore.Core { @@ -100,7 +101,19 @@ public interface IPagedList : IPageable, IList /// 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); } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs b/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs index b7ffec8f69..4ed72318a1 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs @@ -71,7 +71,7 @@ public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable(object key = null, ILifetimeScope scope = null) where T : class + [DebuggerStepThrough] + public T Resolve(object key = null, ILifetimeScope scope = null) where T : class { if (key == null) { @@ -36,22 +38,26 @@ public T Resolve(object key = null, ILifetimeScope scope = null) where T : cl return (scope ?? Scope()).ResolveKeyed(key); } - public T ResolveNamed(string name, ILifetimeScope scope = null) where T : class + [DebuggerStepThrough] + public T ResolveNamed(string name, ILifetimeScope scope = null) where T : class { return (scope ?? Scope()).ResolveNamed(name); } - public object Resolve(Type type, ILifetimeScope scope = null) + [DebuggerStepThrough] + public object Resolve(Type type, ILifetimeScope scope = null) { return (scope ?? Scope()).Resolve(type); } - public object ResolveNamed(string name, Type type, ILifetimeScope scope = null) + [DebuggerStepThrough] + public object ResolveNamed(string name, Type type, ILifetimeScope scope = null) { return (scope ?? Scope()).ResolveNamed(name, type); } - public T[] ResolveAll(object key = null, ILifetimeScope scope = null) + [DebuggerStepThrough] + public T[] ResolveAll(object key = null, ILifetimeScope scope = null) { if (key == null) { @@ -130,7 +136,8 @@ private bool TryResolveAll(Type[] types, out object[] instances, ILifetimeScope } } - public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance) + [DebuggerStepThrough] + public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance) { instance = null; @@ -144,7 +151,8 @@ public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instan } } - public bool TryResolve(ILifetimeScope scope, out T instance) + [DebuggerStepThrough] + public bool TryResolve(ILifetimeScope scope, out T instance) { instance = default(T); @@ -178,6 +186,7 @@ public T InjectUnsetProperties(T instance, ILifetimeScope scope = null) return (scope ?? Scope()).InjectUnsetProperties(instance); } + [DebuggerStepThrough] public ILifetimeScope Scope() { var scope = _container.Resolve().GetLifetimeScope(null); diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs index bf84c53263..1c7fed864c 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs @@ -61,6 +61,7 @@ public ILifetimeScope GetLifetimeScope(Action configurationAct _state.SetState((scope = BeginLifetimeScope(configurationAction))); //scope.CurrentScopeEnding += OnScopeEnding; } + return scope; } @@ -76,7 +77,7 @@ public ILifetimeScope GetLifetimeScope(Action configurationAct // Debug.WriteLine("Scope END, Web: " + isWeb); //} - private ILifetimeScope BeginLifetimeScope(Action configurationAction) + public ILifetimeScope BeginLifetimeScope(Action configurationAction) { return (configurationAction == null) ? _rootContainer.BeginLifetimeScope(ScopeTag) diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs index 03beda80b8..ee09073226 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs @@ -39,5 +39,14 @@ public interface ILifetimeScopeAccessor /// 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); } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs index 18dcfc4222..0029454b28 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using System.Web.Mvc; @@ -148,6 +149,7 @@ public bool IsFullyInitialized set; } + [DebuggerStepThrough] public T Resolve(string name = null) where T : class { if (name.HasValue()) @@ -157,6 +159,7 @@ public T Resolve(string name = null) where T : class return ContainerManager.Resolve(); } + [DebuggerStepThrough] public object Resolve(Type type, string name = null) { if (name.HasValue()) @@ -166,7 +169,7 @@ public object Resolve(Type type, string name = null) return ContainerManager.Resolve(type); } - + [DebuggerStepThrough] public T[] ResolveAll() { return ContainerManager.ResolveAll(); diff --git a/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs b/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs index f06b87c086..a437eebd7d 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs @@ -95,7 +95,7 @@ protected virtual void LoadPluginAssemblies() AppDomain.CurrentDomain.GetAssemblies().Select(x => x.FullName), StringComparer.OrdinalIgnoreCase); - var pluginAssemblies = new HashSet(PluginManager.ReferencedPlugins.Select(x => x.ReferencedAssembly).Where(x => x != null)); + var pluginAssemblies = new HashSet(PluginManager.ReferencedPlugins.Select(x => x.Assembly.Assembly)); foreach (var assembly in pluginAssemblies) { diff --git a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs index 1b043a2108..6bd1f49e02 100644 --- a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs @@ -74,13 +74,12 @@ public void Log(LogLevel level, Exception exception, string message, object[] ar protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent) { - HttpRequest httpRequest = null; + var isAppInitialized = EngineContext.Current.IsFullyInitialized; - try - { - httpRequest = HttpContext.Current.Request; - } - catch + // 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; @@ -100,14 +99,12 @@ protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent) { using (new ActionDisposable(() => props["sm:ThreadInfoAdded"] = true)) { - if (DataSettings.DatabaseIsInstalled() && EngineContext.Current.IsFullyInitialized) + if (DataSettings.DatabaseIsInstalled() && isAppInitialized) { var container = EngineContext.Current.ContainerManager; - IWorkContext workContext; - // CustomerId - if (container.TryResolve(null, out workContext)) + if (container.TryResolve(null, out IWorkContext workContext)) { try { @@ -119,10 +116,9 @@ protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent) } } - IWebHelper webHelper; // Url & stuff - if (container.TryResolve(null, out webHelper)) + if (container.TryResolve(null, out IWebHelper webHelper)) { try { diff --git a/src/Libraries/SmartStore.Core/Money.cs b/src/Libraries/SmartStore.Core/Money.cs index e4bdf4a24f..33244b92ef 100644 --- a/src/Libraries/SmartStore.Core/Money.cs +++ b/src/Libraries/SmartStore.Core/Money.cs @@ -135,8 +135,14 @@ int IComparable.CompareTo(object obj) public override bool Equals(object obj) { - return Equals(obj as Money); - } + // Prevent stack overflow. + if (obj != null) + { + return Equals(obj as Money); + } + + return false; + } bool IEquatable.Equals(Money other) { diff --git a/src/Libraries/SmartStore.Core/PagedList`T.cs b/src/Libraries/SmartStore.Core/PagedList`T.cs index f7e951d047..0fa22d90e1 100644 --- a/src/Libraries/SmartStore.Core/PagedList`T.cs +++ b/src/Libraries/SmartStore.Core/PagedList`T.cs @@ -5,6 +5,7 @@ using System.Data.Entity.Infrastructure; using System.Collections; using System.Collections.ObjectModel; +using System.Threading.Tasks; namespace SmartStore.Core { @@ -66,6 +67,26 @@ private void EnsureIsLoaded() } } + private async Task EnsureIsLoadedAsync() + { + if (_list == null) + { + if (_totalCount == null) + { + _totalCount = SourceQuery.Count(); + } + + if (_queryIsPagedAlready) + { + _list = await SourceQuery.ToListAsync(); + } + else + { + _list = await ApplyPaging(SourceQuery).ToListAsync(); + } + } + } + #region IPageable Members public IQueryable SourceQuery { get; private set; } @@ -101,6 +122,7 @@ public IQueryable ApplyPaging(IQueryable query) public IPagedList Load(bool force = false) { + // Returns instance for chaining. if (force && _list != null) { _list.Clear(); @@ -112,6 +134,20 @@ public IPagedList Load(bool force = false) 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; } diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs index 35b2b3e8c9..ab6a32754e 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs @@ -11,6 +11,30 @@ 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] public class PluginDescriptor : IComparable { @@ -19,17 +43,16 @@ public class PluginDescriptor : IComparable public PluginDescriptor() { - this.Version = new Version("1.0"); - this.MinAppVersion = SmartStoreVersion.Version; + Version = new Version("1.0"); + MinAppVersion = SmartStoreVersion.Version; } // Unit tests - public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFile, Type pluginType) + public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFile, Type pluginClrType) : this() { - this.ReferencedAssembly = referencedAssembly; - this.OriginalAssemblyFile = originalAssemblyFile; - this.PluginType = pluginType; + Assembly = new AssemblyReference { Assembly = referencedAssembly, OriginalFile = originalAssemblyFile, File = originalAssemblyFile }; + PluginClrType = pluginClrType; } /// @@ -42,6 +65,12 @@ 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 /// @@ -79,33 +108,23 @@ public string BrandImageFileName } /// - /// Plugin type + /// Plugin runtime type /// - public Type PluginType { get; set; } - - /// - /// The assembly that has been shadow copied and was loaded into the AppDomain - /// - public Assembly ReferencedAssembly { get; internal set; } - - /// - /// The original assembly file that a shadow copy was made from it - /// - public FileInfo OriginalAssemblyFile { get; internal set; } + public Type PluginClrType { get; set; } /// - /// The list of assembly files found in the plugin folder, except the main plugin assembly. + /// The plugin assembly. /// - public IEnumerable ReferencedLocalAssemblyFiles { get; internal set; } + public AssemblyReference Assembly { get; internal set; } = new AssemblyReference(); /// - /// Gets any exception thrown during plugin activation & initializing + /// List of assemblies files found in the plugin folder, except the main plugin assembly. /// - public Exception ActivationException { get; internal set; } + internal AssemblyReference[] ReferencedLocalAssemblies { get; set; } = new AssemblyReference[0]; - /// - /// Gets or sets the plugin group - /// + /// + /// Gets or sets the plugin group + /// [DataMember] public string Group { get; internal set; } @@ -123,12 +142,6 @@ public bool IsInKnownGroup [DataMember] public string FriendlyName { get; set; } - /// - /// Gets the folder name - /// - [DataMember] - public string FolderName { get; internal set; } - /// /// Gets or sets the system name /// @@ -243,10 +256,10 @@ public string ResourceRootKey public T Instance() where T : class, IPlugin { object instance; - if (!EngineContext.Current.ContainerManager.TryResolve(PluginType, null, out instance)) + if (!EngineContext.Current.ContainerManager.TryResolve(PluginClrType, null, out instance)) { // Not registered - instance = EngineContext.Current.ContainerManager.ResolveUnregistered(PluginType); + instance = EngineContext.Current.ContainerManager.ResolveUnregistered(PluginClrType); } var typedInstance = instance as T; diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs b/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs index 07fcd1de9e..10e0ce284f 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs @@ -42,7 +42,9 @@ public override int Compare(string x, string y) "Sales", "Design", "Performance", - "Misc" + "B2B", + "Storefront", + "Law" }; public readonly static IComparer KnownGroupComparer = new GroupComparer(); diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs b/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs index 03c6ee288a..4eddd033dd 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs @@ -1,32 +1,33 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace SmartStore.Core.Plugins { - /// - /// Plugin finder - /// 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 s_lock = new object(); + private static readonly object _lock = new object(); - public PluginFinder() + private PluginFinder() { - lock (s_lock) + 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; @@ -34,7 +35,10 @@ private void LoadPlugins() foreach (var plugin in plugins) { _nameMap[plugin.SystemName] = plugin; - _assemblyMap[plugin.ReferencedAssembly] = plugin; + if (plugin.Assembly.Assembly != null) + { + _assemblyMap[plugin.Assembly.Assembly] = plugin; + } } } @@ -47,7 +51,7 @@ private void LoadPlugins() public virtual IEnumerable GetPlugins(bool installedOnly = true) where T : class, IPlugin { foreach (var plugin in _plugins) - if (typeof(T).IsAssignableFrom(plugin.PluginType)) + if (typeof(T).IsAssignableFrom(plugin.PluginClrType)) if (!installedOnly || plugin.Installed) yield return plugin.Instance(); } @@ -74,15 +78,14 @@ public virtual IEnumerable GetPluginDescriptors(bool instal where T : class, IPlugin { foreach (var plugin in _plugins) - if (typeof(T).IsAssignableFrom(plugin.PluginType)) + if (typeof(T).IsAssignableFrom(plugin.PluginClrType)) if (!installedOnly || plugin.Installed) yield return plugin; } public virtual PluginDescriptor GetPluginDescriptorByAssembly(Assembly assembly, bool installedOnly = true) { - PluginDescriptor descriptor; - if (assembly != null && _assemblyMap.TryGetValue(assembly, out descriptor)) + if (assembly != null && _assemblyMap.TryGetValue(assembly, out var descriptor)) { if (!installedOnly || descriptor.Installed) return descriptor; @@ -117,12 +120,11 @@ public virtual PluginDescriptor GetPluginDescriptorBySystemName(string systemNam /// >Plugin descriptor public virtual PluginDescriptor GetPluginDescriptorBySystemName(string systemName, bool installedOnly = true) where T : class, IPlugin { - PluginDescriptor descriptor; - if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out descriptor)) + if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out var descriptor)) { if (!installedOnly || descriptor.Installed) { - if (typeof(T).IsAssignableFrom(descriptor.PluginType)) + if (typeof(T).IsAssignableFrom(descriptor.PluginClrType)) return descriptor; } } diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs b/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs index 2d4e6ae541..dff9c225b5 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs @@ -38,10 +38,14 @@ private static bool DetectAndCleanStalePlugins(IEnumerable plu .Select(x => new FileInfo(Path.Combine(_shadowCopyDir.FullName, x))) .ToArray(); - foreach (var f in staleFiles) + // 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) { - // If that fails, then we will try to remove it the hard way by renaming it to .delete if it doesn't delete nicely - TryDeleteStaleFile(f); + staleFiles.AsParallel().ForAll(f => TryDeleteStaleFile(f)); + } + else + { + staleFiles.Each(f => TryDeleteStaleFile(f)); } return true; @@ -132,14 +136,11 @@ private static void SavePluginsAssemblies(IEnumerable plugins) foreach (var p in plugins) { - if (p.ReferencedLocalAssemblyFiles != null) - { - p.ReferencedLocalAssemblyFiles.Each(x => list.Add(x.Name)); - } + p.ReferencedLocalAssemblies.Each(x => list.Add(x.OriginalFile.Name)); - if (p.OriginalAssemblyFile != null) + if (p.Assembly.OriginalFile != null) { - list.Add(p.OriginalAssemblyFile.Name); + list.Add(p.Assembly.OriginalFile.Name); } } @@ -166,15 +167,12 @@ private static int ComputePluginsHash(IEnumerable plugins) // Add each *.dll, Description.txt, web.config to the hash foreach (var p in plugins) { - if (p.OriginalAssemblyFile != null) + if (p.Assembly.OriginalFile != null) { - hashCombiner.Add(p.OriginalAssemblyFile); + hashCombiner.Add(p.Assembly.OriginalFile); } - if (p.ReferencedLocalAssemblyFiles != null) - { - p.ReferencedLocalAssemblyFiles.Each(x => hashCombiner.Add(x)); - } + p.ReferencedLocalAssemblies.Each(x => hashCombiner.Add(x.OriginalFile)); hashCombiner.Add(Path.Combine(p.PhysicalPath, "Description.txt")); hashCombiner.Add(Path.Combine(p.PhysicalPath, "web.config")); diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs index 6a95a30aae..c7c4d6fe4e 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs @@ -16,6 +16,7 @@ using SmartStore.Utilities; using SmartStore.Utilities.Threading; using SmartStore.Core.Data; +using System.Threading.Tasks; // Contributor: Umbraco (http://www.umbraco.com). Thanks a lot! // SEE THIS POST for full details of what this does @@ -33,7 +34,7 @@ public partial class PluginManager [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetDllDirectory(string lpPathName); - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); + private static readonly object _lock = new object(); private static readonly string _pluginsPath = "~/Plugins"; private static DirectoryInfo _shadowCopyDir; private static readonly ConcurrentDictionary _referencedPlugins = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); @@ -105,101 +106,122 @@ public static void Initialize() // adding a process-specific environment path (either bin/x86 or bin/x64) // ensures that unmanaged native dependencies can be resolved successfully. SetNativeDllPath(); - //SetPrivateEnvPath(); DynamicModuleUtility.RegisterModule(typeof(AutofacRequestLifetimeHttpModule)); - #region Plugins + var incompatiblePlugins = (new HashSet(StringComparer.OrdinalIgnoreCase)).AsSynchronized(); + var inactiveAssemblies = _inactiveAssemblies.AsSynchronized(); + var dirty = false; - var incompatiblePlugins = new HashSet(StringComparer.OrdinalIgnoreCase); + var watch = Stopwatch.StartNew(); - using (Locker.GetWriteLock()) - { - _shadowCopyDir = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory); + _shadowCopyDir = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory); + + var plugins = LoadPluginDescriptors().ToArray(); + var compatiblePlugins = plugins.Where(x => !x.Incompatible).ToArray(); + + var ms = watch.ElapsedMilliseconds; + Debug.WriteLine("INIT PLUGINS (LoadPluginDescriptors). Time elapsed: {0} ms.".FormatCurrentUI(ms)); + + // If plugins state is dirty, we copy files over to the dynamic folder, + // otherwise we just reference the previously copied file. + dirty = DetectAndCleanStalePlugins(compatiblePlugins); + + // Perf: Initialize/probe all plugins in parallel + plugins.AsParallel().ForAll(x => + { + // Deploy to ASP.NET dynamic folder. + DeployPlugin(x, dirty); + + // Finalize + FinalizePlugin(x); + }); + + //// Retry when failed, because during parallel execution assembly loading MAY fail. + //// Therefore we retry initialization for failed plugins, but sequentially this time. + //foreach (var p in plugins) + //{ + // // INFO: this seems redundant, but it's ok: + // // DeployPlugin() only probes assemblies that are not loaded yet. + // DeployPlugin(p, dirty); + + // // Finalize + // FinalizePlugin(p); + //} + + 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); - var plugins = LoadPluginDescriptors().ToArray(); - var compatiblePlugins = plugins.Where(x => !x.Incompatible).ToArray(); + // Save names of all deployed assemblies to disk (so we can nuke them later) + SavePluginsAssemblies(_referencedPlugins.Values); + } - // If plugins state is dirty, we copy files over to the dynamic folder, - // otherwise we just reference the previously copied file. - var dirty = DetectAndCleanStalePlugins(compatiblePlugins); + IncompatiblePlugins = incompatiblePlugins.AsReadOnly(); - foreach (var plugin in plugins) + ms = watch.ElapsedMilliseconds; + Debug.WriteLine("INIT PLUGINS (Deployment complete). Time elapsed: {0} ms.".FormatCurrentUI(ms)); + + void DeployPlugin(PluginDescriptor p, bool shadowCopy) + { + if (p.Incompatible) { - if (plugin.Incompatible) - { - incompatiblePlugins.Add(plugin.SystemName); - continue; - } - else + // Do nothing if plugin is incompatible + return; + } + + // First copy referenced local assemblies (if any) + for (int i = 0; i < p.ReferencedLocalAssemblies.Length; ++i) + { + var refAr = p.ReferencedLocalAssemblies[i]; + if (refAr.Assembly == null) { - _referencedPlugins[plugin.SystemName] = plugin; + Probe(refAr, p, shadowCopy); } + } - try - { - // Shadow copy main plugin assembly - plugin.ReferencedAssembly = Probe(plugin.OriginalAssemblyFile, dirty); - - // Shadow copy other referenced plugin local assemblies - if (plugin.ReferencedLocalAssemblyFiles != null) - { - foreach (var assemblyFile in plugin.ReferencedLocalAssemblyFiles) - { - Probe(assemblyFile, dirty); - } - } - - if (!plugin.Installed) - { - _inactiveAssemblies.Add(plugin.ReferencedAssembly); - } - - // Initialize: Find IPlugin, IPreApplicationStart, IConfigurable etc. - ActivatePlugin(plugin); - } - catch (UnauthorizedAccessException) + // Then copy main plugin assembly + var ar = p.Assembly; + if (ar.Assembly == null) + { + Probe(ar, p, shadowCopy); + if (ar.Assembly != null) { - // Throw the exception if its UnauthorizedAccessException as this will - // be because we most likely cannot copy to the dynamic folder. - throw; + // Activate (even if uninstalled): Find IPlugin, IPreApplicationStart, IConfigurable etc. + ActivatePlugin(p); } - catch (ReflectionTypeLoadException ex) - { - var msg = string.Empty; - foreach (var e in ex.LoaderExceptions) - { - msg += e.Message + Environment.NewLine; - } + } + } - HandlePluginActivationException(ex, plugin, msg, incompatiblePlugins); - } - catch (Exception ex) - { - var msg = string.Empty; - for (var e = ex; e != null; e = e.InnerException) - { - msg += e.Message + Environment.NewLine; - } + void FinalizePlugin(PluginDescriptor p) + { + _referencedPlugins[p.SystemName] = p; - HandlePluginActivationException(ex, plugin, msg, incompatiblePlugins); - } + if (p.Incompatible) + { + incompatiblePlugins.Add(p.SystemName); + return; } - if (dirty && DataSettings.DatabaseIsInstalled()) + var firstFailedAssembly = p.ReferencedLocalAssemblies.FirstOrDefault(x => x.ActivationException != null); + if (firstFailedAssembly == null && p.Assembly.ActivationException != null) { - // Save current hash of all deployed plugins to disk - var hash = ComputePluginsHash(_referencedPlugins.Values.OrderBy(x => x.FolderName).ToArray()); - SavePluginsHash(hash); - - // Save names of all deployed assemblies to disk (so we can nuke them later) - SavePluginsAssemblies(_referencedPlugins.Values); + firstFailedAssembly = p.Assembly; + } + if (firstFailedAssembly != null) + { + Debug.WriteLine(firstFailedAssembly.ActivationException.Message); + p.Incompatible = true; + incompatiblePlugins.Add(p.SystemName); } - IncompatiblePlugins = incompatiblePlugins.AsReadOnly(); + if ((!p.Installed || firstFailedAssembly != null) && p.Assembly.Assembly != null) + { + inactiveAssemblies.Add(p.Assembly.Assembly); + } } - - #endregion } /// @@ -216,7 +238,7 @@ private static IEnumerable LoadPluginDescriptors() if (!pluginsDir.Exists) { pluginsDir.Create(); - yield break; + return Enumerable.Empty(); } // Determine all plugin folders: ~/Plugins/{SystemName} @@ -225,19 +247,56 @@ private static IEnumerable LoadPluginDescriptors() .OrderBy(x => x.Name) .ToArray(); - var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(); + var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile().AsSynchronized(); // Load/activate all plugins - foreach (var d in allPluginDirs) - { - var descriptor = LoadPluginDescriptor(d, installedPluginSystemNames); - if (descriptor != null) - { - yield return descriptor; - } - } + 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")); @@ -282,9 +341,9 @@ private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollectio .ToDictionarySafe(x => x.Name, StringComparer.OrdinalIgnoreCase); // Set 'OriginalAssemblyFile' property - descriptor.OriginalAssemblyFile = pluginBinaries.Get(descriptor.PluginFileName); + descriptor.Assembly.OriginalFile = pluginBinaries.Get(descriptor.PluginFileName); - if (descriptor.OriginalAssemblyFile == null) + if (descriptor.Assembly.OriginalFile == null) { throw new SmartException("The main assembly '{0}' for plugin '{1}' could not be found.".FormatInvariant(descriptor.PluginFileName, descriptor.SystemName)); } @@ -294,7 +353,7 @@ private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollectio .Where(x => !x.Key.IsCaseInsensitiveEqual(descriptor.PluginFileName)) .Select(x => x.Value); - descriptor.ReferencedLocalAssemblyFiles = otherAssemblyFiles.ToArray(); + descriptor.ReferencedLocalAssemblies = otherAssemblyFiles.Select(x => new AssemblyReference { OriginalFile = x }).ToArray(); return descriptor; } @@ -302,15 +361,15 @@ private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollectio private static void ActivatePlugin(PluginDescriptor plugin) { // Init plugin type (only one plugin per assembly is allowed) - var exportedTypes = plugin.ReferencedAssembly.ExportedTypes; 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.PluginType = t; + plugin.PluginClrType = t; plugin.IsConfigurable = typeof(IConfigurable).IsAssignableFrom(t); pluginFound = true; } @@ -332,22 +391,6 @@ private static void ActivatePlugin(PluginDescriptor plugin) } } - private static void HandlePluginActivationException(Exception ex, PluginDescriptor plugin, string msg, ICollection incompatiblePlugins) - { - msg = "Error loading plugin '{0}'".FormatInvariant(plugin.SystemName) + Environment.NewLine + msg; - - var fail = new SmartException(msg, ex); - Debug.WriteLine(fail.Message); - - if (plugin.ReferencedAssembly != null) - { - _inactiveAssemblies.Add(plugin.ReferencedAssembly); - } - - plugin.ActivationException = fail; - incompatiblePlugins.Add(plugin.SystemName); - } - /// /// Mark plugin as installed /// @@ -527,51 +570,101 @@ private static void SetNativeDllPath() /// /// Perform file deploy /// - /// Plugin main dll file info + /// Assembly reference to probe /// Reference to the shadow copied Assembly - private static Assembly Probe(FileInfo plugin, bool performShadowCopy) + private static Assembly Probe(AssemblyReference ar, PluginDescriptor d, bool shadowCopy) { - if (plugin.Directory == null || plugin.Directory.Parent == null) + var file = ar.OriginalFile; + + try { - throw new InvalidOperationException("The plugin directory for the " + plugin.Name + - " file exists in a folder outside of the allowed SmartStore folder hierarchy"); + 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) + { + // 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; + } - var probedPlugin = InitializeFullTrust(plugin, performShadowCopy); + 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; + } - // We can now register the plugin definition - var probedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(probedPlugin.FullName)); + ar.ActivationException = CreateException(msg, ex); + } - // Add the reference to the build manager - BuildManager.AddReferencedAssembly(probedAssembly); + return ar.Assembly; - return probedAssembly; - } + 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 /// - /// Plugin main dll file + /// Plugin dll file /// Shadow copied file - private static FileInfo InitializeFullTrust(FileInfo plugin, bool performShadowCopy) + private static FileInfo InitializeFullTrust(FileInfo dll, bool shadowCopy) { - var probedPlugin = new FileInfo(Path.Combine(_shadowCopyDir.FullName, plugin.Name)); + 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 (!performShadowCopy && probedPlugin.Exists) - return probedPlugin; + if (!shadowCopy && probedDll.Exists) + return probedDll; try { - File.Copy(plugin.FullName, probedPlugin.FullName, true); + 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.", probedPlugin.Directory.FullName)); + 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) { - Debug.WriteLine(probedPlugin.FullName + " is locked, attempting to rename"); + Debug.WriteLine(probedDll.FullName + " is locked, attempting to rename"); // This occurs when the files are locked, // For some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them @@ -579,23 +672,23 @@ private static FileInfo InitializeFullTrust(FileInfo plugin, bool performShadowC try { // If all else fails during the cleanup and we cannot copy over so we need to rename with a GUID - var deleteName = GetNewDeleteName(probedPlugin); - File.Move(probedPlugin.FullName, deleteName); + 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.", probedPlugin.Directory.FullName)); + 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) { - throw new IOException(probedPlugin.FullName + " rename failed, cannot initialize plugin", exc); + throw new IOException(probedDll.FullName + " rename failed, cannot initialize plugin", exc); } // OK, we've made it this so far, now retry the shadow copy - File.Copy(plugin.FullName, probedPlugin.FullName, true); + File.Copy(dll.FullName, probedDll.FullName, true); } - return probedPlugin; + return probedDll; } /// diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs index d07a628170..8b806c43fc 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs @@ -1,13 +1,41 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using SmartStore.Core.Domain.DataExchange; +using SmartStore.Utilities; namespace SmartStore.Core.Plugins { - public class ProviderMetadata + 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 @@ -112,7 +140,7 @@ public override bool Equals(object obj) public override int GetHashCode() { - return SystemName.GetHashCode(); + return HashCodeCombiner.Start().Add(typeof(ProviderMetadata)).Add(SystemName).CombinedHash; } } } diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index 4aa8caff8a..e89e76c15d 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -1,5 +1,6 @@  - + + Debug AnyCPU @@ -154,10 +155,12 @@ Properties\AssemblyVersionInfo.cs - + + + @@ -173,6 +176,7 @@ + @@ -210,6 +214,9 @@ + + + @@ -245,7 +252,17 @@ + + + + + + + + + + @@ -254,6 +271,7 @@ + @@ -276,7 +294,7 @@ - + @@ -331,11 +349,7 @@ - - - - @@ -387,7 +401,6 @@ - @@ -688,6 +701,7 @@ + diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs index 75f292f08f..e5bc5e5e48 100644 --- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs +++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs @@ -43,8 +43,11 @@ public static class SmartStoreVersion new Version("2.5"), new Version("3.0"), new Version("3.1"), - new Version("3.1.5") - }; + new Version("3.1.5"), + new Version("3.2"), + new Version("3.2.1"), + new Version("3.2.2") + }; private const string HELP_BASEURL = "https://docs.smartstore.com/display/"; @@ -113,8 +116,8 @@ public static string GenerateHelpUrl(string languageCode, string path) public static string GetUserGuideSpaceKey(string languageCode) { return languageCode.IsCaseInsensitiveEqual("de") - ? "SDDE31" - : "SMNET31"; + ? "SDDE32" + : "SMNET32"; } /// diff --git a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs index 5a4a2253b2..0c90853524 100644 --- a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs +++ b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs @@ -8,6 +8,7 @@ using System.Threading; using SmartStore.Collections; using SmartStore.Core.Events; +using SmartStore.Core.Infrastructure.DependencyManagement; using SmartStore.Core.Logging; using SmartStore.Utilities; @@ -29,10 +30,10 @@ public partial class DefaultThemeRegistry : DisposableObject, IThemeRegistry public DefaultThemeRegistry(IEventPublisher eventPublisher, IApplicationEnvironment env, bool? enableMonitoring, string themesBasePath, bool autoLoadThemes) { - this._enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("sm:MonitorThemesFolder", true); - this._eventPublisher = eventPublisher; - this._env = env; - this._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; diff --git a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs index 09c40503e8..77d708c5c8 100644 --- a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs +++ b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs @@ -34,10 +34,10 @@ public class InheritedThemeFileResult /// public string ResultThemeName { get; set; } - public bool IsExplicit { get; set; } + public bool IsBased { get; set; } /// - /// The query string, e.g. '?explicit' + /// The query string, e.g. '?base' /// public string Query { get; set; } } diff --git a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs index 2f337b8c39..1fb1f2240d 100644 --- a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs @@ -19,7 +19,20 @@ 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; + } + /// /// Generate random digit code /// @@ -27,19 +40,21 @@ public static partial class CommonHelper /// Result string public static string GenerateRandomDigitCode(int length) { - var random = new Random(); - string str = string.Empty; - for (int i = 0; i < length; i++) - str = String.Concat(str, random.Next(10).ToString()); - return str; - } + var buffer = new int[length]; + for (int i = 0; i < length; ++i) + { + buffer[i] = GetRandomizer().Next(10); + } + + return string.Join("", buffer); + } /// - /// Returns an random interger number within a specified rage + /// Returns a random number within the range to - 1. /// /// Minimum number - /// Maximum number - /// Result + /// Maximum number (exclusive!). + /// Random integer number. public static int GenerateRandomInteger(int min = 0, int max = 2147483647) { var randomNumberBuffer = new byte[10]; diff --git a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs index a7f0a60e39..40572c1b8b 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs @@ -104,10 +104,10 @@ public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false /// Items to be downloaded public async Task DownloadAsync(FileDownloadManagerContext context, IEnumerable items) { - await DownloadFiles(context, items); + await DownloadFilesAsync(context, items); } - private async Task DownloadFiles(FileDownloadManagerContext context, IEnumerable items) + private async Task DownloadFilesAsync(FileDownloadManagerContext context, IEnumerable items) { try { diff --git a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs index ef9e57293e..2ce13789c5 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs @@ -9,13 +9,16 @@ 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(CommonHelper.GetAppSetting("sm:TempDirectory", "~/App_Data/_temp"), subDirectory); + return TempDirInternal(_pathTemp, subDirectory); } /// @@ -24,18 +27,16 @@ public static string TempDir(string subDirectory = null) /// Name of a sub directory to be created and returned (optional) public static string TempDirTenant(string subDirectory = null) { - return TempDirInternal(DataSettings.Current.TenantPath + "/_temp", subDirectory); + return TempDirInternal(_pathTempTenant, subDirectory); } - private static string TempDirInternal(string virtualPath, string subDirectory = null) + private static string TempDirInternal(string path, string subDirectory = null) { - var path = CommonHelper.MapPath(virtualPath); - if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } - + if (subDirectory.HasValue()) { path = Path.Combine(path, subDirectory); @@ -50,41 +51,130 @@ private static string TempDirInternal(string virtualPath, string subDirectory = } /// - /// Safe way to cleanup the temp directory. Should be called via scheduled task. + /// Safe way to cleanup temporary directories. Should be called via scheduled task. /// - public static void TempCleanup() + public static void ClearTempDirectories() { - try - { - var dirs = new string[] { TempDir(), TempDirTenant() }; + 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); + } - foreach (var dir in dirs) + /// + /// 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 { - if (Directory.Exists(dir)) + foreach (var fsi in dir.EnumerateFileSystemInfos()) { - var oldestDate = DateTime.Now.Subtract(new TimeSpan(0, 5, 0, 0)); - var files = Directory.EnumerateFiles(dir); - - foreach (string file in files) + if (fsi is FileInfo file) { - var fi = new FileInfo(file); + if (file.LastWriteTimeUtc >= olderThanDate) + continue; + + if (exceptFileNames.Any(x => x.IsCaseInsensitiveEqual(file.Name))) + continue; - if (fi.LastWriteTime < oldestDate) - Delete(file); + 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(); } } - catch (Exception ex) + + if (deleteIfEmpfy) { - ex.Dump(); + try + { + if (!dir.EnumerateFileSystemInfos().Any()) + { + dir.Delete(true); + } + } + catch (Exception ex) + { + ex.Dump(); + } } } /// /// Safe way to delete a file. /// - public static bool Delete(string path) + public static bool DeleteFile(string path) { if (path.IsEmpty()) return true; @@ -96,7 +186,8 @@ public static bool Delete(string path) throw new UnauthorizedAccessException("Deleting folders not possible due to security reasons: {0}".FormatWith(path)); } - File.Delete(path); // no exception, if file doesn't exists + // No exception, if file doesn't exists + File.Delete(path); return true; } catch (Exception ex) @@ -109,7 +200,7 @@ public static bool Delete(string path) /// /// Safe way to copy a file. /// - public static bool Copy(string sourcePath, string destinationPath, bool overwrite = true, bool deleteSource = false) + public static bool CopyFile(string sourcePath, string destinationPath, bool overwrite = true, bool deleteSource = false) { bool result = true; try @@ -117,7 +208,7 @@ public static bool Copy(string sourcePath, string destinationPath, bool overwrit File.Copy(sourcePath, destinationPath, overwrite); if (deleteSource) - Delete(sourcePath); + DeleteFile(sourcePath); } catch (Exception exc) { @@ -174,73 +265,6 @@ public static bool CopyDirectory(DirectoryInfo source, DirectoryInfo target, boo return result; } - /// - /// Safe way to delete all directory content - /// - /// A directory path - /// Delete directoryPath too - /// Name of files not to be deleted - public static void ClearDirectory(string directoryPath, bool selfToo, List exceptFileNames = null) - { - if (directoryPath.IsEmpty()) - return; - - try - { - var dir = new DirectoryInfo(directoryPath); - - foreach (var fi in dir.GetFiles()) - { - if (exceptFileNames != null && exceptFileNames.Any(x => x.IsCaseInsensitiveEqual(fi.Name))) - continue; - - try - { - fi.IsReadOnly = false; - fi.Delete(); - } - catch (Exception) - { - try - { - Thread.Sleep(0); - fi.Delete(); - } - catch { } - } - } - - foreach (var di in dir.GetDirectories()) - { - ClearDirectory(di.FullName, false); - - try - { - di.Delete(); - } - catch (Exception) - { - try - { - Thread.Sleep(0); - di.Delete(); - } - catch (Exception) { } - } - } - } - catch (Exception) { } - - if (selfToo) - { - try - { - Directory.Delete(directoryPath, true); // just deletes the (now empty) directory - } - catch (Exception) { } - } - } - /// /// Creates a non existing directory name /// diff --git a/src/Libraries/SmartStore.Core/Utilities/Retry.cs b/src/Libraries/SmartStore.Core/Utilities/Retry.cs index 7c42ace8d6..916b64e3b2 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Retry.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Retry.cs @@ -86,11 +86,11 @@ public static async Task RunAsync(Func operation, int attempts, TimeSpan? { Guard.NotNull(operation, nameof(operation)); - Func> wrapper = async () => + async Task wrapper() { - await operation().ConfigureAwait(false); + await operation(); return true; - }; + } await RunAsync(wrapper, attempts, wait, onFailed); } @@ -123,7 +123,7 @@ public static async Task RunAsync(Func> operation, int attempts, T try { // Call the function passed in by the caller. - return await operation().ConfigureAwait(false); + return await operation(); } catch (Exception ex) { diff --git a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs index 4c2b752d0b..721b1a38b8 100644 --- a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs @@ -22,88 +22,111 @@ public static class SeoHelper [SuppressMessage("ReSharper", "PossibleNullReferenceException")] public static string GetSeName(string name, bool convertNonWesternChars, bool allowUnicodeChars, string charConversions = null) { - // Return empty value if text is null - if (name == null) return ""; - - const int maxlen = 400; - - if (charConversions != null && _userSeoCharacterTable == null) - { - InitializeUserSeoCharacterTable(charConversions); - } - - // Normalize - name = name.ToLowerInvariant(); - - var len = name.Length; - var sb = new StringBuilder(len); - var prevdash = false; - - char c; - - for (int i = 0; i < len; i++) - { - c = name[i]; - - if (charConversions != null && _userSeoCharacterTable != null && _userSeoCharacterTable.TryGetValue(c, out string userChar)) - { - sb.Append(userChar); - prevdash = false; - } - else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '/') - { - sb.Append(c); - prevdash = (c == '_'); - } - else if (c == ' ' || c == ',' || c == '.' || c == '\\' || c == '-' || c == '_' || c == '=') - { - if (!prevdash && sb.Length > 0) - { - sb.Append('-'); - prevdash = true; - } - } - else - { - var category = CharUnicodeInfo.GetUnicodeCategory(c); - - if (category >= UnicodeCategory.ConnectorPunctuation && category <= UnicodeCategory.MathSymbol) - { - if (!prevdash && sb.Length > 0) - { - sb.Append('-'); - prevdash = true; - } - } - else if ((int)c >= 128) - { - var prevlen = sb.Length; - var c2 = c; - - if (convertNonWesternChars) - { - c2 = c.TryRemoveDiacritic(); - } - - if ((allowUnicodeChars && Char.IsLetterOrDigit(c2)) || (c2 >= 'a' && c2 <= 'z')) - { - sb.Append(c2); - } - - if (prevlen != sb.Length) prevdash = false; - } - } - - if (i >= maxlen) break; - } - - if (prevdash) - return sb.ToString().Substring(0, sb.Length - 1).Trim('/'); - else - return sb.ToString().Trim('/'); + return GetSeName(name, convertNonWesternChars, allowUnicodeChars, true, charConversions); } - public static void ResetUserSeoCharacterTable() + /// + /// 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 + /// A value indicating whether the forward slash (/) is allowed. Should be false for physical file names. + /// Raw data of semicolon separated char conversions + /// SEO friendly string + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public static string GetSeName( + string name, + bool convertNonWesternChars, + bool allowUnicodeChars, + bool allowForwardSlash, + string charConversions = null) + { + // Return empty value if text is null + if (name == null) return ""; + + const int maxlen = 400; + + if (charConversions != null && _userSeoCharacterTable == null) + { + InitializeUserSeoCharacterTable(charConversions); + } + + // Normalize + name = name.ToLowerInvariant(); + + var len = name.Length; + var sb = new StringBuilder(len); + var prevdash = false; + + char c; + + for (int i = 0; i < len; i++) + { + c = name[i]; + + if (charConversions != null && _userSeoCharacterTable != null && _userSeoCharacterTable.TryGetValue(c, out string userChar)) + { + sb.Append(userChar); + prevdash = false; + } + else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '/') + { + if (c != '/' || allowForwardSlash) + { + sb.Append(c); + prevdash = (c == '_'); + } + } + else if (c == ' ' || c == ',' || c == '.' || c == '\\' || c == '-' || c == '_' || c == '=') + { + if (!prevdash && sb.Length > 0) + { + sb.Append('-'); + prevdash = true; + } + } + else + { + var category = CharUnicodeInfo.GetUnicodeCategory(c); + + if (category >= UnicodeCategory.ConnectorPunctuation && category <= UnicodeCategory.MathSymbol) + { + if (!prevdash && sb.Length > 0) + { + sb.Append('-'); + prevdash = true; + } + } + else if ((int)c >= 128) + { + var prevlen = sb.Length; + var c2 = c; + + if (convertNonWesternChars) + { + c2 = c.TryRemoveDiacritic(); + } + + if ((allowUnicodeChars && Char.IsLetterOrDigit(c2)) || (c2 >= 'a' && c2 <= 'z')) + { + sb.Append(c2); + } + + if (prevlen != sb.Length) prevdash = false; + } + } + + if (i >= maxlen) break; + } + + if (prevdash) + return sb.ToString().Substring(0, sb.Length - 1).Trim('/'); + else + return sb.ToString().Trim('/'); + } + + public static void ResetUserSeoCharacterTable() { if (_userSeoCharacterTable != null) { diff --git a/src/Libraries/SmartStore.Core/Utilities/Threading/KeyedLock.cs b/src/Libraries/SmartStore.Core/Utilities/Threading/KeyedLock.cs new file mode 100644 index 0000000000..e08a6deb1c --- /dev/null +++ b/src/Libraries/SmartStore.Core/Utilities/Threading/KeyedLock.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace SmartStore.Utilities.Threading +{ + public sealed class KeyedLock + { + private static readonly TimeSpan _noTimeout = TimeSpan.FromMilliseconds(-1); + private static readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); + + private int _waiterCount; + + private KeyedLock(object key) + { + Key = key; + Semaphore = new SemaphoreSlim(1, 1); + } + + private object Key { get; set; } + private SemaphoreSlim Semaphore { get; set; } + + public bool HasWaiters + { + get { return _waiterCount > 1; } + } + + public int WaiterCount + { + get { return _waiterCount; } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void IncrementCount() => Interlocked.Increment(ref _waiterCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DecrementCount() => Interlocked.Decrement(ref _waiterCount); + + public static bool IsLockHeld(object key) + { + return _locks.ContainsKey(key); + } + + public static IDisposable Lock(object key) + { + 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 Task LockAsync(object key) + { + 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, 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; + } + } + } +} diff --git a/src/Libraries/SmartStore.Core/WebHelper.cs b/src/Libraries/SmartStore.Core/WebHelper.cs index 6304d682bd..3a94e1a417 100644 --- a/src/Libraries/SmartStore.Core/WebHelper.cs +++ b/src/Libraries/SmartStore.Core/WebHelper.cs @@ -47,13 +47,13 @@ public WebHelper(HttpContextBase httpContext) public virtual string GetUrlReferrer() { - return _httpContext?.Request?.UrlReferrer?.ToString() ?? string.Empty; + return _httpContext.SafeGetHttpRequest()?.UrlReferrer?.ToString() ?? string.Empty; } public virtual string GetClientIdent() { var ipAddress = this.GetCurrentIpAddress(); - var userAgent = _httpContext.Request?.UserAgent.EmptyNull(); + var userAgent = _httpContext.SafeGetHttpRequest()?.UserAgent.EmptyNull(); if (ipAddress.HasValue() && userAgent.HasValue()) { @@ -70,12 +70,13 @@ public virtual string GetCurrentIpAddress() return _ipAddress; } - if (_httpContext == null && _httpContext.Request == null) + var httpRequest = _httpContext.SafeGetHttpRequest(); + if (httpRequest == null) { return string.Empty; } - var vars = _httpContext.Request.ServerVariables; + var vars = httpRequest.ServerVariables; var keysToCheck = new string[] { @@ -90,29 +91,43 @@ public virtual string GetCurrentIpAddress() }; string result = null; + IPAddress ipv6 = null; foreach (var key in keysToCheck) { var ipString = vars[key]; - if (ipString.HasValue()) + + if (!string.IsNullOrEmpty(ipString)) { var arrStrings = ipString.Split(','); - // Take the last entry - ipString = arrStrings[arrStrings.Length - 1].Trim(); - IPAddress address; - if (IPAddress.TryParse(ipString, out address)) - { - result = ipString; - break; - } + // Iterate list from end to start (IPv6 addresses usually have precedence) + for (int i = arrStrings.Length - 1; i >= 0; i--) + { + ipString = arrStrings[i].Trim(); + + if (IPAddress.TryParse(ipString, out var address)) + { + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + ipv6 = address; + } + else + { + result = ipString; + break; + } + } + } } } - if (result == "::1") - { - result = "127.0.0.1"; - } + if (string.IsNullOrEmpty(result) && ipv6 != null) + { + result = ipv6.ToString() == "::1" + ? "127.0.0.1" + : ipv6.MapToIPv4().ToString(); + } return (_ipAddress = result.EmptyNull()); } @@ -126,36 +141,37 @@ public virtual string GetThisPageUrl(bool includeQueryString) public virtual string GetThisPageUrl(bool includeQueryString, bool useSsl) { string url = string.Empty; - if (_httpContext?.Request == null) + var httpRequest = _httpContext.SafeGetHttpRequest(); + + if (httpRequest == null) return url; if (includeQueryString) { - bool appPathPossiblyAppended; - string storeHost = GetStoreHost(useSsl, out appPathPossiblyAppended).TrimEnd('/'); + string storeHost = GetStoreHost(useSsl, out bool appPathPossiblyAppended).TrimEnd('/'); - string rawUrl; + string rawUrl; if (appPathPossiblyAppended) { - rawUrl = _httpContext.Request.AppRelativeCurrentExecutionFilePath.TrimStart('~'); + rawUrl = httpRequest.AppRelativeCurrentExecutionFilePath.TrimStart('~'); - if (_httpContext.Request.Url != null && _httpContext.Request.Url.Query != null) + if (httpRequest.Url != null && httpRequest.Url.Query != null) { - rawUrl += _httpContext.Request.Url.Query; + rawUrl += httpRequest.Url.Query; } } else { - rawUrl = _httpContext.Request.RawUrl; + rawUrl = httpRequest.RawUrl; } url = storeHost + rawUrl; } else { - if (_httpContext.Request.Url != null) + if (httpRequest.Url != null) { - url = _httpContext.Request.Url.GetLeftPart(UriPartial.Path); + url = httpRequest.Url.GetLeftPart(UriPartial.Path); } } @@ -167,9 +183,10 @@ public virtual bool IsCurrentConnectionSecured() if (!_isCurrentConnectionSecured.HasValue) { _isCurrentConnectionSecured = false; - if (_httpContext?.Request != null) + var httpRequest = _httpContext.SafeGetHttpRequest(); + if (httpRequest != null) { - _isCurrentConnectionSecured = _httpContext.Request.IsSecureConnection(); + _isCurrentConnectionSecured = httpRequest.IsSecureConnection(); } } @@ -178,23 +195,7 @@ public virtual bool IsCurrentConnectionSecured() public virtual string ServerVariables(string name) { - string result = string.Empty; - - try - { - if (_httpContext != null && _httpContext.Request != null) - { - if (_httpContext.Request.ServerVariables[name] != null) - { - result = _httpContext.Request.ServerVariables[name]; - } - } - } - catch - { - result = string.Empty; - } - return result; + return _httpContext.SafeGetHttpRequest()?.ServerVariables[name].EmptyNull(); } [SuppressMessage("ReSharper", "UnusedMember.Local")] @@ -239,8 +240,7 @@ private string GetStoreHost(bool useSsl, out bool appPathPossiblyAppended) if (_currentStore == null) { - IStoreContext storeContext; - if (EngineContext.Current.ContainerManager.TryResolve(null, out storeContext)) // Unit test safe! + if (EngineContext.Current.ContainerManager.TryResolve(null, out IStoreContext storeContext)) // Unit test safe! { _currentStore = storeContext.CurrentStore; if (_currentStore == null) @@ -323,19 +323,18 @@ public virtual string GetStoreLocation() public virtual string GetStoreLocation(bool useSsl) { - //return HostingEnvironment.ApplicationVirtualPath; - - bool appPathPossiblyAppended; - string result = GetStoreHost(useSsl, out appPathPossiblyAppended); + string result = GetStoreHost(useSsl, out var appPathPossiblyAppended); if (result.EndsWith("/")) { result = result.Substring(0, result.Length - 1); } - if ( _httpContext?.Request != null) + var httpRequest = _httpContext.SafeGetHttpRequest(); + + if (httpRequest != null) { - var appPath = _httpContext.Request.ApplicationPath; + var appPath = httpRequest.ApplicationPath; if (!appPathPossiblyAppended && !result.EndsWith(appPath, StringComparison.OrdinalIgnoreCase)) { // in a shared ssl scenario the user defined https url could contain @@ -424,13 +423,12 @@ public virtual string RemoveQueryString(string url, string queryString) public virtual T QueryString(string name) { - string queryParam = null; - - if (_httpContext != null && _httpContext.Request.QueryString[name] != null) - queryParam = _httpContext.Request.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); } @@ -499,10 +497,7 @@ private void DeleteMvcTypeCacheFiles() File.Delete(Path.Combine(userCacheDir, "MVC-ControllerTypeCache.xml")); File.Delete(Path.Combine(userCacheDir, "MVC-AreaRegistrationTypeCache.xml")); } - catch - { - - } + catch { } } private bool TryWriteBinFolder() @@ -629,8 +624,9 @@ public static string MakeAllUrlsAbsolute(string html, string protocol, string ho /// /// 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) + public static string GetAbsoluteUrl(string url, HttpRequestBase request, bool enforceScheme = false, string protocol = null) { Guard.NotEmpty(url, nameof(url)); Guard.NotNull(request, nameof(request)); @@ -645,10 +641,12 @@ public static string GetAbsoluteUrl(string url, HttpRequestBase request, bool en return url; } + protocol = protocol ?? request.Url.Scheme; + if (url.StartsWith("//")) { return enforceScheme - ? String.Concat(request.Url.Scheme, ":", url) + ? String.Concat(protocol, ":", url) : url; } @@ -657,7 +655,7 @@ public static string GetAbsoluteUrl(string url, HttpRequestBase request, bool en url = VirtualPathUtility.ToAbsolute(url); } - url = string.Format("{0}://{1}{2}", request.Url.Scheme, request.Url.Authority, url); + url = string.Format("{0}://{1}{2}", protocol, request.Url.Authority, url); return url; } diff --git a/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs b/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs index cdb68ddb14..e055db6a8c 100644 --- a/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs +++ b/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs @@ -115,7 +115,7 @@ internal static DbCacheEntry GetCacheEntry(this IQueryable source, string { return null; } - + if (!cache.RequestTryGet(cacheKey.Key, out var entry)) { entry = cache.RequestPut(cacheKey.Key, valueFactory(), cacheKey.AffectedEntitySets); diff --git a/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs b/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs index e7e2b37c00..67d4561746 100644 --- a/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs +++ b/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs @@ -8,6 +8,7 @@ using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.Tasks; using SmartStore.Core.Infrastructure.DependencyManagement; +using SmartStore.Utilities.Threading; namespace SmartStore.Data.Caching { @@ -192,13 +193,13 @@ public virtual bool TryGet(string key, out object value) key = HashKey(key); var now = DateTime.UtcNow; - var entry = _cache.Get(key); + var entry = _cache.Get(key, independent: true); if (entry != null) { if (entry.HasExpired(now)) { - lock (String.Intern(key)) + using (KeyedLock.Lock(key)) { InvalidateItemUnlocked(entry); } @@ -222,7 +223,7 @@ public virtual DbCacheEntry Put(string key, object value, IEnumerable de key = HashKey(key); - lock (String.Intern(key)) + using (KeyedLock.Lock(key)) { var entitySets = dependentEntitySets.Distinct().ToArray(); var entry = new DbCacheEntry @@ -290,7 +291,7 @@ public virtual void InvalidateItem(string key) Guard.NotEmpty(key, nameof(key)); - lock (String.Intern(key)) + using (KeyedLock.Lock(key)) { InvalidateItemUnlocked(key); } @@ -305,7 +306,7 @@ private void InvalidateItemUnlocked(string key) if (_cache.Contains(key)) { - var entry = _cache.Get(key); + var entry = _cache.Get(key, true); if (entry != null) { InvalidateItemUnlocked(entry); diff --git a/src/Libraries/SmartStore.Data/Caching/EfDbModelStore.cs b/src/Libraries/SmartStore.Data/Caching/EfDbModelStore.cs new file mode 100644 index 0000000000..393becfbf3 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Caching/EfDbModelStore.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity.Infrastructure; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using SmartStore.Core.Plugins; +using SmartStore.Utilities; + +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; + } + } +} diff --git a/src/Libraries/SmartStore.Data/Caching/EfMappingViewCache.cs b/src/Libraries/SmartStore.Data/Caching/EfMappingViewCache.cs new file mode 100644 index 0000000000..80720306ed --- /dev/null +++ b/src/Libraries/SmartStore.Data/Caching/EfMappingViewCache.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Data.Entity.Infrastructure.MappingViews; +using System.Data.Entity.Core.Metadata.Edm; + +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; } + } +} diff --git a/src/Libraries/SmartStore.Data/Caching/EfMappingViewCacheFactory.cs b/src/Libraries/SmartStore.Data/Caching/EfMappingViewCacheFactory.cs new file mode 100644 index 0000000000..021da40be7 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Caching/EfMappingViewCacheFactory.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data.Entity; +using System.Data.Entity.Core.Mapping; +using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Infrastructure; +using System.Data.Entity.Infrastructure.MappingViews; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using SmartStore.Utilities; + +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 { } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs index 4920d45081..c69a6f88d4 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs @@ -12,7 +12,8 @@ public CategoryMap() this.Property(c => c.Name).IsRequired().HasMaxLength(400); this.Property(c => c.FullName).HasMaxLength(400); this.Property(c => c.BottomDescription).IsMaxLength(); - this.Property(c => c.Description).IsMaxLength(); + this.Property(c => c.ExternalLink).HasMaxLength(255).IsOptional(); + this.Property(c => c.Description).IsMaxLength(); this.Property(c => c.MetaKeywords).HasMaxLength(400); this.Property(c => c.MetaTitle).HasMaxLength(400); this.Property(c => c.PageSizeOptions).HasMaxLength(200).IsOptional(); diff --git a/src/Libraries/SmartStore.Data/Mapping/Directory/QuantityUnitMap.cs b/src/Libraries/SmartStore.Data/Mapping/Directory/QuantityUnitMap.cs index 7bd783bcad..a840571e67 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Directory/QuantityUnitMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Directory/QuantityUnitMap.cs @@ -7,12 +7,13 @@ public partial class QuantityUnitMap : EntityTypeConfiguration { public QuantityUnitMap() { - this.ToTable("QuantityUnit"); - this.HasKey(c => c.Id); - this.Property(c => c.Name).IsRequired().HasMaxLength(50); - this.Property(c => c.Description).HasMaxLength(50); - this.Property(c => c.DisplayLocale).HasMaxLength(50); - this.Property(c => c.DisplayOrder); + ToTable("QuantityUnit"); + HasKey(c => c.Id); + Property(c => c.Name).IsRequired().HasMaxLength(50); + Property(c => c.NamePlural).IsRequired().HasMaxLength(50); + Property(c => c.Description).HasMaxLength(50); + Property(c => c.DisplayLocale).HasMaxLength(50); + Property(c => c.DisplayOrder); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.Designer.cs new file mode 100644 index 0000000000..b943551be8 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.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 Menus : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(Menus)); + + string IMigrationMetadata.Id + { + get { return "201904110735029_Menus"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.cs b/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.cs new file mode 100644 index 0000000000..645fdd9314 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.cs @@ -0,0 +1,90 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Core.Data; + using SmartStore.Data.Setup; + using SmartStore.Data.Utilities; + + public partial class Menus : DbMigration, IDataSeeder + { + 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), + }) + .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), + }) + .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"); + DropIndex("dbo.MenuItemRecord", "IX_MenuItem_DisplayOrder"); + DropIndex("dbo.MenuItemRecord", "IX_MenuItem_Published"); + DropIndex("dbo.MenuItemRecord", "IX_MenuItem_ParentItemId"); + DropIndex("dbo.MenuItemRecord", new[] { "MenuId" }); + DropIndex("dbo.MenuRecord", "IX_Menu_SubjectToAcl"); + DropIndex("dbo.MenuRecord", "IX_Menu_LimitedToStores"); + DropIndex("dbo.MenuRecord", "IX_Menu_Published"); + DropIndex("dbo.MenuRecord", "IX_Menu_SystemName_IsSystemMenu"); + DropTable("dbo.MenuItemRecord"); + DropTable("dbo.MenuRecord"); + } + + public bool RollbackOnFailure => true; + + public void Seed(SmartObjectContext context) + { + if (DataSettings.DatabaseIsInstalled()) + { + DataMigrator.CreateSystemMenus(context); + } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.resx b/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.resx new file mode 100644 index 0000000000..3c944a5f47 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201904110735029_Menus.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/201905020948354_WidgetTopics.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.Designer.cs new file mode 100644 index 0000000000..7a420fcffe --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.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 WidgetTopics : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(WidgetTopics)); + + string IMigrationMetadata.Id + { + get { return "201905020948354_WidgetTopics"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.cs b/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.cs new file mode 100644 index 0000000000..304479485c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.cs @@ -0,0 +1,49 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Topics; + using SmartStore.Data.Setup; + + public partial class WidgetTopics : DbMigration, IDataSeeder + { + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false)) + { + var widgetTopics = new[] + { + "CheckoutAsGuestOrRegister", + "ContactUs", + "ForumWelcomeMessage", + "HomePageText", + "LoginRegistrationInfo" + }; + + var topics = context.Set().Where(x => widgetTopics.Contains(x.SystemName)).ToList(); + topics.Each(x => + { + x.RenderAsWidget = true; + x.WidgetWrapContent = false; + }); + + context.SaveChanges(); + } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.resx b/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.resx new file mode 100644 index 0000000000..274628a4fc --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905020948354_WidgetTopics.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/201905101159134_V320Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.Designer.cs new file mode 100644 index 0000000000..e07144c09d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.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 V320Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V320Resources)); + + string IMigrationMetadata.Id + { + get { return "201905101159134_V320Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.cs new file mode 100644 index 0000000000..231a748e84 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.cs @@ -0,0 +1,876 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Core.Domain.Catalog; + 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 void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + + MigrateSettings(context); + context.SaveChanges(); + } + + public void MigrateSettings(SmartObjectContext context) + { + context.MigrateSettings(x => + { + x.Add(TypeHelper.NameOf(y => y.CacheSegmentSize, true), 500); + x.Add(TypeHelper.NameOf(y => y.AlwaysPrefetchTranslations, true), false); + x.Add(TypeHelper.NameOf(y => y.AlwaysPrefetchUrlSlugs, true), false); + + // New CatalogSettings properties + x.Add(TypeHelper.NameOf(y => y.ShowSubCategoriesInSubPages, true), false); + x.Add(TypeHelper.NameOf(y => y.ShowDescriptionInSubPages, true), false); + x.Add(TypeHelper.NameOf(y => y.IncludeFeaturedProductsInSubPages, true), false); + }); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.ReturnRequests.MaxRefundAmount", + "Maximum refund amount", + "Maximaler Erstattungsbetrag", + "The maximum amount that can be refunded for this return request.", + "Der maximale Betrag, der fr diesen Rcksendewunsch erstattet werden kann."); + + builder.AddOrUpdate("Admin.Customers.Customers.Fields.Title", + "Title", + "Titel", + "Specifies the title.", + "Legt den Titel fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.FolderName.Validate", + "Please enter a valid, relative folder path for the export data. The path must be at least 3 characters long and not the application folder.", + "Bitte einen gltigen, relativen Ordnerpfad fr die zu exportierenden Daten eingeben. Der Pfad muss mindestens 3 Zeichen lang und nicht der Anwendungsordner sein."); + + builder.AddOrUpdate("Admin.Catalog.Customers.CustomerSearchType", "Search in:", "Suche in:"); + + // Fix some FluentValidation german translations + builder.AddOrUpdate("Validation.LengthValidator") + .Value("de", "'{PropertyName}' muss zwischen {MinLength} und {MaxLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); + builder.AddOrUpdate("Validation.MinimumLengthValidator") + .Value("de", "'{PropertyName}' muss mind. {MinLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); + builder.AddOrUpdate("Validation.MaximumLengthValidator") + .Value("de", "'{PropertyName}' darf max. {MaxLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); + builder.AddOrUpdate("Validation.ExactLengthValidator") + .Value("de", "'{PropertyName}' muss genau {MaxLength} lang sein. Sie haben {TotalLength} Zeichen eingegeben."); + builder.AddOrUpdate("Validation.ExclusiveBetweenValidator") + .Value("de", "'{PropertyName}' muss grer als {From} und kleiner als {To} sein. Sie haben '{Value}' eingegeben."); + builder.AddOrUpdate("Validation.InclusiveBetweenValidator") + .Value("de", "'{PropertyName}' muss zwischen {From} and {To} liegen. Sie haben '{Value}' eingegeben."); + builder.AddOrUpdate("Validation.NotNullValidator") + .Value("de", "'{PropertyName}' ist erforderlich."); + builder.AddOrUpdate("Validation.NotEmptyValidator") + .Value("de", "'{PropertyName}' ist erforderlich."); + builder.AddOrUpdate("Validation.LessThanValidator") + .Value("de", "'{PropertyName}' muss kleiner sein als '{ComparisonValue}'."); + builder.AddOrUpdate("Validation.RegularExpressionValidator") + .Value("de", "'{PropertyName}' entspricht nicht dem erforderlichen Muster."); + builder.AddOrUpdate("Validation.ScalePrecisionValidator") + .Value("de", "'{PropertyName}' darf insgesamt nicht mehr als {expectedPrecision} Ziffern enthalten, unter Bercksichtigung von {expectedScale} Dezimalstellen. {digits} Ziffern und {actualScale} Dezimalstellen wurden gefunden."); + + // Some new resources for custom validators + builder.AddOrUpdate("Validation.CreditCardCvvNumberValidator", + "'{PropertyName}' is invalid.", + "'{PropertyName}' ist ungltig."); + + // Get rid of duplicate validator resource entries + builder.Delete( + "Admin.Catalog.Products.Fields.Name.Required", + "Admin.Catalog.Categories.Fields.Name.Required", + "Admin.Catalog.Manufacturers.Fields.Name.Required", + "Admin.Validation.RequiredField", + "Admin.Catalog.Attributes.ProductAttributes.Fields.Name.Required", + "Admin.Catalog.ProductReviews.Fields.Title.Required", + "Admin.Catalog.ProductReviews.Fields.ReviewText.Required", + "Admin.Catalog.ProductTags.Fields.Name.Required", + "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.Name.Required", + "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.Quantity.GreaterOrEqualToOne", + "Admin.Catalog.Attributes.SpecificationAttributes.Options.Fields.Name.Required", + "Admin.Catalog.Attributes.SpecificationAttributes.Fields.Name.Required", + "Admin.ContentManagement.Blog.BlogPosts.Fields.Title.Required", + "Admin.ContentManagement.Blog.BlogPosts.Fields.Body.Required", + "Admin.Common.GenericAttributes.Fields.Name.Required", + "Admin.Customers.CustomerRoles.Fields.Name.Required", + "Admin.Configuration.Countries.Fields.Name.Required", + "Admin.Configuration.Countries.Fields.TwoLetterIsoCode.Required", + "Admin.Configuration.Countries.Fields.TwoLetterIsoCode.Length", + "Admin.Configuration.Countries.Fields.ThreeLetterIsoCode.Required", + "Admin.Configuration.Countries.Fields.ThreeLetterIsoCode.Length", + "Admin.Configuration.Measures.Dimensions.Fields.Name.Required", + "Admin.Configuration.Measures.Dimensions.Fields.SystemKeyword.Required", + "Admin.Configuration.Measures.Weights.Fields.Name.Required", + "Admin.Configuration.Measures.Weights.Fields.SystemKeyword.Required", + "Admin.Configuration.Countries.States.Fields.Name.Required", + "Admin.Configuration.DeliveryTimes.Fields.Name.Required", + "Admin.Configuration.DeliveryTimes.Fields.ColorHexValue.Required", + "Admin.Configuration.DeliveryTimes.Fields.ColorHexValue.Range", + "Admin.Configuration.DeliveryTimes.Fields.Name.Range", + "Admin.Configuration.Currencies.Fields.Name.Required", + "Admin.Configuration.Currencies.Fields.Name.Range", + "Admin.Configuration.Currencies.Fields.CurrencyCode.Required", + "Admin.Configuration.Currencies.Fields.CurrencyCode.Range", + "Admin.Configuration.Currencies.Fields.Rate.Range", + "Admin.Configuration.Currencies.Fields.CustomFormatting.Validation", + "Admin.Promotions.Discounts.Fields.Name.Required", + "Admin.ContentManagement.Forums.ForumGroup.Fields.Name.Required", + "Admin.ContentManagement.Forums.Forum.Fields.Name.Required", + "Admin.ContentManagement.Forums.Forum.Fields.ForumGroupId.Required", + "Admin.Configuration.Languages.Resources.Fields.Name.Required", + "Admin.Configuration.Languages.Resources.Fields.Value.Required", + "Admin.Configuration.Languages.Fields.Name.Required", + "Admin.Configuration.Languages.Fields.UniqueSeoCode.Required", + "Admin.Configuration.Languages.Fields.UniqueSeoCode.Length", + "Admin.Promotions.Campaigns.Fields.Name.Required", + "Admin.Promotions.Campaigns.Fields.Subject.Required", + "Admin.Promotions.Campaigns.Fields.Body.Required", + "Admin.ContentManagement.MessageTemplates.Fields.Subject.Required", + "Admin.ContentManagement.MessageTemplates.Fields.Body.Required", + "Admin.Promotions.NewsLetterSubscriptions.Fields.Email.Required", + "Admin.System.QueuedEmails.Fields.Priority.Required", + "Admin.System.QueuedEmails.Fields.From.Required", + "Admin.System.QueuedEmails.Fields.To.Required", + "Admin.System.QueuedEmails.Fields.SentTries.Required", + "Admin.System.QueuedEmails.Fields.Priority.Range", + "Admin.System.QueuedEmails.Fields.SentTries.Range", + "Admin.ContentManagement.News.NewsItems.Fields.Title.Required", + "Admin.ContentManagement.News.NewsItems.Fields.Short.Required", + "Admin.ContentManagement.News.NewsItems.Fields.Full.Required", + "Admin.Catalog.Attributes.CheckoutAttributes.Fields.Name.Required", + "Admin.Catalog.Attributes.CheckoutAttributes.Values.Fields.Name.Required", + "Admin.Configuration.Plugins.Fields.FriendlyName.Required", + "Admin.ContentManagement.Polls.Answers.Fields.Name.Required", + "Admin.ContentManagement.Polls.Fields.Name.Required", + "Admin.Configuration.Shipping.Methods.Fields.Name.Required", + "Admin.Configuration.Stores.Fields.Name.Required", + "Admin.Configuration.Stores.Fields.Url.Required", + "Admin.Configuration.Settings.AllSettings.Fields.Name.Required", + "Admin.System.ScheduleTasks.Name.Required", + "Admin.Configuration.Tax.Categories.Fields.Name.Required", + "Admin.ContentManagement.Topics.Fields.SystemName.Required", + "Admin.Address.Fields.FirstName.Required", + "Admin.Address.Fields.LastName.Required", + "Admin.Address.Fields.Email.Required", + "Admin.Address.Fields.Company.Required", + "Admin.Address.Fields.City.Required", + "Admin.Address.Fields.Address1.Required", + "Admin.Address.Fields.Address2.Required", + "Admin.Address.Fields.ZipPostalCode.Required", + "Admin.Address.Fields.PhoneNumber.Required", + "Admin.Address.Fields.FaxNumber.Required", + "Admin.Address.Fields.EmailMatch.Required", + "Admin.Customers.Customers.Fields.FirstName.Required", + "Admin.Customers.Customers.Fields.LastName.Required", + "Admin.Customers.Customers.Fields.Company.Required", + "Admin.Customers.Customers.Fields.StreetAddress.Required", + "Admin.Customers.Customers.Fields.StreetAddress2.Required", + "Admin.Customers.Customers.Fields.ZipPostalCode.Required", + "Admin.Customers.Customers.Fields.City.Required", + "Admin.Customers.Customers.Fields.Phone.Required", + "Admin.Customers.Customers.Fields.Fax.Required", + "Admin.Validation.Name", + "Admin.Validation.EmailAddress", + "Admin.Validation.Url", + "Admin.Validation.UsernamePassword", + "Admin.DataExchange.Export.FileNamePattern.Validate", + "Admin.DataExchange.Export.Partition.Validate", + "Admin.Common.WrongEmail", + "Payment.CardCode.Wrong" + ); + + // Get rid of duplicate CreatedOn resources also + builder.Delete( + "Admin.Affiliates.Orders.CreatedOn", + "Admin.ContentManagement.Blog.Comments.Fields.CreatedOn", + "Admin.ContentManagement.Blog.BlogPosts.Fields.CreatedOn", + "Admin.ContentManagement.Blog.BlogPosts.Fields.CreatedOn", + "Admin.Catalog.ProductReviews.Fields.CreatedOn", + "Admin.Customers.Customers.Fields.CreatedOn", + "Admin.Customers.Customers.Orders.CreatedOn", + "Admin.Customers.Customers.ActivityLog.CreatedOn", + "Admin.Orders.Fields.CreatedOn", + "Admin.Customers.Customers.Fields.CreatedOn", + "Admin.Promotions.NewsLetterSubscriptions.Fields.CreatedOn", + "Admin.Configuration.Currencies.Fields.CreatedOn", + "Admin.Promotions.Discounts.History.CreatedOn", + "Admin.ContentManagement.Forums.ForumGroup.Fields.CreatedOn", + "Admin.ContentManagement.Forums.Forum.Fields.CreatedOn", + "Admin.Configuration.ActivityLog.ActivityLog.Fields.CreatedOn", + "Admin.System.Log.Fields.CreatedOn", + "Admin.Promotions.Campaigns.Fields.CreatedOn", + "Admin.Promotions.NewsLetterSubscriptions.Fields.CreatedOn", + "Admin.System.QueuedEmails.Fields.CreatedOn", + "Admin.ContentManagement.News.Comments.Fields.CreatedOn", + "Admin.ContentManagement.News.NewsItems.Fields.CreatedOn", + "Admin.GiftCards.Fields.CreatedOn", + "Admin.GiftCards.History.CreatedOn", + "Admin.Orders.Fields.CreatedOn", + "Admin.Orders.OrderNotes.Fields.CreatedOn", + "Admin.RecurringPayments.History.CreatedOn", + "Admin.ReturnRequests.Fields.CreatedOn" + ); + + // duplicate validator resource entries in frontend + builder.Delete( + "Blog.Comments.CommentText.Required", + "Forum.TextCannotBeEmpty", + "Forum.TopicSubjectCannotBeEmpty", + "Forum.TextCannotBeEmpty", + "Account.Fields.Email.Required", + "Products.AskQuestion.Question.Required", + "Account.Fields.FullName.Required", + "Products.EmailAFriend.FriendEmail.Required", + "Products.EmailAFriend.YourEmailAddress.Required", + "Reviews.Fields.Title.Required", + "Reviews.Fields.Title.MaxLengthValidation", + "Reviews.Fields.ReviewText.Required", + "Address.Fields.FirstName.Required", + "Address.Fields.LastName.Required", + "Address.Fields.Email.Required", + "Account.Fields.Company.Required", + "Account.Fields.StreetAddress.Required", + "Account.Fields.StreetAddress2.Required", + "Account.Fields.ZipPostalCode.Required", + "Account.Fields.City.Required", + "Account.Fields.Phone.Required", + "Account.Fields.Fax.Required", + "Admin.Address.Fields.EmailMatch.Required", + "ContactUs.Email.Required", + "ContactUs.Enquiry.Required", + "ContactUs.FullName.Required", + "Account.ChangePassword.Fields.OldPassword.Required", + "Account.ChangePassword.Fields.NewPassword.Required", + "Account.ChangePassword.Fields.NewPassword.LengthValidation", + "Account.ChangePassword.Fields.ConfirmNewPassword.Required", + "Account.ChangePassword.Fields.NewPassword.LengthValidation", + "Account.Fields.Email.Required", + "Account.Fields.FirstName.Required", + "Account.Fields.LastName.Required", + "Account.Fields.Company.Required", + "Account.Fields.StreetAddress.Required", + "Account.Fields.StreetAddress2.Required", + "Account.Fields.ZipPostalCode.Required", + "Account.Fields.City.Required", + "Account.Fields.Phone.Required", + "Account.Fields.Fax.Required", + "Account.Fields.Password.Required", + "Account.Fields.Vat.Required", + "Account.PasswordRecovery.NewPassword.Required", + "Account.PasswordRecovery.NewPassword.LengthValidation", + "Account.PasswordRecovery.ConfirmNewPassword.Required", + "Account.PasswordRecovery.Email.Required", + "News.Comments.CommentTitle.Required", + "News.Comments.CommentTitle.MaxLengthValidation", + "News.Comments.CommentText.Required", + "PrivateMessages.SubjectCannotBeEmpty", + "PrivateMessages.MessageCannotBeEmpty", + "Wishlist.EmailAFriend.FriendEmail.Required", + "Wishlist.EmailAFriend.YourEmailAddress.Required" + ); + + // remove duplicate resources for display order + builder.Delete( + "Admin.Catalog.Categories.Fields.DisplayOrder", + "Admin.Catalog.Categories.Products.Fields.DisplayOrder", + "Admin.Catalog.Manufacturers.Fields.DisplayOrder", + "Admin.Catalog.Manufacturers.Products.Fields.DisplayOrder", + "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.DisplayOrder", + "Admin.Catalog.Products.BundleItems.Fields.DisplayOrder", + "Admin.Catalog.Products.Fields.HomePageDisplayOrder", + "Admin.Catalog.Products.SpecificationAttributes.Fields.DisplayOrder", + "Admin.Catalog.Products.Pictures.Fields.DisplayOrder", + "Admin.Catalog.Products.Categories.Fields.DisplayOrder", + "Admin.Catalog.Products.Manufacturers.Fields.DisplayOrder", + "Admin.Catalog.Products.RelatedProducts.Fields.DisplayOrder", + "Admin.Catalog.Products.AssociatedProducts.Fields.DisplayOrder", + "Admin.Catalog.Products.BundleItems.Fields.DisplayOrder", + "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Fields.DisplayOrder", + "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.DisplayOrder", + "Admin.Catalog.Products.SpecificationAttributes.Fields.DisplayOrder", + "Admin.Catalog.Attributes.SpecificationAttributes.Fields.DisplayOrder", + "Admin.Catalog.Attributes.SpecificationAttributes.Options.Fields.DisplayOrder", + "Admin.Catalog.Categories.Fields.DisplayOrder", + "Admin.Catalog.Manufacturers.Fields.DisplayOrder", + "Admin.Configuration.Countries.Fields.DisplayOrder", + "Admin.Configuration.Currencies.Fields.DisplayOrder", + "Admin.Configuration.DeliveryTimes.Fields.DisplayOrder", + "Admin.Configuration.Measures.Dimensions.Fields.DisplayOrder", + "Admin.Configuration.Measures.Weights.Fields.DisplayOrder", + "Admin.Configuration.Countries.States.Fields.DisplayOrder", + "Admin.ContentManagement.Forums.ForumGroup.Fields.DisplayOrder", + "Admin.ContentManagement.Forums.Forum.Fields.DisplayOrder", + "Admin.Configuration.Languages.Fields.DisplayOrder", + "Admin.Catalog.Attributes.CheckoutAttributes.Fields.DisplayOrder", + "Admin.Catalog.Attributes.CheckoutAttributes.Values.Fields.DisplayOrder", + "Admin.Configuration.Plugins.Fields.DisplayOrder", + "Admin.ContentManagement.Polls.Answers.Fields.DisplayOrder", + "Admin.ContentManagement.Polls.Fields.DisplayOrder", + "Admin.Configuration.Shipping.Methods.Fields.DisplayOrder", + "Admin.Configuration.Stores.Fields.DisplayOrder", + "Admin.Configuration.Tax.Categories.Fields.DisplayOrder" + ); + + builder.AddOrUpdate("Common.DisplayOrder.Hint", + "Specifies display order. 1 represents the top of the list.", + "Legt die Anzeige-Prioritt fest. 1 steht bspw. fr das erste Element in der Liste."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.UseInvisibleReCaptcha", + "Use invisible reCAPTCHA", + "Unsichtbaren reCAPTCHA verwenden", + "Does not require the user to click on a checkbox, instead it is invoked directly when the user submits a form. By default only the most suspicious traffic will be prompted to solve a captcha.", + "Der Benutzer muss nicht auf ein Kontrollkstchen klicken, sondern die Validierung erfolgt direkt beim Absenden eines Formulars. Nur bei 'verdchtigem' Traffic wird der Benutzer aufgefordert, ein Captcha zu lsen."); + + builder.AddOrUpdate("Admin.ContentManagement.Topics.Fields.ShortTitle", + "Short title", + "Kurztitel", + "Optional. Used as link text. If empty, 'Title' sets the link text.", + "Optional. Wird u.A. als Linktext verwendet. Wenn leer, stellt 'Titel' den Linktext."); + + builder.AddOrUpdate("Admin.ContentManagement.Topics.Fields.Intro", + "Intro", + "Intro", + "Optional. Short introduction / teaser.", + "Optional. Einleitung / Teaser."); + + builder.AddOrUpdate("Common.Download.Versions", "Versions", "Versionen"); + builder.AddOrUpdate("Common.Download.Version", "Version", "Version"); + builder.AddOrUpdate("Common.Download.Delete", "Delete download", "Download lschen"); + builder.AddOrUpdate("Common.Downloads", "Downloads", "Downloads"); + + builder.AddOrUpdate("Admin.Catalog.Products.Fields.NewVersionDownloadId", + "New download version", + "Neue Version des Downloads", + "Upload a new version of the download file here.", + "Laden Sie hier eine neue Version der Download-Datei hoch."); + + builder.AddOrUpdate("Admin.Catalog.Products.Download.VersionDelete", "Delete this file version.", "Diese Dateiversion lschen."); + builder.AddOrUpdate("Admin.Catalog.Products.Download.AddChangelog", "Edit changelog", "nderungshistorie bearbeiten"); + builder.AddOrUpdate("Customer.Downloads.NoChangelogAvailable", "No changelog available.", "Keine nderungshistorie verfgbar."); + + builder.AddOrUpdate("Admin.Catalog.Products.Download.SemanticVersion.NotValid", + "The specified version information is not valid. Please enter the version number in the correct format (e.g.: 1.0.0.0, 2.0 or 3.1.5).", + "Die angegebenen Versionsinformationen sind nicht gltig. Bitte geben Sie die Versionsnummer in korrektem Format an (z.B.: 1.0.0.0, 2.0 oder 3.1.5)."); + + builder.AddOrUpdate("Admin.Catalog.Products.Fields.HasPreviewPicture", + "Exclude first image from gallery", + "Erstes Bild aus Gallerie ausschlieen", + "Activate this option if the first image should be displayed as a preview in product lists but not in the product detail gallery.", + "Aktivieren Sie diese Option, wenn das erste Bild als Vorschau in Produktlisten, nicht aber in der Produktdetail-Gallerie angezeigt werden soll."); + + builder.AddOrUpdate("Products.Free", "Free", "Kostenlos"); + + builder.AddOrUpdate("Admin.Catalog.Products.Fields.ProductTags.Hint", + "Product tags are keywords that this product can also be identified by. Enter a list of the tags to be associated with this product. The more products associated with a particular tag, the larger it will show on the tag cloud.", + "Eine Liste von Schlsselwrtern, die das Produkt taxonomisch charakterisieren. Je mehr Produkte einem Schlsselwort (Tag) zugeordnet sind, desto mehr visuelles Gewicht erhlt das Tag."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.Initial", "Position", "Position"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.Relevance", "Relevance", "Beste Ergebnisse"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.SubjectAsc", "Title: A to Z", "Titel: A bis Z"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.SubjectDesc", "Title: Z to A", "Titel: Z bis A"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.UserNameAsc", "User name: A to Z", "Benutzername: A bis Z"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.UserNameDesc", "User name: Z to A", "Benutzername: Z bis A"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.CreatedOnAsc", "Created on: Oldest first", "Erstellt am: ltere zuerst"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.CreatedOnDesc", "Created on: Newest first", "Erstellt am: neuere zuerst"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.PostsAsc", "Post number: ascending", "Anzahl Beitrge: aufsteigend"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.PostsDesc", "Post number: descending", "Anzahl Beitrge: absteigend"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastVisit", "Since last visit", "Seit dem letzten Besuch"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.Yesterday", "Yesterday", "Gestern"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastWeek", "Last week", "In der letzten Woche"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastTwoWeeks", "Last 2 weeks", "In den letzten 2 Wochen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastMonth", "Last month", "Im letzten Monat"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastThreeMonths", "Last 3 months", "In den letzten 3 Monaten"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastSixMonths", "Last 6 months", "In den letzten 6 Monaten"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastYear", "Last year", "Im letzten Jahr"); + + builder.AddOrUpdate("Search.Facet.Forum", "Forum", "Forum"); + builder.AddOrUpdate("Search.Facet.Customer", "User name", "Benutzername"); + builder.AddOrUpdate("Search.Facet.Date", "Period", "Zeitraum"); + builder.AddOrUpdate("Search.Facet.Date.Newer", "and newer", "und neuer"); + builder.AddOrUpdate("Search.Facet.Date.Older", "and older", "und lter"); + + builder.AddOrUpdate("Forum.PostText", "Post text", "Beitragstext"); + builder.AddOrUpdate("Forum.Sticky", "Sticky topic", "Festes Thema"); + + builder.AddOrUpdate("Search.HitsFor", "{0} hits for {1}", "{0} Treffer fr {1}"); + builder.AddOrUpdate("Search.NoMoreHitsFound", "There were no more hits found.", "Es wurden keine weiteren Treffer gefunden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Search.WildcardSearchNote", + "The wildcard mode can slow down the search for a large number of objects.", + "Der Wildcard-Modus kann bei einer groen Anzahl an Objekten die Suche verlangsamen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Search.SearchMode", + "Search mode", + "Suchmodus", + "Specifies the search mode. Please keep in mind that the search mode can - depending on number of objects - strongly affect search performance. 'Is equal to' is the fastest, 'Contains' the slowest.", + "Legt den Suchmodus fest. Bitte beachten Sie, dass der Suchmodus die Geschwindigkeit der Suche (abhngig von der Objektanzahl) beeinflusst. 'Ist gleich' ist am schnellsten, 'Beinhaltet' am langsamsten."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Search.Forum.SearchFields", + "Search fields", + "Suchfelder", + "Specifies additional search fields. The topic title is always searched.", + "Legt zustzlich zu durchsuchende Felder fest. Der Thementitel wird grundstzlich immer durchsucht."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Search.DefaultSortOrder", + "Default sort order", + "Standardsortierreihenfolge", + "Specifies the default sort order in search results.", + "Legt die Standardsortierreihenfolge in den Suchergebnissen fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Search.InstantSearchNumberOfHits", + "Number of hits", + "Anzahl der Treffer", + "Specifies the number of hits displayed in instant search.", + "Legt die Anzahl der angezeigten Suchtreffer in der Instantsuche fest."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Forums.AllowSorting", + "Allow sorting", + "Sortierung zulassen", + "Specifies whether forum posts can be sorted.", + "Legt fest, ob Forenbeitrge sortiert werden knnen."); + + builder.AddOrUpdate("Admin.Common.DefaultPageSizeOptions", + "Page size options", + "Auswahlmglichkeiten fr Seitengre", + "Comma-separated page size options that a customer can select in lists.", + "Kommagetrennte Liste mit Optionen fr Seitengre, die ein Kunde in Listen whlen kann."); + + builder.AddOrUpdate("Admin.Common.AllowCustomersToSelectPageSize", + "Allow customers to select page size", + "Kunde kann Listengre ndern", + "Whether customers are allowed to select the page size from a predefined list of options.", + "Kunden knnen die Listengre mit Hilfe einer vorgegebenen Optionsliste ndern."); + + + builder.Delete( + "Admin.Configuration.Settings.Search.DefaultSortOrderMode", + "Admin.Configuration.Settings.Search.InstantSearchNumberOfProducts", + "Admin.Configuration.Settings.CustomerUser.DefaultAvatarEnabled", + "Forum.Search.LimitResultsToPrevious.AllResults", + "Forum.Search.LimitResultsToPrevious.1day", + "Forum.Search.LimitResultsToPrevious.7days", + "Forum.Search.LimitResultsToPrevious.2weeks", + "Forum.Search.LimitResultsToPrevious.1month", + "Forum.Search.LimitResultsToPrevious.3months", + "Forum.Search.LimitResultsToPrevious.6months", + "Forum.Search.LimitResultsToPrevious.1year", + "Forum.Search.SearchInForum.All", + "Forum.Search.SearchWithin.All", + "Forum.Search.SearchWithin.TopicTitlesOnly", + "Forum.Search.SearchWithin.PostTextOnly", + "Forum.SearchTermMinimumLengthIsNCharacters", + "Enums.SmartStore.Core.Domain.Forums.ForumSearchType.All", + "Enums.SmartStore.Core.Domain.Forums.ForumSearchType.PostTextOnly", + "Enums.SmartStore.Core.Domain.Forums.ForumSearchType.TopicTitlesOnly", + "Forum.AdvancedSearch", + "Forum.SearchButton", + "Forum.PageTitle.Search"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.PriceDisplayStyle", + "Price display style", + "Preisdarstellung", + "Specifies the form in which prices are displayed in product lists and on the product detail page.", + "Bestimmt die Darstellungform von Preisen in Produktlisten und auf der Produktdetailseite."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.DisplayTextForZeroPrices", + "Display text when prices are 0,00", + "Zeige Text wenn Preise 0,00 sind", + "Specifies whether to display a textual resource (free) instead of the value 0.00.", + "Bestimmt, ob statt dem Wert 0,00 eine textuelle Resource (kostenlos) angezeigt werden soll."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayStyle.Default", "Default", "Standard"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayStyle.BadgeAll", "In bagdes", "Markiert"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayStyle.BadgeFreeProductsOnly", "Badge free products only", "Nur kostenlose Produkte markieren"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.WorkingLanguageId", + "Language", + "Sprache", + "Filter by language", + "Nach Sprache filtern"); + + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.CaptchaShowOnForumPage", + "Show on forum pages", + "Auf Forenseiten anzeigen", + "Specifies whether to display a CAPTCHA on forum pages when creating or replying to a topic.", + "Legt fest, ob ein CAPTCHA auf Forenseiten angezeigt werden soll, wenn ein Thema erstellt oder darauf geantwortet wird."); + + builder.AddOrUpdate("Admin.Catalog.Products.BundleItems.NoProductLinkageForBundleItem", + "The product \"{0}\" cannot be assigned an attribute of the type \"product\" because it is bundle item of a product bundle.", + "Dem Produkt \"{0}\" kann kein Attribut vom Typ \"Produkt\" zugeordnet werden, weil es auf der Stckliste eines Produkt-Bundle steht."); + + builder.AddOrUpdate("Search.RelatedSearchTerms", + "Related search terms", + "Verwandte Suchbegriffe"); + + builder.AddOrUpdate("Plugins.CannotLoadModule", + "The plugin or provider \"{0}\" cannot be loaded.", + "Das Plugin oder der Provider \"{0}\" kann nicht geladen werden."); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.RunPerMachine", + "Run per machine", + "Pro Maschine ausfhren", + "Indicates whether the task is executed decidedly on each machine of a web farm.", + "Gibt an, ob die Aufgabe auf jeder Maschine einer Webfarm dezidiert ausgefhrt wird."); + + builder.Delete("Address.Fields.Required.Hint"); + + builder.AddOrUpdate("Common.FormFields.Required.Hint", + "* Input elements with asterisk are required and have to be filled out.", + "* Eingabefelder mit Sternchen sind Pflichfelder und mssen ausgefllt werden."); + + builder.AddOrUpdate("Forum.Post.Vote.OnlyRegistered", + "Only registered users can vote for posts.", + "Nur registrierte Benutzer knnen Beitrge bewerten."); + + builder.AddOrUpdate("Forum.Post.Vote.OwnPostNotAllowed", + "You cannot vote for your own post.", + "Sie knnen nicht Ihren eigenen Beitrag bewerten."); + + builder.AddOrUpdate("Forum.Post.Vote.SuccessfullyVoted", + "Thank you for your vote.", + "Danke fr Ihre Bewertung."); + + builder.AddOrUpdate("Common.Liked", "Liked", "Gefllt"); + builder.AddOrUpdate("Common.LikeIt", "I like it", "Gefllt mir"); + builder.AddOrUpdate("Common.DoNotLikeIt", "I do not like it anymore", "Gefllt mir nicht mehr"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Forums.AllowCustomersToVoteOnPosts", + "Allow customers to vote on posts", + "Benutzer knnen Beitrge bewerten", + "Specifies whether customers can vote on posts.", + "Legt fest, ob Benutzer Beitrge bewerten knnen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Forums.AllowGuestsToVoteOnPosts", + "Allow guests to vote on posts", + "Gste knnen Beitrge bewerten", + "Specifies whether guests can vote on posts.", + "Legt fest, ob Gste Beitrge bewerten knnen."); + + // Typos. + builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements") + .Value("de", "Voraussetzungen"); + builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements.DiscountRequirementType") + .Value("de", "Typ der Voraussetzung"); + builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements.DiscountRequirementType.Hint") + .Value("de", "Voraussetzungen fr den Rabatt"); + builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements.Remove") + .Value("de", "Voraussetzung fr den Rabatt entfernen"); + builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements.SaveBeforeEdit") + .Value("de", "Sie mssen den Rabatt zunchst speichern, bevor Sie Voraussetzungen fr seine Anwendung festlegen knnen"); + + builder.AddOrUpdate("Common.Voting", "Voting", "Abstimmung"); + builder.AddOrUpdate("Common.Answer", "Answer", "Antwort"); + builder.AddOrUpdate("Common.Size", "Size", "Gre"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerFormFields.Description", + "Manage form fields that are displayed during registration.", + "Verwalten Sie Formularfelder, die whrend der Registrierung angezeigt werden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields.Description", + "Manage form fields that are displayed during checkout and on \"My account\" page.", + "Verwalten Sie Formularfelder, die whrend des Checkout-Prozesses und im \"Mein Konto\" Bereich angezeigt werden."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.RelatedEntityType.TierPrice", "Tier price", "Staffelpreis"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.RelatedEntityType.ProductVariantAttributeValue", "Attribute option", "Attribut-Option"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.RelatedEntityType.ProductVariantAttributeCombination", "Attribute combination", "Attribut-Kombination"); + + builder.AddOrUpdate("Admin.DataExchange.Export.ExportRelatedData.Validate", + "Related data cannot be exported if the option \"Export attribute combinations\" is activated.", + "Zugehrige Daten knnen nicht exportiert werden, wenn die Option \"Attributkombinationen exportieren\" aktiviert ist."); + + builder.AddOrUpdate("Admin.Common.ProcessingInfo", + "{0}: {1} of {2} processed", + "{0}: {1} von {2} verarbeitet"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowSubCategoriesInSubPages", + "Show subcategories also in subpages", + "Unterwarengruppen auch in Unterseiten anzeigen", + "Subpage: List index greater than 1 or any active filter.", + "Unterseite: Listenindex grer 1 oder mind. ein aktiver Filter."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowDescriptionInSubPages", + "Show page description also in subpages", + "Seitenbeschreibungen auch in Unterseiten anzeigen", + "Subpage: List index greater than 1 or any active filter.", + "Unterseite: Listenindex grer 1 oder mind. ein aktiver Filter."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.IncludeFeaturedProductsInSubPages", + "Show featured products also in subpages", + "Top-Produkte auch in Unterseiten anzeigen", + "Subpage: List index greater than 1 or any active filter.", + "Unterseite: Listenindex grer 1 oder mind. ein aktiver Filter."); + + builder.AddOrUpdate("Admin.Common.CopyOf", "Copy of {0}", "Kopie von {0}"); + + builder.AddOrUpdate("Admin.Configuration.Languages.DefaultLanguage.Note", + "The default language of the shop is {0}. The default is always the first published language.", + "Die Standardsprache des Shops ist {0}. Standard ist stets die erste verffentlichte Sprache."); + + 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.BrowseFiles", "Browse", "Durchsuchen"); + builder.AddOrUpdate("Common.Url", "URL", "URL"); + builder.AddOrUpdate("Common.File", "File", "Datei"); + builder.AddOrUpdate("Common.Entity.Product", "Product", "Produkt"); + builder.AddOrUpdate("Common.Entity.Category", "Category", "Warengruppe"); + builder.AddOrUpdate("Common.Entity.Manufacturer", "Manufacturer", "Hersteller"); + builder.AddOrUpdate("Common.Entity.Topic", "Topic", "Seite"); + + builder.AddOrUpdate("Common.Entity.SelectProduct", "Select product", "Produkt auswhlen"); + builder.AddOrUpdate("Common.Entity.SelectCategory", "Select category", "Warengruppe auswhlen"); + builder.AddOrUpdate("Common.Entity.SelectManufacturer", "Select manufacturer", "Hersteller auswhlen"); + builder.AddOrUpdate("Common.Entity.SelectTopic", "Select topic", "Seite auswhlen"); + + builder.Delete("Admin.Customers.Customers.List.SearchDeletedOnly"); + builder.AddOrUpdate("Admin.Customers.Customers.List.SearchActiveOnly", "Only activated customers", "Nur aktivierte Kunden"); + + builder.AddOrUpdate("Products.LoginForPrice", + "Prices will be displayed after login.", + "Preise werden nach Anmeldung angezeigt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowLoginForPriceNote", + "Show login for price note", + "Hinweis \"Preis nach Anmeldung\" anzeigen", + "Specifies whether to display a message stating that prices will not be displayed until login.", + "Legt fest, ob ein Hinweis erscheinen soll, dass Preise erst nach Anmeldung angezeigt werden."); + + builder.AddOrUpdate("Products.EmailAFriend.LoginNote", + "Please log in to use this function. Login now", + "Bitte melden Sie sich an, um diese Funktion nutzen zu knnen. Jetzt anmelden"); + + builder.AddOrUpdate("Account.Login.Fields.UsernameOrEmail", + "Username or email", + "Benutzername oder Email"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerLoginType", + "Customer login type", + "Art des Kundenlogins", + "Specifies the customer login type.", + "Legt die Art des Kundenlogins fest."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerLoginType.Username", "Username", "Benutzername"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerLoginType.Email", "Email", "Email"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerLoginType.UsernameOrEmail", "Username or email", "Benutzername oder Email"); + + builder.AddOrUpdate("Admin.ContentManagement.Topics.Fields.HtmlId", + "Html Id", + "Html-ID", + "Specifies the Html Id of the page.", + "Legt die Html-ID der Seite fest."); + + builder.AddOrUpdate("Admin.ContentManagement.Topics.Fields.BodyCssClass", + "Body CSS class", + "Body CSS-Klasse", + "Specifies the CSS class of the body element.", + "Legt die CSS-Klasse des Body-Elements fest."); + + builder.AddOrUpdate("Admin.ContentManagement.Topics.Validation.NoWhiteSpace", + "Whitespace isn't allowed.", + "Leerzeichen sind nicht erlaubt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Tax.VatRequired", + "Customers must enter a VAT number", + "Kunden mssen eine Steuernummer angeben", + "Specifies whether customers must enter a VAT identification number.", + "Legt fest, ob Kunden bei der Registrierung eine Steuernummer angeben mssen."); + + builder.AddOrUpdate("Common.Top", "Top", "Oben"); + builder.AddOrUpdate("Common.Bottom", "Bottom", "Unten"); + builder.AddOrUpdate("Common.Left", "Left", "Links"); + builder.AddOrUpdate("Common.Right", "Right", "Rechts"); + builder.AddOrUpdate("Common.TopLeft", "Top left", "Links oben"); + builder.AddOrUpdate("Common.TopRight", "Top right", "Rechts oben"); + builder.AddOrUpdate("Common.BottomLeft", "Bottom left", "Links unten"); + builder.AddOrUpdate("Common.BottomRight", "Bottom right", "Rechts unten"); + builder.AddOrUpdate("Common.Center", "Center", "Mitte"); + builder.AddOrUpdate("Common.NoTitle", "No title", "Ohne Titel"); + builder.AddOrUpdate("Common.MoveUp", "Move up", "Nach oben"); + builder.AddOrUpdate("Common.MoveDown", "Move down", "Nach unten"); + + builder.AddOrUpdate("Common.IncreaseValue", "Increase value", "Wert erhhen"); + builder.AddOrUpdate("Common.DecreaseValue", "Decrease value", "Wert verringern"); + builder.AddOrUpdate("Common.QueryString", "Query string", "Query String"); + + builder.AddOrUpdate("Admin.ContentManagement.Menus", "Menus", "Mens"); + builder.AddOrUpdate("Admin.ContentManagement.AddMenu", "Add menu", "Men hinzufgen"); + builder.AddOrUpdate("Admin.ContentManagement.EditMenu", "Edit menu", "Men bearbeiten"); + + builder.AddOrUpdate("Admin.ContentManagement.MenuLinks", "Menu items", "Men Links"); + builder.AddOrUpdate("Admin.ContentManagement.AddMenuItem", "Add menu item", "Men Link hinzufgen"); + builder.AddOrUpdate("Admin.ContentManagement.EditMenuItem", "Edit menu item", "Men Link bearbeiten"); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.NoMenuItemsAvailable", + "There are no menu links available.", + "Es sind keine Men Links vorhanden."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.CannotBeDeleted", + "This menu is required by your shop and can therefore not be deleted.", + "Dieses Men wird von Ihrem Shop bentigt und kann daher nicht gelscht werden."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.CatalogNote", + "The category tree is dynamically integrated into the menu.", + "Der Warengruppenbaum wird dynamisch in das Men eingebunden."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.SpecifyLinkTarget", + "Please specify link target", + "Bitte Link Ziel angeben"); + + builder.AddOrUpdate("Providers.MenuItems.FriendlyName.Entity", "Internal object or URL", "Internes Objekt oder URL"); + builder.AddOrUpdate("Providers.MenuItems.FriendlyName.Route", "Route", "Route"); + builder.AddOrUpdate("Providers.MenuItems.FriendlyName.Catalog", "Category tree", "Warengruppenbaum"); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.RouteName", "Route name", "Name der Route"); + builder.AddOrUpdate("Admin.ContentManagement.Menus.RouteValues", "Route values (JSON)", "Route Werte (JSON)"); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.SystemName", + "System name", + "Systemname", + "The system name of the menu.", + "Der Systemname des Mens."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Template", + "Design template", + "Design Vorlage", + "The template defines the way how the menu is displayed.", + "ber die Vorlage wird die Darstellungsart des Mens festgelegt."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.WidgetZone", + "Widget zone", + "Widget Zone", + "Specifies widget zones in which the menu should be displayed.", + "Legt Widget Zonen fest, in denen das Men dargestellt werden soll."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Title", + "Title", + "Titel", + "Specifies the title.", + "Legt den Titel fest."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Published", + "Published", + "Verffentlicht", + "Specifies whether the menu is visible in the shop.", + "Legt fest, ob das Men im Shop sichtbar ist."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.DisplayOrder", + "Display order", + "Reihenfolge", + "Specifies the display order of the widget zones.", + "Legt die Darstellungreihenfolge der Widget Zonen fest."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.ParentItem", + "Parent menu item", + "bergeordnetes Menelement", + "Specifies the parent menu item. Leave the field empty to create a first-level menu item.", + "Legt das bergeordnete Menelement fest. Lassen Sie das Feld leer, um ein Menelement erster Ebene zu erzeugen."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.LinkTarget", + "Target", + "Ziel", + "Specifies the link target.", + "Legt das Ziel des Links fest."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.ShortDescription", + "Short description", + "Kurzbeschreibung", + "Specifies a short description. Used as the 'title' attribute for the menu link.", + "Legt eine Kurzbeschreibung fest. Wird als 'title' Attribut fr das Menelement verwendet."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.PermissionNames", + "Required permissions", + "Erforderliche Rechte", + "Specifies access permissions that are required to display the menu item (at least 1 permission must be granted).", + "Legt Zugriffsrechte fest, die fr die Anzeige des Menelementes erforderlich sind (mind. 1 Recht muss gewhrt sein)."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.Published", + "Published", + "Verffentlicht", + "Specifies whether the menu item is visible in the shop.", + "Legt fest, ob das Menelement im Shop sichtbar ist."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.DisplayOrder", + "Display order", + "Reihenfolge", + "Specifies the order of the menu item within a menu level.", + "Legt die Reihenfolge des Menelements innerhalb einer Menebene fest."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.BeginGroup", + "Begin group", + "Gruppe beginnen", + "Inserts a separator before the link and optionally a heading (short description).", + "Fgt vor den Link ein Trennelement sowie optional eine berschrift ein (Kurzbeschreibung)."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.ShowExpanded", + "Show expanded", + "Geffnet anzeigen", + "If selected and this menu item has children, the menu will initially appear expanded.", + "Legt fest, ob das Men anfnglich geffnet ist, sofern es Kindelemente besitzt."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.NoFollow", + "nofollow", + "nofollow", + "Sets the HTML attribute rel='nofollow'.", + "Gibt das HTML-Attribut rel='nofollow' aus."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.NewWindow", + "Open in new browser tab", + "In neuem Browsertab ffnen"); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.Icon", + "Icon", + "Icon", + "Specifies an optional icon.", + "Legt ein optionales Icon fest."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.HtmlId", + "HTML ID", + "HTML ID", + "Sets the HTML ID attribute for the menu link.", + "Legt das HTML ID Attribut fr das Menelement fest."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.CssClass", + "CSS class", + "CSS Klasse", + "Sets a CSS class for the menu link.", + "Legt eine CSS Klasse fr das Menelement fest."); + + builder.Delete("Admin.Configuration.Settings.GeneralCommon.SocialSettings.GooglePlusLink"); + builder.Delete("Admin.Configuration.Settings.GeneralCommon.SocialSettings.GooglePlusLink.Hint"); + + builder.AddOrUpdate("Products.BasePriceInfo.LanguageInsensitive", + "{0} / {1} {2}", + "{0} / {1} {2}"); + + builder.AddOrUpdate("Common.Advanced", + "Advanced", + "Erweitert"); + + builder.AddOrUpdate("ShoppingCart.DiscountCouponCode.NoMoreDiscount", + "Further discounts are not possible.", + "Eine weitere Rabattierung ist nicht mglich."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.resx b/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.resx new file mode 100644 index 0000000000..47694659f0 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905101159134_V320Resources.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/201905271110370_V321Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.Designer.cs new file mode 100644 index 0000000000..a194593e7c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.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 V321Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V321Resources)); + + string IMigrationMetadata.Id + { + get { return "201905271110370_V321Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs new file mode 100644 index 0000000000..89ade79f68 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs @@ -0,0 +1,35 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class V321Resources : 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(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + 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."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.resx b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.resx new file mode 100644 index 0000000000..eea8366a2c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.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/201906252008551_QuantityUnitNamePlural.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.Designer.cs new file mode 100644 index 0000000000..186f72e27d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.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 QuantityUnitNamePlural : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(QuantityUnitNamePlural)); + + string IMigrationMetadata.Id + { + get { return "201906252008551_QuantityUnitNamePlural"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs new file mode 100644 index 0000000000..313530a024 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs @@ -0,0 +1,170 @@ +namespace SmartStore.Data.Migrations +{ + using System.Collections.Generic; + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Directory; + using SmartStore.Core.Domain.Localization; + using SmartStore.Data.Setup; + + public partial class QuantityUnitNamePlural : DbMigration, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.QuantityUnit", "NamePlural", c => c.String(nullable: false, maxLength: 50, defaultValue: "" )); + } + + public override void Down() + { + DropColumn("dbo.QuantityUnit", "NamePlural"); + } + + public bool RollbackOnFailure => true; + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + + if (!DataSettings.DatabaseIsInstalled()) + { + return; + } + + var quPluralDe = new Dictionary + { + { "Stck", "Stck" }, + { "Schachtel", "Schachteln" }, + { "Paket", "Pakete" }, + { "Palette", "Paletten" }, + { "Kiste", "Kisten" }, + { "Einheit", "Einheiten" }, + { "Sack", "Scke" }, + { "Tte", "Tten" }, + { "Dose", "Dosen" }, + { "Stange", "Stangen" }, + { "Flasche", "Flaschen" }, + { "Glas", "Glser" }, + { "Bund", "Bnde" }, + { "Rolle", "Rollen" }, + { "Fass", "Fsser" }, + { "Set", "Sets" } + }; + + var quPluralEn = new Dictionary + { + { "Piece", "Pieces" }, + { "Box", "Boxes" }, + { "Parcel", "Parcels" }, + { "Palette", "Pallets" }, + { "Unit", "Units" }, + { "Sack", "Sacks" }, + { "Bag", "Bags" }, + { "Can", "Cans" }, + { "Tin", "Tins" }, + { "Packet", "Packets" }, + { "Package", "Packages" }, + { "Bar", "Bars" }, + { "Bottle", "Bottles" }, + { "Glass", "Glasses" }, + { "Bunch", "Bunches" }, + { "Roll", "Rolls" }, + { "Cup", "Cups" }, + { "Bundle", "Bundles" }, + { "Barrel", "Barrels" }, + { "Set", "Sets" }, + { "Bucket", "Buckets" } + }; + + using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false, autoCommit: false)) + { + var languages = context.Set().ToDictionary(x => x.Id, x => x); + var defaultLang = languages.Values.OrderBy(x => x.DisplayOrder).First(); + + // Quantity units. + var quantityUnits = context.Set().ToList(); + if (quantityUnits.Any()) + { + foreach (var quantityUnit in quantityUnits) + { + var name = quantityUnit.Name.EmptyNull(); + string namePlural = null; + + if (defaultLang.UniqueSeoCode.IsCaseInsensitiveEqual("de")) + { + quPluralDe.TryGetValue(name, out namePlural); + } + else if (defaultLang.UniqueSeoCode.IsCaseInsensitiveEqual("en")) + { + quPluralEn.TryGetValue(name, out namePlural); + } + + quantityUnit.NamePlural = namePlural.NullEmpty() ?? name; + } + + scope.Commit(); + } + + // Localized properties. + var lpSet = context.Set(); + var pluralCount = lpSet + .Where(x => x.LocaleKeyGroup == "QuantityUnit" && x.LocaleKey == "NamePlural") + .Count(); + + if (pluralCount == 0) + { + var localizedProperties = lpSet + .Where(x => x.LocaleKeyGroup == "QuantityUnit" && x.LocaleKey == "Name") + .ToList(); + + if (localizedProperties.Any()) + { + foreach (var lp in localizedProperties) + { + if (languages.TryGetValue(lp.LanguageId, out var language)) + { + var name = lp.LocaleValue.EmptyNull(); + string namePlural = null; + + if (language.UniqueSeoCode.IsCaseInsensitiveEqual("de")) + { + quPluralDe.TryGetValue(name, out namePlural); + } + else if (language.UniqueSeoCode.IsCaseInsensitiveEqual("en")) + { + quPluralEn.TryGetValue(name, out namePlural); + } + + if (namePlural.HasValue()) + { + lpSet.Add(new LocalizedProperty + { + EntityId = lp.EntityId, + LanguageId = lp.LanguageId, + LocaleKeyGroup = lp.LocaleKeyGroup, + LocaleKey = "NamePlural", + LocaleValue = namePlural + }); + } + } + } + + scope.Commit(); + } + } + } + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Common.Plural", "Plural", "Mehrzahl"); + + builder.AddOrUpdate("Admin.Configuration.QuantityUnit.Fields.NamePlural", + "Name plural", + "Name Mehrzahl", + "Sets the name in plural. Example: \"Barrels\" for the unit \"Barrel\".", + "Legt den Namen in Mehrzahl fest. Beispiel: \"Fsser\" fr die Verpackungseinheit \"Fass\"."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.resx b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.resx new file mode 100644 index 0000000000..887e09fc9d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.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/201907032251575_CategoryExternalLink.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.Designer.cs new file mode 100644 index 0000000000..784b2cb7ce --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.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 CategoryExternalLink : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CategoryExternalLink)); + + string IMigrationMetadata.Id + { + get { return "201907032251575_CategoryExternalLink"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs new file mode 100644 index 0000000000..708ca38b83 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs @@ -0,0 +1,18 @@ +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/201907032251575_CategoryExternalLink.resx b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.resx new file mode 100644 index 0000000000..3a1de6db32 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.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/201908150749388_V322Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.Designer.cs new file mode 100644 index 0000000000..be8c91d1d2 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.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 V322Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V322Resources)); + + string IMigrationMetadata.Id + { + get { return "201908150749388_V322Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs new file mode 100644 index 0000000000..12a38b031e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs @@ -0,0 +1,69 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class V322Resources : 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(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + 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("Checkout.OrderCompletes", + "Your order will be completed.", + "Ihre Bestellung wird abgeschlossen."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.CheckoutAttributes.Fields.TextPrompt", + "Text prompt", + "Text Eingabeaufforderung", + "Specifies the prompt text.", + "Legt den Text zur Eingabeaufforderung fest."); + + builder.AddOrUpdate("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Fields.TextPrompt", + "Text prompt", + "Text Eingabeaufforderung"); + + builder.AddOrUpdate("Admin.Catalog.Categories.Fields.ExternalLink", + "External link", + "Externer Link", + "Alternative external link for this category in the main menu and in category listings. For example, to a landing page that contains a back link to the category.", + "Abweichender, externer Verweis fr diese Warengruppe im Hauptmen und in Warengruppen-Listings. Z.B. auf eine Landingpage, die einen Rckverweis auf die Warengruppe enthlt."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Title", + "Title", + "Titel", + "Specifies the title. Please keep in mind that this title is not displayed in all design templates.", + "Legt den Titel fest. Bitte beachten Sie, dass dieser Titel nicht bei allen Design-Vorlagen ausgegeben wird."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.InvalidRouteValues", + "Please check the link data. No link can be created on the basis of your input.", + "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."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.resx b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.resx new file mode 100644 index 0000000000..a3562fc0f2 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.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 fa81751642..18a610b862 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -30,683 +30,27 @@ public MigrationsConfiguration() public void SeedDatabase(SmartObjectContext context) { - Seed(context); + using (var scope = new DbContextScope(context, hooksEnabled: false)) + { + Seed(context); + scope.Commit(); + } } protected override void Seed(SmartObjectContext context) { context.MigrateLocaleResources(MigrateLocaleResources); MigrateSettings(context); - - context.SaveChanges(); } public void MigrateSettings(SmartObjectContext context) { - context.MigrateSettings(x => - { - x.Add(TypeHelper.NameOf(y => y.CacheSegmentSize, true), 500); - x.Add(TypeHelper.NameOf(y => y.AlwaysPrefetchTranslations, true), false); - x.Add(TypeHelper.NameOf(y => y.AlwaysPrefetchUrlSlugs, true), false); - // New CatalogSettings properties - x.Add(TypeHelper.NameOf(y => y.ShowSubCategoriesInSubPages, true), false); - x.Add(TypeHelper.NameOf(y => y.ShowDescriptionInSubPages, true), false); - x.Add(TypeHelper.NameOf(y => y.IncludeFeaturedProductsInSubPages, true), false); - }); } public void MigrateLocaleResources(LocaleResourcesBuilder builder) { - builder.AddOrUpdate("Admin.ReturnRequests.MaxRefundAmount", - "Maximum refund amount", - "Maximaler Erstattungsbetrag", - "The maximum amount that can be refunded for this return request.", - "Der maximale Betrag, der für diesen Rücksendewunsch erstattet werden kann."); - - builder.AddOrUpdate("Admin.Customers.Customers.Fields.Title", - "Title", - "Titel", - "Specifies the title.", - "Legt den Titel fest."); - - builder.AddOrUpdate("Admin.DataExchange.Export.FolderName.Validate", - "Please enter a valid, relative folder path for the export data. The path must be at least 3 characters long and not the application folder.", - "Bitte einen gültigen, relativen Ordnerpfad für die zu exportierenden Daten eingeben. Der Pfad muss mindestens 3 Zeichen lang und nicht der Anwendungsordner sein."); - - builder.AddOrUpdate("Admin.Catalog.Customers.CustomerSearchType", "Search in:", "Suche in:"); - - // Fix some FluentValidation german translations - builder.AddOrUpdate("Validation.LengthValidator") - .Value("de", "'{PropertyName}' muss zwischen {MinLength} und {MaxLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); - builder.AddOrUpdate("Validation.MinimumLengthValidator") - .Value("de", "'{PropertyName}' muss mind. {MinLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); - builder.AddOrUpdate("Validation.MaximumLengthValidator") - .Value("de", "'{PropertyName}' darf max. {MaxLength} Zeichen lang sein. Sie haben {TotalLength} Zeichen eingegeben."); - builder.AddOrUpdate("Validation.ExactLengthValidator") - .Value("de", "'{PropertyName}' muss genau {MaxLength} lang sein. Sie haben {TotalLength} Zeichen eingegeben."); - builder.AddOrUpdate("Validation.ExclusiveBetweenValidator") - .Value("de", "'{PropertyName}' muss größer als {From} und kleiner als {To} sein. Sie haben '{Value}' eingegeben."); - builder.AddOrUpdate("Validation.InclusiveBetweenValidator") - .Value("de", "'{PropertyName}' muss zwischen {From} and {To} liegen. Sie haben '{Value}' eingegeben."); - builder.AddOrUpdate("Validation.NotNullValidator") - .Value("de", "'{PropertyName}' ist erforderlich."); - builder.AddOrUpdate("Validation.NotEmptyValidator") - .Value("de", "'{PropertyName}' ist erforderlich."); - builder.AddOrUpdate("Validation.LessThanValidator") - .Value("de", "'{PropertyName}' muss kleiner sein als '{ComparisonValue}'."); - builder.AddOrUpdate("Validation.RegularExpressionValidator") - .Value("de", "'{PropertyName}' entspricht nicht dem erforderlichen Muster."); - builder.AddOrUpdate("Validation.ScalePrecisionValidator") - .Value("de", "'{PropertyName}' darf insgesamt nicht mehr als {expectedPrecision} Ziffern enthalten, unter Berücksichtigung von {expectedScale} Dezimalstellen. {digits} Ziffern und {actualScale} Dezimalstellen wurden gefunden."); - - // Some new resources for custom validators - builder.AddOrUpdate("Validation.CreditCardCvvNumberValidator", - "'{PropertyName}' is invalid.", - "'{PropertyName}' ist ungültig."); - - // Get rid of duplicate validator resource entries - builder.Delete( - "Admin.Catalog.Products.Fields.Name.Required", - "Admin.Catalog.Categories.Fields.Name.Required", - "Admin.Catalog.Manufacturers.Fields.Name.Required", - "Admin.Validation.RequiredField", - "Admin.Catalog.Attributes.ProductAttributes.Fields.Name.Required", - "Admin.Catalog.ProductReviews.Fields.Title.Required", - "Admin.Catalog.ProductReviews.Fields.ReviewText.Required", - "Admin.Catalog.ProductTags.Fields.Name.Required", - "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.Name.Required", - "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.Quantity.GreaterOrEqualToOne", - "Admin.Catalog.Attributes.SpecificationAttributes.Options.Fields.Name.Required", - "Admin.Catalog.Attributes.SpecificationAttributes.Fields.Name.Required", - "Admin.ContentManagement.Blog.BlogPosts.Fields.Title.Required", - "Admin.ContentManagement.Blog.BlogPosts.Fields.Body.Required", - "Admin.Common.GenericAttributes.Fields.Name.Required", - "Admin.Customers.CustomerRoles.Fields.Name.Required", - "Admin.Configuration.Countries.Fields.Name.Required", - "Admin.Configuration.Countries.Fields.TwoLetterIsoCode.Required", - "Admin.Configuration.Countries.Fields.TwoLetterIsoCode.Length", - "Admin.Configuration.Countries.Fields.ThreeLetterIsoCode.Required", - "Admin.Configuration.Countries.Fields.ThreeLetterIsoCode.Length", - "Admin.Configuration.Measures.Dimensions.Fields.Name.Required", - "Admin.Configuration.Measures.Dimensions.Fields.SystemKeyword.Required", - "Admin.Configuration.Measures.Weights.Fields.Name.Required", - "Admin.Configuration.Measures.Weights.Fields.SystemKeyword.Required", - "Admin.Configuration.Countries.States.Fields.Name.Required", - "Admin.Configuration.DeliveryTimes.Fields.Name.Required", - "Admin.Configuration.DeliveryTimes.Fields.ColorHexValue.Required", - "Admin.Configuration.DeliveryTimes.Fields.ColorHexValue.Range", - "Admin.Configuration.DeliveryTimes.Fields.Name.Range", - "Admin.Configuration.Currencies.Fields.Name.Required", - "Admin.Configuration.Currencies.Fields.Name.Range", - "Admin.Configuration.Currencies.Fields.CurrencyCode.Required", - "Admin.Configuration.Currencies.Fields.CurrencyCode.Range", - "Admin.Configuration.Currencies.Fields.Rate.Range", - "Admin.Configuration.Currencies.Fields.CustomFormatting.Validation", - "Admin.Promotions.Discounts.Fields.Name.Required", - "Admin.ContentManagement.Forums.ForumGroup.Fields.Name.Required", - "Admin.ContentManagement.Forums.Forum.Fields.Name.Required", - "Admin.ContentManagement.Forums.Forum.Fields.ForumGroupId.Required", - "Admin.Configuration.Languages.Resources.Fields.Name.Required", - "Admin.Configuration.Languages.Resources.Fields.Value.Required", - "Admin.Configuration.Languages.Fields.Name.Required", - "Admin.Configuration.Languages.Fields.UniqueSeoCode.Required", - "Admin.Configuration.Languages.Fields.UniqueSeoCode.Length", - "Admin.Promotions.Campaigns.Fields.Name.Required", - "Admin.Promotions.Campaigns.Fields.Subject.Required", - "Admin.Promotions.Campaigns.Fields.Body.Required", - "Admin.ContentManagement.MessageTemplates.Fields.Subject.Required", - "Admin.ContentManagement.MessageTemplates.Fields.Body.Required", - "Admin.Promotions.NewsLetterSubscriptions.Fields.Email.Required", - "Admin.System.QueuedEmails.Fields.Priority.Required", - "Admin.System.QueuedEmails.Fields.From.Required", - "Admin.System.QueuedEmails.Fields.To.Required", - "Admin.System.QueuedEmails.Fields.SentTries.Required", - "Admin.System.QueuedEmails.Fields.Priority.Range", - "Admin.System.QueuedEmails.Fields.SentTries.Range", - "Admin.ContentManagement.News.NewsItems.Fields.Title.Required", - "Admin.ContentManagement.News.NewsItems.Fields.Short.Required", - "Admin.ContentManagement.News.NewsItems.Fields.Full.Required", - "Admin.Catalog.Attributes.CheckoutAttributes.Fields.Name.Required", - "Admin.Catalog.Attributes.CheckoutAttributes.Values.Fields.Name.Required", - "Admin.Configuration.Plugins.Fields.FriendlyName.Required", - "Admin.ContentManagement.Polls.Answers.Fields.Name.Required", - "Admin.ContentManagement.Polls.Fields.Name.Required", - "Admin.Configuration.Shipping.Methods.Fields.Name.Required", - "Admin.Configuration.Stores.Fields.Name.Required", - "Admin.Configuration.Stores.Fields.Url.Required", - "Admin.Configuration.Settings.AllSettings.Fields.Name.Required", - "Admin.System.ScheduleTasks.Name.Required", - "Admin.Configuration.Tax.Categories.Fields.Name.Required", - "Admin.ContentManagement.Topics.Fields.SystemName.Required", - "Admin.Address.Fields.FirstName.Required", - "Admin.Address.Fields.LastName.Required", - "Admin.Address.Fields.Email.Required", - "Admin.Address.Fields.Company.Required", - "Admin.Address.Fields.City.Required", - "Admin.Address.Fields.Address1.Required", - "Admin.Address.Fields.Address2.Required", - "Admin.Address.Fields.ZipPostalCode.Required", - "Admin.Address.Fields.PhoneNumber.Required", - "Admin.Address.Fields.FaxNumber.Required", - "Admin.Address.Fields.EmailMatch.Required", - "Admin.Customers.Customers.Fields.FirstName.Required", - "Admin.Customers.Customers.Fields.LastName.Required", - "Admin.Customers.Customers.Fields.Company.Required", - "Admin.Customers.Customers.Fields.StreetAddress.Required", - "Admin.Customers.Customers.Fields.StreetAddress2.Required", - "Admin.Customers.Customers.Fields.ZipPostalCode.Required", - "Admin.Customers.Customers.Fields.City.Required", - "Admin.Customers.Customers.Fields.Phone.Required", - "Admin.Customers.Customers.Fields.Fax.Required", - "Admin.Validation.Name", - "Admin.Validation.EmailAddress", - "Admin.Validation.Url", - "Admin.Validation.UsernamePassword", - "Admin.DataExchange.Export.FileNamePattern.Validate", - "Admin.DataExchange.Export.Partition.Validate", - "Admin.Common.WrongEmail", - "Payment.CardCode.Wrong" - ); - - // Get rid of duplicate CreatedOn resources also - builder.Delete( - "Admin.Affiliates.Orders.CreatedOn", - "Admin.ContentManagement.Blog.Comments.Fields.CreatedOn", - "Admin.ContentManagement.Blog.BlogPosts.Fields.CreatedOn", - "Admin.ContentManagement.Blog.BlogPosts.Fields.CreatedOn", - "Admin.Catalog.ProductReviews.Fields.CreatedOn", - "Admin.Customers.Customers.Fields.CreatedOn", - "Admin.Customers.Customers.Orders.CreatedOn", - "Admin.Customers.Customers.ActivityLog.CreatedOn", - "Admin.Orders.Fields.CreatedOn", - "Admin.Customers.Customers.Fields.CreatedOn", - "Admin.Promotions.NewsLetterSubscriptions.Fields.CreatedOn", - "Admin.Configuration.Currencies.Fields.CreatedOn", - "Admin.Promotions.Discounts.History.CreatedOn", - "Admin.ContentManagement.Forums.ForumGroup.Fields.CreatedOn", - "Admin.ContentManagement.Forums.Forum.Fields.CreatedOn", - "Admin.Configuration.ActivityLog.ActivityLog.Fields.CreatedOn", - "Admin.System.Log.Fields.CreatedOn", - "Admin.Promotions.Campaigns.Fields.CreatedOn", - "Admin.Promotions.NewsLetterSubscriptions.Fields.CreatedOn", - "Admin.System.QueuedEmails.Fields.CreatedOn", - "Admin.ContentManagement.News.Comments.Fields.CreatedOn", - "Admin.ContentManagement.News.NewsItems.Fields.CreatedOn", - "Admin.GiftCards.Fields.CreatedOn", - "Admin.GiftCards.History.CreatedOn", - "Admin.Orders.Fields.CreatedOn", - "Admin.Orders.OrderNotes.Fields.CreatedOn", - "Admin.RecurringPayments.History.CreatedOn", - "Admin.ReturnRequests.Fields.CreatedOn" - ); - - // duplicate validator resource entries in frontend - builder.Delete( - "Blog.Comments.CommentText.Required", - "Forum.TextCannotBeEmpty", - "Forum.TopicSubjectCannotBeEmpty", - "Forum.TextCannotBeEmpty", - "Account.Fields.Email.Required", - "Products.AskQuestion.Question.Required", - "Account.Fields.FullName.Required", - "Products.EmailAFriend.FriendEmail.Required", - "Products.EmailAFriend.YourEmailAddress.Required", - "Reviews.Fields.Title.Required", - "Reviews.Fields.Title.MaxLengthValidation", - "Reviews.Fields.ReviewText.Required", - "Address.Fields.FirstName.Required", - "Address.Fields.LastName.Required", - "Address.Fields.Email.Required", - "Account.Fields.Company.Required", - "Account.Fields.StreetAddress.Required", - "Account.Fields.StreetAddress2.Required", - "Account.Fields.ZipPostalCode.Required", - "Account.Fields.City.Required", - "Account.Fields.Phone.Required", - "Account.Fields.Fax.Required", - "Admin.Address.Fields.EmailMatch.Required", - "ContactUs.Email.Required", - "ContactUs.Enquiry.Required", - "ContactUs.FullName.Required", - "Account.ChangePassword.Fields.OldPassword.Required", - "Account.ChangePassword.Fields.NewPassword.Required", - "Account.ChangePassword.Fields.NewPassword.LengthValidation", - "Account.ChangePassword.Fields.ConfirmNewPassword.Required", - "Account.ChangePassword.Fields.NewPassword.LengthValidation", - "Account.Fields.Email.Required", - "Account.Fields.FirstName.Required", - "Account.Fields.LastName.Required", - "Account.Fields.Company.Required", - "Account.Fields.StreetAddress.Required", - "Account.Fields.StreetAddress2.Required", - "Account.Fields.ZipPostalCode.Required", - "Account.Fields.City.Required", - "Account.Fields.Phone.Required", - "Account.Fields.Fax.Required", - "Account.Fields.Password.Required", - "Account.Fields.Vat.Required", - "Account.PasswordRecovery.NewPassword.Required", - "Account.PasswordRecovery.NewPassword.LengthValidation", - "Account.PasswordRecovery.ConfirmNewPassword.Required", - "Account.PasswordRecovery.Email.Required", - "News.Comments.CommentTitle.Required", - "News.Comments.CommentTitle.MaxLengthValidation", - "News.Comments.CommentText.Required", - "PrivateMessages.SubjectCannotBeEmpty", - "PrivateMessages.MessageCannotBeEmpty", - "Wishlist.EmailAFriend.FriendEmail.Required", - "Wishlist.EmailAFriend.YourEmailAddress.Required" - ); - - // remove duplicate resources for display order - builder.Delete( - "Admin.Catalog.Categories.Fields.DisplayOrder", - "Admin.Catalog.Categories.Products.Fields.DisplayOrder", - "Admin.Catalog.Manufacturers.Fields.DisplayOrder", - "Admin.Catalog.Manufacturers.Products.Fields.DisplayOrder", - "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.DisplayOrder", - "Admin.Catalog.Products.BundleItems.Fields.DisplayOrder", - "Admin.Catalog.Products.Fields.HomePageDisplayOrder", - "Admin.Catalog.Products.SpecificationAttributes.Fields.DisplayOrder", - "Admin.Catalog.Products.Pictures.Fields.DisplayOrder", - "Admin.Catalog.Products.Categories.Fields.DisplayOrder", - "Admin.Catalog.Products.Manufacturers.Fields.DisplayOrder", - "Admin.Catalog.Products.RelatedProducts.Fields.DisplayOrder", - "Admin.Catalog.Products.AssociatedProducts.Fields.DisplayOrder", - "Admin.Catalog.Products.BundleItems.Fields.DisplayOrder", - "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Fields.DisplayOrder", - "Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.DisplayOrder", - "Admin.Catalog.Products.SpecificationAttributes.Fields.DisplayOrder", - "Admin.Catalog.Attributes.SpecificationAttributes.Fields.DisplayOrder", - "Admin.Catalog.Attributes.SpecificationAttributes.Options.Fields.DisplayOrder", - "Admin.Catalog.Categories.Fields.DisplayOrder", - "Admin.Catalog.Manufacturers.Fields.DisplayOrder", - "Admin.Configuration.Countries.Fields.DisplayOrder", - "Admin.Configuration.Currencies.Fields.DisplayOrder", - "Admin.Configuration.DeliveryTimes.Fields.DisplayOrder", - "Admin.Configuration.Measures.Dimensions.Fields.DisplayOrder", - "Admin.Configuration.Measures.Weights.Fields.DisplayOrder", - "Admin.Configuration.Countries.States.Fields.DisplayOrder", - "Admin.ContentManagement.Forums.ForumGroup.Fields.DisplayOrder", - "Admin.ContentManagement.Forums.Forum.Fields.DisplayOrder", - "Admin.Configuration.Languages.Fields.DisplayOrder", - "Admin.Catalog.Attributes.CheckoutAttributes.Fields.DisplayOrder", - "Admin.Catalog.Attributes.CheckoutAttributes.Values.Fields.DisplayOrder", - "Admin.Configuration.Plugins.Fields.DisplayOrder", - "Admin.ContentManagement.Polls.Answers.Fields.DisplayOrder", - "Admin.ContentManagement.Polls.Fields.DisplayOrder", - "Admin.Configuration.Shipping.Methods.Fields.DisplayOrder", - "Admin.Configuration.Stores.Fields.DisplayOrder", - "Admin.Configuration.Tax.Categories.Fields.DisplayOrder" - ); - - builder.AddOrUpdate("Common.DisplayOrder.Hint", - "Specifies display order. 1 represents the top of the list.", - "Legt die Anzeige-Priorität fest. 1 steht bspw. für das erste Element in der Liste."); - - builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.UseInvisibleReCaptcha", - "Use invisible reCAPTCHA", - "Unsichtbaren reCAPTCHA verwenden", - "Does not require the user to click on a checkbox, instead it is invoked directly when the user submits a form. By default only the most suspicious traffic will be prompted to solve a captcha.", - "Der Benutzer muss nicht auf ein Kontrollkästchen klicken, sondern die Validierung erfolgt direkt beim Absenden eines Formulars. Nur bei 'verdächtigem' Traffic wird der Benutzer aufgefordert, ein Captcha zu lösen."); - - builder.AddOrUpdate("Admin.ContentManagement.Topics.Fields.ShortTitle", - "Short title", - "Kurztitel", - "Optional. Used as link text. If empty, 'Title' sets the link text.", - "Optional. Wird u.A. als Linktext verwendet. Wenn leer, stellt 'Titel' den Linktext."); - - builder.AddOrUpdate("Admin.ContentManagement.Topics.Fields.Intro", - "Intro", - "Intro", - "Optional. Short introduction / teaser.", - "Optional. Einleitung / Teaser."); - - builder.AddOrUpdate("Common.Download.Versions", "Versions", "Versionen"); - builder.AddOrUpdate("Common.Download.Version", "Version", "Version"); - builder.AddOrUpdate("Common.Download.Delete", "Delete download", "Download löschen"); - builder.AddOrUpdate("Common.Downloads", "Downloads", "Downloads"); - - builder.AddOrUpdate("Admin.Catalog.Products.Fields.NewVersionDownloadId", - "New download version", - "Neue Version des Downloads", - "Upload a new version of the download file here.", - "Laden Sie hier eine neue Version der Download-Datei hoch."); - - builder.AddOrUpdate("Admin.Catalog.Products.Download.VersionDelete", "Delete this file version.", "Diese Dateiversion löschen."); - builder.AddOrUpdate("Admin.Catalog.Products.Download.AddChangelog", "Edit changelog", "Änderungshistorie bearbeiten"); - builder.AddOrUpdate("Customer.Downloads.NoChangelogAvailable", "No changelog available.", "Keine Änderungshistorie verfügbar."); - - builder.AddOrUpdate("Admin.Catalog.Products.Download.SemanticVersion.NotValid", - "The specified version information is not valid. Please enter the version number in the correct format (e.g.: 1.0.0.0, 2.0 or 3.1.5).", - "Die angegebenen Versionsinformationen sind nicht gültig. Bitte geben Sie die Versionsnummer in korrektem Format an (z.B.: 1.0.0.0, 2.0 oder 3.1.5)."); - - builder.AddOrUpdate("Admin.Catalog.Products.Fields.HasPreviewPicture", - "Exclude first image from gallery", - "Erstes Bild aus Gallerie ausschließen", - "Activate this option if the first image should be displayed as a preview in product lists but not in the product detail gallery.", - "Aktivieren Sie diese Option, wenn das erste Bild als Vorschau in Produktlisten, nicht aber in der Produktdetail-Gallerie angezeigt werden soll."); - - builder.AddOrUpdate("Products.Free", "Free", "Kostenlos"); - - builder.AddOrUpdate("Admin.Catalog.Products.Fields.ProductTags.Hint", - "Product tags are keywords that this product can also be identified by. Enter a list of the tags to be associated with this product. The more products associated with a particular tag, the larger it will show on the tag cloud.", - "Eine Liste von Schlüsselwörtern, die das Produkt taxonomisch charakterisieren. Je mehr Produkte einem Schlüsselwort (Tag) zugeordnet sind, desto mehr visuelles Gewicht erhält das Tag."); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.Initial", "Position", "Position"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.Relevance", "Relevance", "Beste Ergebnisse"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.SubjectAsc", "Title: A to Z", "Titel: A bis Z"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.SubjectDesc", "Title: Z to A", "Titel: Z bis A"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.UserNameAsc", "User name: A to Z", "Benutzername: A bis Z"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.UserNameDesc", "User name: Z to A", "Benutzername: Z bis A"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.CreatedOnAsc", "Created on: Oldest first", "Erstellt am: ältere zuerst"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.CreatedOnDesc", "Created on: Newest first", "Erstellt am: neuere zuerst"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.PostsAsc", "Post number: ascending", "Anzahl Beiträge: aufsteigend"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumTopicSorting.PostsDesc", "Post number: descending", "Anzahl Beiträge: absteigend"); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastVisit", "Since last visit", "Seit dem letzten Besuch"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.Yesterday", "Yesterday", "Gestern"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastWeek", "Last week", "In der letzten Woche"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastTwoWeeks", "Last 2 weeks", "In den letzten 2 Wochen"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastMonth", "Last month", "Im letzten Monat"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastThreeMonths", "Last 3 months", "In den letzten 3 Monaten"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastSixMonths", "Last 6 months", "In den letzten 6 Monaten"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Forums.ForumDateFilter.LastYear", "Last year", "Im letzten Jahr"); - - builder.AddOrUpdate("Search.Facet.Forum", "Forum", "Forum"); - builder.AddOrUpdate("Search.Facet.Customer", "User name", "Benutzername"); - builder.AddOrUpdate("Search.Facet.Date", "Period", "Zeitraum"); - builder.AddOrUpdate("Search.Facet.Date.Newer", "and newer", "und neuer"); - builder.AddOrUpdate("Search.Facet.Date.Older", "and older", "und älter"); - - builder.AddOrUpdate("Forum.PostText", "Post text", "Beitragstext"); - builder.AddOrUpdate("Forum.Sticky", "Sticky topic", "Festes Thema"); - - builder.AddOrUpdate("Search.HitsFor", "{0} hits for {1}", "{0} Treffer für {1}"); - builder.AddOrUpdate("Search.NoMoreHitsFound", "There were no more hits found.", "Es wurden keine weiteren Treffer gefunden."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Search.WildcardSearchNote", - "The wildcard mode can slow down the search for a large number of objects.", - "Der Wildcard-Modus kann bei einer großen Anzahl an Objekten die Suche verlangsamen."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Search.SearchMode", - "Search mode", - "Suchmodus", - "Specifies the search mode. Please keep in mind that the search mode can - depending on number of objects - strongly affect search performance. 'Is equal to' is the fastest, 'Contains' the slowest.", - "Legt den Suchmodus fest. Bitte beachten Sie, dass der Suchmodus die Geschwindigkeit der Suche (abhängig von der Objektanzahl) beeinflusst. 'Ist gleich' ist am schnellsten, 'Beinhaltet' am langsamsten."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Search.Forum.SearchFields", - "Search fields", - "Suchfelder", - "Specifies additional search fields. The topic title is always searched.", - "Legt zusätzlich zu durchsuchende Felder fest. Der Thementitel wird grundsätzlich immer durchsucht."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Search.DefaultSortOrder", - "Default sort order", - "Standardsortierreihenfolge", - "Specifies the default sort order in search results.", - "Legt die Standardsortierreihenfolge in den Suchergebnissen fest."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Search.InstantSearchNumberOfHits", - "Number of hits", - "Anzahl der Treffer", - "Specifies the number of hits displayed in instant search.", - "Legt die Anzahl der angezeigten Suchtreffer in der Instantsuche fest."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Forums.AllowSorting", - "Allow sorting", - "Sortierung zulassen", - "Specifies whether forum posts can be sorted.", - "Legt fest, ob Forenbeiträge sortiert werden können."); - - builder.AddOrUpdate("Admin.Common.DefaultPageSizeOptions", - "Page size options", - "Auswahlmöglichkeiten für Seitengröße", - "Comma-separated page size options that a customer can select in lists.", - "Kommagetrennte Liste mit Optionen für Seitengröße, die ein Kunde in Listen wählen kann."); - - builder.AddOrUpdate("Admin.Common.AllowCustomersToSelectPageSize", - "Allow customers to select page size", - "Kunde kann Listengröße ändern", - "Whether customers are allowed to select the page size from a predefined list of options.", - "Kunden können die Listengröße mit Hilfe einer vorgegebenen Optionsliste ändern."); - - - builder.Delete( - "Admin.Configuration.Settings.Search.DefaultSortOrderMode", - "Admin.Configuration.Settings.Search.InstantSearchNumberOfProducts", - "Admin.Configuration.Settings.CustomerUser.DefaultAvatarEnabled", - "Forum.Search.LimitResultsToPrevious.AllResults", - "Forum.Search.LimitResultsToPrevious.1day", - "Forum.Search.LimitResultsToPrevious.7days", - "Forum.Search.LimitResultsToPrevious.2weeks", - "Forum.Search.LimitResultsToPrevious.1month", - "Forum.Search.LimitResultsToPrevious.3months", - "Forum.Search.LimitResultsToPrevious.6months", - "Forum.Search.LimitResultsToPrevious.1year", - "Forum.Search.SearchInForum.All", - "Forum.Search.SearchWithin.All", - "Forum.Search.SearchWithin.TopicTitlesOnly", - "Forum.Search.SearchWithin.PostTextOnly", - "Forum.SearchTermMinimumLengthIsNCharacters", - "Enums.SmartStore.Core.Domain.Forums.ForumSearchType.All", - "Enums.SmartStore.Core.Domain.Forums.ForumSearchType.PostTextOnly", - "Enums.SmartStore.Core.Domain.Forums.ForumSearchType.TopicTitlesOnly", - "Forum.AdvancedSearch", - "Forum.SearchButton", - "Forum.PageTitle.Search"); - - builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.PriceDisplayStyle", - "Price display style", - "Preisdarstellung", - "Specifies the form in which prices are displayed in product lists and on the product detail page.", - "Bestimmt die Darstellungform von Preisen in Produktlisten und auf der Produktdetailseite."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.DisplayTextForZeroPrices", - "Display text when prices are 0,00", - "Zeige Text wenn Preise 0,00 sind", - "Specifies whether to display a textual resource (free) instead of the value 0.00.", - "Bestimmt, ob statt dem Wert 0,00 eine textuelle Resource (kostenlos) angezeigt werden soll."); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayStyle.Default", "Default", "Standard"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayStyle.BadgeAll", "In bagdes", "Markiert"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayStyle.BadgeFreeProductsOnly", "Badge free products only", "Nur kostenlose Produkte markieren"); - - builder.AddOrUpdate("Admin.DataExchange.Export.Filter.WorkingLanguageId", - "Language", - "Sprache", - "Filter by language", - "Nach Sprache filtern"); - - - builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.CaptchaShowOnForumPage", - "Show on forum pages", - "Auf Forenseiten anzeigen", - "Specifies whether to display a CAPTCHA on forum pages when creating or replying to a topic.", - "Legt fest, ob ein CAPTCHA auf Forenseiten angezeigt werden soll, wenn ein Thema erstellt oder darauf geantwortet wird."); - - builder.AddOrUpdate("Admin.Catalog.Products.BundleItems.NoProductLinkageForBundleItem", - "The product \"{0}\" cannot be assigned an attribute of the type \"product\" because it is bundle item of a product bundle.", - "Dem Produkt \"{0}\" kann kein Attribut vom Typ \"Produkt\" zugeordnet werden, weil es auf der Stückliste eines Produkt-Bundle steht."); - - builder.AddOrUpdate("Search.RelatedSearchTerms", - "Related search terms", - "Verwandte Suchbegriffe"); - - builder.AddOrUpdate("Plugins.CannotLoadModule", - "The plugin or provider \"{0}\" cannot be loaded.", - "Das Plugin oder der Provider \"{0}\" kann nicht geladen werden."); - - builder.AddOrUpdate("Admin.System.ScheduleTasks.RunPerMachine", - "Run per machine", - "Pro Maschine ausführen", - "Indicates whether the task is executed decidedly on each machine of a web farm.", - "Gibt an, ob die Aufgabe auf jeder Maschine einer Webfarm dezidiert ausgeführt wird."); - - builder.Delete("Address.Fields.Required.Hint"); - - builder.AddOrUpdate("Common.FormFields.Required.Hint", - "* Input elements with asterisk are required and have to be filled out.", - "* Eingabefelder mit Sternchen sind Pflichfelder und müssen ausgefüllt werden."); - - builder.AddOrUpdate("Forum.Post.Vote.OnlyRegistered", - "Only registered users can vote for posts.", - "Nur registrierte Benutzer können Beiträge bewerten."); - - builder.AddOrUpdate("Forum.Post.Vote.OwnPostNotAllowed", - "You cannot vote for your own post.", - "Sie können nicht Ihren eigenen Beitrag bewerten."); - - builder.AddOrUpdate("Forum.Post.Vote.SuccessfullyVoted", - "Thank you for your vote.", - "Danke für Ihre Bewertung."); - - builder.AddOrUpdate("Common.Liked", "Liked", "Gefällt"); - builder.AddOrUpdate("Common.LikeIt", "I like it", "Gefällt mir"); - builder.AddOrUpdate("Common.DoNotLikeIt", "I do not like it anymore", "Gefällt mir nicht mehr"); - - builder.AddOrUpdate("Admin.Configuration.Settings.Forums.AllowCustomersToVoteOnPosts", - "Allow customers to vote on posts", - "Benutzer können Beiträge bewerten", - "Specifies whether customers can vote on posts.", - "Legt fest, ob Benutzer Beiträge bewerten können."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Forums.AllowGuestsToVoteOnPosts", - "Allow guests to vote on posts", - "Gäste können Beiträge bewerten", - "Specifies whether guests can vote on posts.", - "Legt fest, ob Gäste Beiträge bewerten können."); - - // Typos. - builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements") - .Value("de", "Voraussetzungen"); - builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements.DiscountRequirementType") - .Value("de", "Typ der Voraussetzung"); - builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements.DiscountRequirementType.Hint") - .Value("de", "Voraussetzungen für den Rabatt"); - builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements.Remove") - .Value("de", "Voraussetzung für den Rabatt entfernen"); - builder.AddOrUpdate("Admin.Promotions.Discounts.Requirements.SaveBeforeEdit") - .Value("de", "Sie müssen den Rabatt zunächst speichern, bevor Sie Voraussetzungen für seine Anwendung festlegen können"); - - builder.AddOrUpdate("Common.Voting", "Voting", "Abstimmung"); - builder.AddOrUpdate("Common.Answer", "Answer", "Antwort"); - builder.AddOrUpdate("Common.Size", "Size", "Größe"); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerFormFields.Description", - "Manage form fields that are displayed during registration.", - "Verwalten Sie Formularfelder, die während der Registrierung angezeigt werden."); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields.Description", - "Manage form fields that are displayed during checkout and on \"My account\" page.", - "Verwalten Sie Formularfelder, die während des Checkout-Prozesses und im \"Mein Konto\" Bereich angezeigt werden."); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.RelatedEntityType.TierPrice", "Tier price", "Staffelpreis"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.RelatedEntityType.ProductVariantAttributeValue", "Attribute option", "Attribut-Option"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.RelatedEntityType.ProductVariantAttributeCombination", "Attribute combination", "Attribut-Kombination"); - - builder.AddOrUpdate("Admin.DataExchange.Export.ExportRelatedData.Validate", - "Related data cannot be exported if the option \"Export attribute combinations\" is activated.", - "Zugehörige Daten können nicht exportiert werden, wenn die Option \"Attributkombinationen exportieren\" aktiviert ist."); - - builder.AddOrUpdate("Admin.Common.ProcessingInfo", - "{0}: {1} of {2} processed", - "{0}: {1} von {2} verarbeitet"); - - builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowSubCategoriesInSubPages", - "Show subcategories also in subpages", - "Unterwarengruppen auch in Unterseiten anzeigen", - "Subpage: List index greater than 1 or any active filter.", - "Unterseite: Listenindex größer 1 oder mind. ein aktiver Filter."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowDescriptionInSubPages", - "Show page description also in subpages", - "Seitenbeschreibungen auch in Unterseiten anzeigen", - "Subpage: List index greater than 1 or any active filter.", - "Unterseite: Listenindex größer 1 oder mind. ein aktiver Filter."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.IncludeFeaturedProductsInSubPages", - "Show featured products also in subpages", - "Top-Produkte auch in Unterseiten anzeigen", - "Subpage: List index greater than 1 or any active filter.", - "Unterseite: Listenindex größer 1 oder mind. ein aktiver Filter."); - - builder.AddOrUpdate("Admin.Common.CopyOf", "Copy of {0}", "Kopie von {0}"); - - builder.AddOrUpdate("Admin.Configuration.Languages.DefaultLanguage.Note", - "The default language of the shop is {0}. The default is always the first published language.", - "Die Standardsprache des Shops ist {0}. Standard ist stets die erste veröffentlichte Sprache."); - - 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 verfügbaren Ressourcen."); - - builder.AddOrUpdate("Common.BrowseFiles", "Browse", "Durchsuchen"); - builder.AddOrUpdate("Common.Url", "URL", "URL"); - builder.AddOrUpdate("Common.File", "File", "Datei"); - builder.AddOrUpdate("Common.Entity.Product", "Product", "Produkt"); - builder.AddOrUpdate("Common.Entity.Category", "Category", "Warengruppe"); - builder.AddOrUpdate("Common.Entity.Manufacturer", "Manufacturer", "Hersteller"); - builder.AddOrUpdate("Common.Entity.Topic", "Topic", "Seite"); - - builder.AddOrUpdate("Common.Entity.SelectProduct", "Select product", "Produkt auswählen"); - builder.AddOrUpdate("Common.Entity.SelectCategory", "Select category", "Warengruppe auswählen"); - builder.AddOrUpdate("Common.Entity.SelectManufacturer", "Select manufacturer", "Hersteller auswählen"); - builder.AddOrUpdate("Common.Entity.SelectTopic", "Select topic", "Seite auswählen"); - - builder.Delete("Admin.Customers.Customers.List.SearchDeletedOnly"); - builder.AddOrUpdate("Admin.Customers.Customers.List.SearchActiveOnly", "Only activated customers", "Nur aktivierte Kunden"); - - builder.AddOrUpdate("Products.LoginForPrice", - "Prices will be displayed after login.", - "Preise werden nach Anmeldung angezeigt."); - - builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowLoginForPriceNote", - "Show login for price note", - "Hinweis \"Preis nach Anmeldung\" anzeigen", - "Specifies whether to display a message stating that prices will not be displayed until login.", - "Legt fest, ob ein Hinweis erscheinen soll, dass Preise erst nach Anmeldung angezeigt werden."); - - builder.AddOrUpdate("Products.EmailAFriend.LoginNote", - "Please log in to use this function. Login now", - "Bitte melden Sie sich an, um diese Funktion nutzen zu können. Jetzt anmelden"); - - builder.AddOrUpdate("Account.Login.Fields.UsernameOrEmail", - "Username or email", - "Benutzername oder Email"); - - builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerLoginType", - "Customer login type", - "Art des Kundenlogins", - "Specifies the customer login type.", - "Legt die Art des Kundenlogins fest."); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerLoginType.Username", "Username", "Benutzername"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerLoginType.Email", "Email", "Email"); - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerLoginType.UsernameOrEmail", "Username or email", "Benutzername oder Email"); - - builder.AddOrUpdate("Admin.ContentManagement.Topics.Fields.HtmlId", - "Html Id", - "Html-ID", - "Specifies the Html Id of the page.", - "Legt die Html-ID der Seite fest."); - - builder.AddOrUpdate("Admin.ContentManagement.Topics.Fields.BodyCssClass", - "Body CSS class", - "Body CSS-Klasse", - "Specifies the CSS class of the body element.", - "Legt die CSS-Klasse des Body-Elements fest."); - - builder.AddOrUpdate("Admin.ContentManagement.Topics.Validation.NoWhiteSpace", - "Whitespace isn't allowed.", - "Leerzeichen sind nicht erlaubt."); - builder.AddOrUpdate("Admin.Configuration.Settings.Tax.VatRequired", - "Customers must enter a VAT number", - "Kunden müssen eine Steuernummer angeben", - "Specifies whether customers must enter a VAT identification number.", - "Legt fest, ob Kunden bei der Registrierung eine Steuernummer angeben müssen."); } } } diff --git a/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs b/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs index 57833b3f28..3faed3ad16 100644 --- a/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs +++ b/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Data.Entity.Validation; using System.Linq; @@ -9,9 +8,7 @@ using System.Threading; using System.Threading.Tasks; using SmartStore.Core; -using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; -using SmartStore.Core.Infrastructure; using SmartStore.Utilities; using EfState = System.Data.Entity.EntityState; @@ -197,55 +194,7 @@ public SaveStage Stage get { return _stage; } } - public int Execute() - { - var autoDetectChanges = _ctx.Configuration.AutoDetectChangesEnabled; - IEnumerable mergeableEntities = null; - - Action endExecute = () => - { - _ctx.Configuration.AutoDetectChangesEnabled = autoDetectChanges; - _ctx.IgnoreMergedData(mergeableEntities, false); - }; - - using (new ActionDisposable(endExecute)) - { - // 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 - IEnumerable changedHookEntries; - PreExecute(out changedHookEntries); - - // save - var result = _ctx.SaveChangesCore(); - - // post - PostExecute(changedHookEntries); - - return result; - } - } - - public Task ExecuteAsync(CancellationToken cancellationToken) + private IDisposable ExecuteCore() { var autoDetectChanges = _ctx.Configuration.AutoDetectChangesEnabled; IEnumerable mergeableEntities = null; @@ -265,32 +214,46 @@ public Task ExecuteAsync(CancellationToken cancellationToken) // We must detect changes earlier in the process // before hooks are executed. Therefore we suppressed the - // implicit DetectChanges() calls by EF and call it here explicitly. + // implicit DetectChanges() call by EF and call it here explicitly. _ctx.ChangeTracker.DetectChanges(); // Now get changed entries _changedEntries = _ctx.GetChangedEntries(); // pre - IEnumerable changedHookEntries; - PreExecute(out changedHookEntries); + PreExecute(out IEnumerable changedHookEntries); - // save - var result = _ctx.SaveChangesCoreAsync(cancellationToken); + return new ActionDisposable(endExecute); - // post - result.ContinueWith((t) => + void endExecute() { - if (!t.IsFaulted) + try { + // post PostExecute(changedHookEntries); } + finally + { + _ctx.Configuration.AutoDetectChangesEnabled = autoDetectChanges; + _ctx.IgnoreMergedData(mergeableEntities, false); + } + } + } - _ctx.Configuration.AutoDetectChangesEnabled = autoDetectChanges; - _ctx.IgnoreMergedData(mergeableEntities, false); - }); + public int Execute() + { + using (ExecuteCore()) + { + return _ctx.SaveChangesCore(); + } + } - return result; + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + using (ExecuteCore()) + { + return await _ctx.SaveChangesCoreAsync(cancellationToken); + } } private IEnumerable PreExecute(out IEnumerable changedHookEntries) diff --git a/src/Libraries/SmartStore.Data/ObjectContextBase.cs b/src/Libraries/SmartStore.Data/ObjectContextBase.cs index 0e7c9c73bf..ea5675eabb 100644 --- a/src/Libraries/SmartStore.Data/ObjectContextBase.cs +++ b/src/Libraries/SmartStore.Data/ObjectContextBase.cs @@ -168,13 +168,6 @@ public IEnumerable SqlQuery(string sql, params object[] para return this.Database.SqlQuery(sql, parameters); } - /// - /// Executes the given DDL/DML command against the database. - /// - /// The command string - /// Timeout value, in seconds. A null value indicates that the default value of the underlying provider will be used - /// The parameters to apply to the command string. - /// The result returned by the database after executing the command. public int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, int? timeout = null, params object[] parameters) { Guard.NotEmpty(sql, "sql"); diff --git a/src/Libraries/SmartStore.Data/Properties/AssemblyInfo.cs b/src/Libraries/SmartStore.Data/Properties/AssemblyInfo.cs index 11132fd2a1..31d6040e23 100644 --- a/src/Libraries/SmartStore.Data/Properties/AssemblyInfo.cs +++ b/src/Libraries/SmartStore.Data/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.CompilerServices; + [assembly: AssemblyTitle("SmartStore.Data")] [assembly: InternalsVisibleTo("SmartStore.Web")] [assembly: InternalsVisibleTo("SmartStore.Data.Tests")] diff --git a/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs b/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs index c92790df60..7042551b5c 100644 --- a/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs +++ b/src/Libraries/SmartStore.Data/Setup/MigrateDatabaseInitializer.cs @@ -5,6 +5,7 @@ using System.Data.Entity.Migrations; using System.Linq; using SmartStore.Core.Data; +using SmartStore.Data.Caching; using SmartStore.Data.Migrations; using SmartStore.Utilities; @@ -87,27 +88,31 @@ public virtual void InitializeDatabase(TContext context) } } - // run all pending migrations - var appliedCount = migrator.RunPendingMigrations(context); - - if (appliedCount > 0) - { - Seed(context); - } - else + using (new DbContextScope(context as IDbContext, hooksEnabled: false)) { - var coreConfig = config as MigrationsConfiguration; - if (coreConfig != null && context is SmartObjectContext) + // run all pending migrations + var appliedCount = migrator.RunPendingMigrations(context); + + if (appliedCount > 0) + { + Seed(context); + } + else { // DB is up-to-date and no migration ran. - // Call the main Seed method anyway (on every startup), - // we could have locale resources or settings to add/update. - coreConfig.SeedDatabase(context as SmartObjectContext); + 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; + // not needed anymore + this.DataSeeders = null; + } } #endregion diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs index a7947fd324..25ec8fc33c 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs @@ -202,42 +202,42 @@ public IList MeasureWeights() protected virtual string TaxNameBooks { - get { return "Books"; } + get => "Books"; } protected virtual string TaxNameDigitalGoods { - get { return "Downloadable Products"; } + get => "Downloadable Products"; } protected virtual string TaxNameJewelry { - get { return "Jewelry"; } + get => "Jewelry"; } protected virtual string TaxNameApparel { - get { return "Apparel & Shoes"; } + get => "Apparel & Shoes"; } protected virtual string TaxNameFood { - get { return "Food"; } + get => "Food"; } protected virtual string TaxNameElectronics { - get { return "Electronics & Software"; } + get => "Electronics & Software"; } protected virtual string TaxNameTaxFree { - get { return "Tax free"; } + get => "Tax free"; } public virtual decimal[] FixedTaxRates { - get { return new decimal[] { 0, 0, 0, 0, 0 }; } + get => new decimal[] { 0, 0, 0, 0, 0 }; } public IList TaxCategories() @@ -3873,6 +3873,8 @@ public IList Topics() 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
" }, @@ -3889,7 +3891,9 @@ public IList Topics() SystemName = "ContactUs", IncludeInSitemap = false, IsPasswordProtected = false, - Title = "Contact us", + RenderAsWidget = true, + WidgetWrapContent = false, + Title = "Contact us", Body = "

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

" }, new Topic @@ -3897,7 +3901,9 @@ public IList Topics() SystemName = "ForumWelcomeMessage", IncludeInSitemap = false, IsPasswordProtected = false, - Title = "Forums", + RenderAsWidget = true, + WidgetWrapContent = false, + Title = "Forums", Body = "

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

" }, new Topic @@ -3905,7 +3911,9 @@ public IList Topics() SystemName = "HomePageText", IncludeInSitemap = false, IsPasswordProtected = false, - Title = "Welcome to our store", + 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 @@ -3913,7 +3921,9 @@ public IList Topics() SystemName = "LoginRegistrationInfo", IncludeInSitemap = false, IsPasswordProtected = false, - Title = "About login / registration", + 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 @@ -3965,6 +3975,11 @@ public IList Topics() 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 @@ -3973,60 +3988,60 @@ public IList Settings() new CommonSettings { }, - new SeoSettings() + new SeoSettings { }, - new SocialSettings() + new SocialSettings { }, - new AdminAreaSettings() + new AdminAreaSettings { }, - new CatalogSettings() + new CatalogSettings { }, - new LocalizationSettings() + new LocalizationSettings { - DefaultAdminLanguageId = _ctx.Set().First().Id + DefaultAdminLanguageId = defaultLanguageId }, - new CustomerSettings() + new CustomerSettings { }, - new AddressSettings() + new AddressSettings { }, - new MediaSettings() + new MediaSettings { }, - new StoreInformationSettings() + new StoreInformationSettings { }, - new RewardPointsSettings() + new RewardPointsSettings { }, - new CurrencySettings() + new CurrencySettings { }, - new MeasureSettings() + new MeasureSettings { - BaseDimensionId = _ctx.Set().Where(m => m.SystemKeyword == "inch").Single().Id, - BaseWeightId = _ctx.Set().Where(m => m.SystemKeyword == "lb").Single().Id, + BaseDimensionId = defaultDimensionId, + BaseWeightId = defaultWeightId, }, - new ShoppingCartSettings() + new ShoppingCartSettings { }, - new OrderSettings() + new OrderSettings { }, - new SecuritySettings() + new SecuritySettings { }, - new ShippingSettings() + new ShippingSettings { }, - new PaymentSettings() + new PaymentSettings { - ActivePaymentMethodSystemNames = new List() + ActivePaymentMethodSystemNames = new List { "Payments.CashOnDelivery", "Payments.Manual", @@ -4034,23 +4049,23 @@ public IList Settings() "Payments.Prepayment" } }, - new TaxSettings() + new TaxSettings { }, - new BlogSettings() + new BlogSettings { }, - new NewsSettings() + new NewsSettings { }, - new ForumSettings() + new ForumSettings { }, - new EmailAccountSettings() + new EmailAccountSettings { - DefaultEmailAccountId = _ctx.Set().First().Id + DefaultEmailAccountId = defaultEmailAccountId }, - new ThemeSettings() + new ThemeSettings { } }; @@ -8899,7 +8914,7 @@ public IList CategoriesFirstLevel() Name = "Apple", Alias = "Apple", CategoryTemplateId = categoryTemplateInGridAndLines.Id, - Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_apple.png"), "image/jpeg", GetSeName("Apple")), + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_apple.png"), "image/png", GetSeName("Apple")), Published = true, DisplayOrder = 1, MetaTitle = "Apple", @@ -9035,6 +9050,77 @@ public IList CategoriesSecondLevel() #region category definitions + #region new + + var categoryFashionJackets = new Category + { + Name = "Jackets", + Alias = "Jackets", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_jackets.jpg"), "image/png", GetSeName("Jackets")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Jackets", + ShowOnHomePage = true + }; + + var categoryFashionLeatherJackets = new Category + { + Name = "Leather jackets", + Alias = "Leather jackets", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_leather_jackets.jpg"), "image/png", GetSeName("Leather jackets")), + 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, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_shoes.png"), "image/png", GetSeName("Shoes")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Shoes", + ShowOnHomePage = true + }; + + var categoryFashionTrousers = new Category + { + Name = "Trousers", + Alias = "Pants", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_trousers.png"), "image/png", GetSeName("Trousers")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Trousers", + ShowOnHomePage = true + }; + + var categoryFashionTracksuits = new Category + { + Name = "Tracksuits", + Alias = "Tracksuits", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_tracksuit.png"), "image/png", GetSeName("Tracksuits")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Tracksuits", + ShowOnHomePage = true + }; + + #endregion + + + var categorySportsGolf = new Category { Name = "Golf", @@ -9164,7 +9250,8 @@ public IList CategoriesSecondLevel() var entities = new List { categorySportsSunglasses,categorySportsSoccer, categorySportsBasketball,categorySportsGolf, categoryBooksSpiegel, categoryBooksCookAndEnjoy, - categoryGamingAccessories, categoryGamingGames + categoryGamingAccessories, categoryGamingGames, categoryFashionJackets, categoryFashionLeatherJackets, categoryFashionShoes, categoryFashionTrousers, + categoryFashionTracksuits }; this.Alter(entities); @@ -9182,11 +9269,37 @@ public IList Manufacturers() //var categoryTemplateInGridAndLines = // this.CategoryTemplates().Where(pt => pt.Name == "Products in Grid or Lines").FirstOrDefault(); - //categories + // + + #region Jack Wolfskin + + var manufacturerJackWolfskin = new Manufacturer + { + Name = "Jack-Wolfskin", + ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "manufacturer_Jack_Wolfskin.png"), "image/png", GetSeName("Jack Wolfskin")), + Published = true, + DisplayOrder = 1 + }; + + #endregion + + #region Mey & Edlich + var manufacturerMeyAndEdlich = new Manufacturer + { + Name = "Mey-And-Edlich", + ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "manufacturer_MeyAndEdlich.jpg"), "image/png", GetSeName("Mey Edlich")), + Published = true, + DisplayOrder = 1 + }; + + #endregion + #region EA Sports - var manufacturerWarnerHome = new Manufacturer + var manufacturerEASports = new Manufacturer { Name = "EA Sports", ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, @@ -9199,7 +9312,7 @@ public IList Manufacturers() #region Warner Home Video Games - var manufacturerEASports = new Manufacturer + var manufacturerWarnerHome = new Manufacturer { Name = "Warner Home Video Games", ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, @@ -9601,17 +9714,19 @@ public IList Manufacturers() #endregion Ubisoft var entities = new List - { - manufacturerEASports,manufacturerWarnerHome,manufacturerBreitling,manufacturerTissot,manufacturerSeiko, manufacturerTitleist,manufacturerApple,manufacturerSamsung,manufacturerLG,manufacturerTrekStor, manufacturerWesternDigital,manufacturerDell, manufacturerMSI, + { + 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 + manufacturerHP, manufacturerAcer, manufacturerSony, manufacturerUbisoft, manufacturerOakley, manufacturerRayban, manufacturerAdidas, manufacturerWilson, + manufacturerPuma,manufacturerNike, manufacturerMeyAndEdlich, manufacturerJackWolfskin, manufacturerMicrosoft }; this.Alter(entities); return entities; } - private List GetFashionProducts() + private List GetFashionProducts(Dictionary specAttributes) { var result = new List(); var productTemplateSimple = _ctx.Set().First(x => x.ViewPath == "Product"); @@ -9668,7 +9783,7 @@ private List GetFashionProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionCotton + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); result.Add(converseAllStar); @@ -9716,8 +9831,8 @@ private List GetFashionProducts() Price = 29.00M }); - var shirtMeccanicaImages = new string[] { "product_shirt_meccanica_red_1.jpg", "product_shirt_meccanica_red_2.jpg", "product_shirt_meccanica_red_3.jpg", - "product_shirt_meccanica_red_4.jpg", "product_shirt_meccanica_black_1.jpg", "product_shirt_meccanica_black_2.jpg", "product_shirt_meccanica_black_3.jpg" }; + 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) { @@ -9733,8 +9848,8 @@ private List GetFashionProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionCotton - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); result.Add(shirtMeccanica); @@ -9773,8 +9888,8 @@ private List GetFashionProducts() DisplayOrder = 1 }); - var ladiesJacketImages = new string[] { "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", "product_ladies_jacket_silver.jpg", "product_ladies_jacket_black.jpg" }; + 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", }; for (var i = 0; i < ladiesJacketImages.Length; ++i) { @@ -9790,7 +9905,7 @@ private List GetFashionProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 11) + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 11) }); result.Add(ladiesJacket); @@ -9836,30 +9951,23 @@ private List GetFashionProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionCotton - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); result.Add(clarkJeans); - - return result; + return result; } - private List GetFurnitureProducts() + 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"); - var specOptionLeather = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 5); - var specOptionWood = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 13); - var specOptionPlastic = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 3); - var specOptionGlass = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 14); - var specOptionSteel = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 1); - var specOptionAluminium = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 4); - // Le Corbusier LC 6 table - var corbusierTable = new Product + // Le Corbusier LC 6 table + var corbusierTable = new Product { ProductType = ProductType.SimpleProduct, VisibleIndividually = true, @@ -9916,15 +10024,15 @@ private List GetFurnitureProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionSteel - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); corbusierTable.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 2, - SpecificationAttributeOption = specOptionGlass - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) + }); result.Add(corbusierTable); @@ -9984,15 +10092,15 @@ private List GetFurnitureProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionPlastic - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) + }); ballChair.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 2, - SpecificationAttributeOption = specOptionLeather - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) + }); result.Add(ballChair); @@ -10060,21 +10168,21 @@ private List GetFurnitureProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionWood - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 13) + }); loungeChair.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 2, - SpecificationAttributeOption = specOptionLeather - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) + }); loungeChair.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 3, - SpecificationAttributeOption = specOptionAluminium + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 4) }); result.Add(loungeChair); @@ -10132,29 +10240,154 @@ private List GetFurnitureProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionLeather - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) + }); result.Add(cubeChair); return result; } - public IList Products() - { - #region definitions - - var productTemplate = _ctx.Set().First(x => x.ViewPath == "Product"); - var firstDeliveryTime = _ctx.Set().First(sa => sa.DisplayOrder == 0); - var secondDeliveryTime = _ctx.Set().First(sa => sa.DisplayOrder == 1); - var thirdDeliveryTime = _ctx.Set().First(sa => sa.DisplayOrder == 2); + public IList Products() + { var specialPriceEndDate = DateTime.UtcNow.AddMonths(1); - #endregion definitions + var productTemplate = _ctx.Set().First(x => x.ViewPath == "Product"); + var firstDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 0); + var secondDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 1); + var thirdDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 2); + + 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); + + + //#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, + // VisibleIndividually = true, + // 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, + // VisibleIndividually = true, + // 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, + //}); + + + //#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, + VisibleIndividually = true, + 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", "jack-wolfskin-kanuka-point-1"); - #region category golf + 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 - var categoryGolf = _ctx.Set().First(c => c.Alias == "Golf"); + + #region category golf #region product Titleist SM6 Tour Chrome @@ -10172,7 +10405,7 @@ public IList Products() Published = true, MetaTitle = "Titleist SM6 Tour Chrome", Price = 164.95M, - OldPrice= 199.95M, + OldPrice = 199.95M, IsGiftCard = false, ManageInventoryMethod = ManageInventoryMethod.ManageStock, OrderMinimumQuantity = 1, @@ -10186,13 +10419,9 @@ public IList Products() AddProductPicture(productTitleistSM6TourChrome, "product_titleist_sm6_tour_chrome.jpg"); - productTitleistSM6TourChrome.ProductCategories.Add(new ProductCategory { Category = categoryGolf, DisplayOrder = 1 }); + productTitleistSM6TourChrome.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); - productTitleistSM6TourChrome.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Titleist").Single(), - DisplayOrder = 1, - }); + productTitleistSM6TourChrome.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); #endregion product Titleist SM6 Tour Chrome @@ -10225,13 +10454,9 @@ public IList Products() AddProductPicture(productTitleistProV1x, "product_titleist-pro-v1x.jpg"); - productTitleistProV1x.ProductCategories.Add(new ProductCategory { Category = categoryGolf, DisplayOrder = 1 }); + productTitleistProV1x.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); - productTitleistProV1x.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Titleist").Single(), - DisplayOrder = 1, - }); + productTitleistProV1x.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); #endregion product Titleist Pro V1x @@ -10265,13 +10490,9 @@ public IList Products() AddProductPicture(productSupremeGolfball, "product_supremeGolfball_1.jpg", "golfball-1"); AddProductPicture(productSupremeGolfball, "product_supremeGolfball_2.jpg", "golfball-2"); - productSupremeGolfball.ProductCategories.Add(new ProductCategory { Category = categoryGolf, DisplayOrder = 1 }); + productSupremeGolfball.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); - productSupremeGolfball.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Titleist").Single(), - DisplayOrder = 1, - }); + productSupremeGolfball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); #endregion product Supreme Golfball @@ -10304,13 +10525,9 @@ public IList Products() AddProductPicture(productGBBEpicSubZeroDriver, "product_gbb-epic-sub-zero-driver.jpg"); - productGBBEpicSubZeroDriver.ProductCategories.Add(new ProductCategory { Category = categoryGolf, DisplayOrder = 1 }); + productGBBEpicSubZeroDriver.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); - productGBBEpicSubZeroDriver.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Titleist").Single(), - DisplayOrder = 1, - }); + productGBBEpicSubZeroDriver.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); #endregion product GBB Epic Sub Zero Driver @@ -10318,8 +10535,6 @@ public IList Products() #region category Soccer - var categorySoccer = _ctx.Set().First(c => c.Alias == "Soccer"); - #region product Nike Strike Football var productNikeStrikeFootball = new Product @@ -10329,7 +10544,7 @@ public IList Products() Name = "Nike Strike Football", IsEsd = false, ShortDescription = "GREAT TOUCH. HIGH VISIBILITY.", - FullDescription = "

Enhance play everyday, with the Nike Strike Football.

Reinforced rubber retains its shape for confident and consistent control. A stand out Visual Power graphic in black, green and orange is best for ball tracking, despite dark or inclement conditions.

  • Visual Power graphic helps give a true read on flight trajectory.
  • Textured casing offers superior touch.
  • Reinforced rubber bladder supports air and shape retention.
  • 66% rubber/ 15% polyurethane/ 13% polyester/ 7% EVA.
", + FullDescription = "

Enhance play everyday, with the Nike Strike Football.

Reinforced rubber retains its shape for confident and consistent control. A stand out Visual Power graphic in black, green and orange is best for ball tracking, despite dark or inclement conditions.

  • Visual Power graphic helps give a true read on flight trajectory.
  • Textured casing offers superior touch.
  • Reinforced rubber bladder supports air and shape retention.
  • 66% rubber/ 15% polyurethane/ 13% polyester/ 7% EVA.
", Sku = "P-5004", ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, @@ -10351,13 +10566,9 @@ public IList Products() AddProductPicture(productNikeStrikeFootball, "products_nike-strike-football.jpg"); - productNikeStrikeFootball.ProductCategories.Add(new ProductCategory { Category = categorySoccer, DisplayOrder = 1 }); + productNikeStrikeFootball.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); - productNikeStrikeFootball.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Nike").Single(), - DisplayOrder = 1, - }); + productNikeStrikeFootball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Nike"], DisplayOrder = 1 }); productNikeStrikeFootball.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10365,7 +10576,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Manufacturer -> Nike - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder ==20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 20).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 20) }); productNikeStrikeFootball.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10373,7 +10584,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Material -> rubber - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 12).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 12) }); productNikeStrikeFootball.TierPrices.Add(new TierPrice { Quantity = 6, Price = 26.90M }); @@ -10411,13 +10622,9 @@ public IList Products() AddProductPicture(productNikeEvoPowerBall, "product_nike-vopower-53-trainer-hs-ball.jpg"); - productNikeEvoPowerBall.ProductCategories.Add(new ProductCategory { Category = categorySoccer, DisplayOrder = 1 }); + productNikeEvoPowerBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); - productNikeEvoPowerBall.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Nike").Single(), - DisplayOrder = 1, - }); + productNikeEvoPowerBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Nike"], DisplayOrder = 1 }); productNikeEvoPowerBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10425,7 +10632,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Manufacturer -> Nike - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 20).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 20) }); productNikeEvoPowerBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10433,7 +10640,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); #endregion Evopower 5.3 Trainer HS Ball @@ -10471,13 +10678,9 @@ public IList Products() AddProductPicture(productTorfabrikOfficialGameBall, "product_torfabrik-offizieller-spielball_blue.png", "official-game-ball-blue"); AddProductPicture(productTorfabrikOfficialGameBall, "product_torfabrik-offizieller-spielball_green.png", "official-game-ball-green"); - productTorfabrikOfficialGameBall.ProductCategories.Add(new ProductCategory { Category = categorySoccer, DisplayOrder = 1 }); + productTorfabrikOfficialGameBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); - productTorfabrikOfficialGameBall.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), - DisplayOrder = 1, - }); + productTorfabrikOfficialGameBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); productTorfabrikOfficialGameBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10485,7 +10688,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Manufacturer -> Adidas - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 19).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 19) }); productTorfabrikOfficialGameBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10493,7 +10696,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); #endregion Torfabrik official game ball @@ -10533,13 +10736,9 @@ public IList Products() AddProductPicture(productAdidasTangoSalaBall, "product_adidas-tango-pasadena-ball-brown.jpg", "adidas-tango-pasadena-ball-brown"); AddProductPicture(productAdidasTangoSalaBall, "product_adidas-tango-pasadena-ball-blue.jpg", "adidas-tango-pasadena-ball-blue"); - productAdidasTangoSalaBall.ProductCategories.Add(new ProductCategory { Category = categorySoccer, DisplayOrder = 1 }); + productAdidasTangoSalaBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); - productAdidasTangoSalaBall.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), - DisplayOrder = 1, - }); + productAdidasTangoSalaBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); productAdidasTangoSalaBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10547,7 +10746,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Manufacturer -> Adidas - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 19).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 19) }); productAdidasTangoSalaBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10555,7 +10754,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); #endregion Adidas TANGO SALA BALL @@ -10564,8 +10763,6 @@ public IList Products() #region category Basketball - var categoryBasketball = _ctx.Set().First(c => c.Alias == "Basketball"); - #region Wilson Evolution High School Game Basketball var productEvolutionHighSchoolGameBasketball = new Product @@ -10597,13 +10794,9 @@ public IList Products() AddProductPicture(productEvolutionHighSchoolGameBasketball, "product_evolution-high-school-game-basketball.jpg"); - productEvolutionHighSchoolGameBasketball.ProductCategories.Add(new ProductCategory { Category = categoryBasketball, DisplayOrder = 1 }); + productEvolutionHighSchoolGameBasketball.ProductCategories.Add(new ProductCategory { Category = categories["Basketball"], DisplayOrder = 1 }); - productEvolutionHighSchoolGameBasketball.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), - 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 }); @@ -10640,13 +10833,9 @@ public IList Products() AddProductPicture(productAllCourtBasketball, "product_all-court-basketball.png"); - productAllCourtBasketball.ProductCategories.Add(new ProductCategory { Category = categoryBasketball, DisplayOrder = 1 }); + productAllCourtBasketball.ProductCategories.Add(new ProductCategory { Category = categories["Basketball"], DisplayOrder = 1 }); - productAllCourtBasketball.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), - DisplayOrder = 1, - }); + productAllCourtBasketball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); #endregion All Court Basketball @@ -10654,8 +10843,6 @@ public IList Products() #region category sunglasses - var categorySunglasses = _ctx.Set().First(c => c.Alias == "Sunglasses"); - #region product Top bar var productRayBanTopBar = new Product @@ -10687,13 +10874,9 @@ public IList Products() 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 = categorySunglasses, DisplayOrder = 1 }); + productRayBanTopBar.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - productRayBanTopBar.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Ray-Ban").Single(), - DisplayOrder = 1, - }); + productRayBanTopBar.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ray-Ban"], DisplayOrder = 1 }); #endregion product Top bar @@ -10731,13 +10914,9 @@ public IList Products() AddProductPicture(productOriginalWayfarer, "product_productOriginalWayfarer_5.jpg", "wayfarer-green-classic-havana-black"); AddProductPicture(productOriginalWayfarer, "product_productOriginalWayfarer_6.jpg", "wayfarer-blue-gray-classic-black-3"); - productOriginalWayfarer.ProductCategories.Add(new ProductCategory { Category = categorySunglasses, DisplayOrder = 1 }); + productOriginalWayfarer.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - productOriginalWayfarer.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Ray-Ban").Single(), - DisplayOrder = 1, - }); + productOriginalWayfarer.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ray-Ban"], DisplayOrder = 1 }); #endregion product ORIGINAL WAYFARER AT COLLECTION @@ -10770,13 +10949,9 @@ public IList Products() AddProductPicture(productRadarEVPrizmSportsSunglasses, "product_radar_ev_prizm.jpg"); - productRadarEVPrizmSportsSunglasses.ProductCategories.Add(new ProductCategory { Category = categorySunglasses, DisplayOrder = 1 }); + productRadarEVPrizmSportsSunglasses.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - productRadarEVPrizmSportsSunglasses.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Oakley").Single(), - DisplayOrder = 1, - }); + productRadarEVPrizmSportsSunglasses.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Oakley"], DisplayOrder = 1 }); #endregion product Radar EV Prizm Sports Sunglasses @@ -10855,13 +11030,9 @@ public IList Products() AddProductPicture(productCustomFlakSunglasses, "product_CustomFlak_skyblue_sapphireiridium.jpg", "custom_flak_skyblue_sapphireiridium"); AddProductPicture(productCustomFlakSunglasses, "product_CustomFlak_skyblue_violetiridium.jpg", "custom_flak_skyblue_violetiridium"); - productCustomFlakSunglasses.ProductCategories.Add(new ProductCategory { Category = categorySunglasses, DisplayOrder = 1 }); + productCustomFlakSunglasses.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - productCustomFlakSunglasses.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Oakley").Single(), - DisplayOrder = 1, - }); + productCustomFlakSunglasses.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Oakley"], DisplayOrder = 1 }); #endregion product Custom Flak Sunglasses @@ -10870,8 +11041,6 @@ public IList Products() #region category apple - var categoryApple = _ctx.Set().First(c => c.Alias == "Apple"); - #region product iphone plus var productIphoneplus = new Product @@ -10909,7 +11078,7 @@ public IList Products() AddProductPicture(productIphoneplus, "product_iphone-plus_rose.jpg", "iphone-plus-rose"); AddProductPicture(productIphoneplus, "product_iphone-plus_gold.jpg", "iphone-plus-gold"); - productIphoneplus.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); + productIphoneplus.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10917,7 +11086,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // offer type -> Permanent low price - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute @@ -10926,7 +11095,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 64gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10934,7 +11103,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 128gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10942,7 +11111,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // operating system -> ios - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 5).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() + SpecificationAttributeOption = specAttributes[5].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); #endregion product iphone plus @@ -10978,22 +11147,17 @@ public IList Products() AddProductPicture(productWatchSeries2, "product_watchseries2_1.jpg", "watchseries-1"); AddProductPicture(productWatchSeries2, "product_watchseries2_2.jpg", "watchseries-2"); - productWatchSeries2.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); + productWatchSeries2.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); - productWatchSeries2.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Apple").Single(), - DisplayOrder = 1, - }); + productWatchSeries2.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); - //attributes productWatchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, // offer type -> offer of the day - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); productWatchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute @@ -11002,7 +11166,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 32gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productWatchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute @@ -11011,7 +11175,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // operating system -> ios - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 5).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() + SpecificationAttributeOption = specAttributes[5].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); #endregion product Watch Series 2 @@ -11050,13 +11214,9 @@ public IList Products() AddProductPicture(productAirpods, "products_airpods_gold.jpg", "airpods-gold"); AddProductPicture(productAirpods, "products_airpods_mint.jpg", "airpods-mint"); - productAirpods.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); + productAirpods.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); - productAirpods.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Apple").Single(), - DisplayOrder = 7, - }); + productAirpods.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 7 }); #endregion product Airpods @@ -11097,16 +11257,12 @@ public IList Products() AddProductPicture(productAppleProHipsterBundle, "product_iphoneplus_2.jpg", "bundle-iphoneplus"); AddProductPicture(productAppleProHipsterBundle, "category_apple.png", "bundle-apple"); - productAppleProHipsterBundle.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); + productAppleProHipsterBundle.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); - productAppleProHipsterBundle.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Apple").Single(), - DisplayOrder = 1, - }); + productAppleProHipsterBundle.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); #endregion product Ultimate Apple Pro Hipster Bundle - + #region product 9,7 iPad var product97ipad = new Product @@ -11151,14 +11307,9 @@ public IList Products() AddProductPicture(product97ipad, "product_97-ipad-gold.jpg", "ipad-gold"); AddProductPicture(product97ipad, "product_97-ipad-silver.jpg", "ipad-silver"); - product97ipad.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); - - product97ipad.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Apple").Single(), - DisplayOrder = 1, - }); + product97ipad.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + product97ipad.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11166,16 +11317,16 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // offer type -> promotion - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - + product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 64gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11183,7 +11334,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 128gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11191,7 +11342,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // operating system -> ios - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 5).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() + SpecificationAttributeOption = specAttributes[5].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); #endregion product 9,7 iPad @@ -11200,641 +11351,633 @@ public IList Products() #region category Gift Cards - var categoryGiftCards = _ctx.Set().First(c => c.Alias == "Gift Cards"); - - #region product10GiftCard - - var product10GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - 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 = categoryGiftCards, DisplayOrder = 1 }); - - #endregion product10GiftCard - - #region product25GiftCard - - var product25GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - 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 = categoryGiftCards, DisplayOrder = 1 }); - - #endregion product25GiftCard - - #region product50GiftCard - - var product50GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - 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 = categoryGiftCards, DisplayOrder = 1 }); - - #endregion product50GiftCard - - #region product100GiftCard + #region product10GiftCard - var product100GiftCard = new Product + var product10GiftCard = new Product { ProductType = ProductType.SimpleProduct, VisibleIndividually = true, - Name = "$100 Virtual Gift Card", + Name = "$10 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.", + 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-10033", + Sku = "P-1000", ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, Published = true, - MetaTitle = "$100 Virtual Gift Card", - Price = 100M, + MetaTitle = "$10 Virtual Gift Card", + Price = 10M, IsGiftCard = true, - GiftCardType = GiftCardType.Virtual, ManageInventoryMethod = ManageInventoryMethod.DontManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, - DisplayOrder = 4, + DisplayOrder = 1 }; - AddProductPicture(product100GiftCard, "product_gift_card_100.png"); - product100GiftCard.ProductCategories.Add(new ProductCategory { Category = categoryGiftCards, DisplayOrder = 1 }); + AddProductPicture(product10GiftCard, "product_gift_card_10.png"); + product10GiftCard.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); - #endregion product100GiftCard + #endregion product10GiftCard + + #region product25GiftCard + + var product25GiftCard = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + 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, + VisibleIndividually = true, + 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, + VisibleIndividually = true, + 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 product100GiftCard #endregion category Gift Cards #region category books - var categorySpiegelBestseller = _ctx.Set().First(c => c.Alias == "SPIEGEL-Bestseller"); - var categoryCookAndEnjoy = _ctx.Set().First(c => c.Alias == "Cook and enjoy"); - var categoryBooks = _ctx.Set().First(c => c.Alias == "Books"); + #region productBooksUberMan - #region productBooksUberMan - - var productBooksUberMan = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Überman: The novel", - ShortDescription = "(Hardcover)", - FullDescription = "

From idiots to riches - and back ... Ever since it with my Greek financial advisors were no more delicious cookies to meetings, I should have known something. Was the last cookie it when I bought a Romanian forest funds and leveraged discount certificates on lean hogs - which is sort of a more stringent bet that the price of lean hogs will remain stable, and that's nothing special because it is also available for cattle and cotton and fat pig. Again and again and I joked Kosmas Nikiforos Sarantakos. About all the part-time seer who tremblingly put for fear the euro crisis gold coins under the salami slices of their frozen pizzas And then came the day that revealed to me in almost Sarantakos fraudulent casualness that my plan had not worked out really. 'Why all of a sudden> my plan', 'I heard myself asking yet, but it was in the garage I realized what that really meant minus 211.2 percent in my portfolio report: personal bankruptcy, gutter and Drug Addiction with subsequent loss of the incisors . Not even the study of my friend, I would still be able to finance. The only way out was to me as quickly as secretly again to draw from this unspeakable Greek shit - I had to be Überman! By far the bekloppteste story about 'idiot' Simon Peter! »Tommy Jaud – Deutschlands witzigste Seite.« Alex Dengler, Bild am Sonntag

", + var productBooksUberMan = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Überman: The novel", + ShortDescription = "(Hardcover)", + FullDescription = "

From idiots to riches - and back ... Ever since it with my Greek financial advisors were no more delicious cookies to meetings, I should have known something. Was the last cookie it when I bought a Romanian forest funds and leveraged discount certificates on lean hogs - which is sort of a more stringent bet that the price of lean hogs will remain stable, and that's nothing special because it is also available for cattle and cotton and fat pig. Again and again and I joked Kosmas Nikiforos Sarantakos. About all the part-time seer who tremblingly put for fear the euro crisis gold coins under the salami slices of their frozen pizzas And then came the day that revealed to me in almost Sarantakos fraudulent casualness that my plan had not worked out really. 'Why all of a sudden> my plan', 'I heard myself asking yet, but it was in the garage I realized what that really meant minus 211.2 percent in my portfolio report: personal bankruptcy, gutter and Drug Addiction with subsequent loss of the incisors . Not even the study of my friend, I would still be able to finance. The only way out was to me as quickly as secretly again to draw from this unspeakable Greek shit - I had to be Überman! By far the bekloppteste story about 'idiot' Simon Peter! »Tommy Jaud – Deutschlands witzigste Seite.« Alex Dengler, Bild am Sonntag

", Sku = "P-1003", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Überman: The novel", - Price = 16.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true - }; + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Überman: The novel", + Price = 16.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true + }; AddProductPicture(productBooksUberMan, "0000932_uberman-der-roman.jpeg"); - productBooksUberMan.ProductCategories.Add(new ProductCategory { Category = categorySpiegelBestseller, DisplayOrder = 1 }); + productBooksUberMan.ProductCategories.Add(new ProductCategory { Category = categories["SPIEGEL-Bestseller"], DisplayOrder = 1 }); - productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 7).Single() - }); - productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 7) + }); + productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksUberMan + #endregion productBooksUberMan - #region productBooksGefangeneDesHimmels + #region productBooksGefangeneDesHimmels - var productBooksGefangeneDesHimmels = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "The Prisoner of Heaven: A Novel", - ShortDescription = "(Hardcover)", - FullDescription = "

By Shadow of the Wind and The Angel's Game, the new large-Barcelona novel by Carlos Ruiz Zafón. - Barcelona, Christmas 1957th The bookseller Daniel Sempere and his friend Fermín be drawn again into a great adventure. In the continuation of his international success with Carlos Ruiz Zafón takes the reader on a fascinating journey into his Barcelona. Creepy and fascinating, with incredible suction power and humor, the novel, the story of Fermin, who 'rose from the dead, and the key to the future is.' Fermin's life story linking the threads of The Shadow of the Wind with those from The Angel's Game. A masterful puzzle that keeps the reader around the world in thrall.

Product Hardcover: 416 pages Publisher: S. Fischer Verlag; 1 edition (October 25, 2012) Language: German ISBN-10: 3,100,954,025 ISBN-13: 978-3100954022 Original title: El prisionero del cielo Size and / or weight: 21.4 x 13.6 cm x 4.4

", - ProductTemplateId = productTemplate.Id, + var productBooksGefangeneDesHimmels = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "The Prisoner of Heaven: A Novel", + ShortDescription = "(Hardcover)", + FullDescription = "

By Shadow of the Wind and The Angel's Game, the new large-Barcelona novel by Carlos Ruiz Zafón. - Barcelona, Christmas 1957th The bookseller Daniel Sempere and his friend Fermín be drawn again into a great adventure. In the continuation of his international success with Carlos Ruiz Zafón takes the reader on a fascinating journey into his Barcelona. Creepy and fascinating, with incredible suction power and humor, the novel, the story of Fermin, who 'rose from the dead, and the key to the future is.' Fermin's life story linking the threads of The Shadow of the Wind with those from The Angel's Game. A masterful puzzle that keeps the reader around the world in thrall.

Product Hardcover: 416 pages Publisher: S. Fischer Verlag; 1 edition (October 25, 2012) Language: German ISBN-10: 3,100,954,025 ISBN-13: 978-3100954022 Original title: El prisionero del cielo Size and / or weight: 21.4 x 13.6 cm x 4.4

", + ProductTemplateId = productTemplate.Id, Sku = "P-1004", - AllowCustomerReviews = true, - Published = true, - MetaTitle = "The Prisoner of Heaven: A Novel", - Price = 22.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "The Prisoner of Heaven: A Novel", + Price = 22.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksGefangeneDesHimmels, "0000935_der-gefangene-des-himmels-roman_300.jpeg"); - productBooksGefangeneDesHimmels.ProductCategories.Add(new ProductCategory { Category = categorySpiegelBestseller, DisplayOrder = 1 }); + productBooksGefangeneDesHimmels.ProductCategories.Add(new ProductCategory { Category = categories["SPIEGEL-Bestseller"], DisplayOrder = 1 }); - productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 7).Single() - }); - productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> bound + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 7) + }); + productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksGefangeneDesHimmels + #endregion productBooksGefangeneDesHimmels - #region productBooksBestGrillingRecipes + #region productBooksBestGrillingRecipes - var productBooksBestGrillingRecipes = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Best Grilling Recipes", - ShortDescription = "More Than 100 Regional Favorites Tested and Perfected for the Outdoor Cook (Hardcover)", - FullDescription = "

Take a winding cross-country trip and you'll discover barbecue shacks with offerings like tender-smoky Baltimore pit beef and saucy St. Louis pork steaks. To bring you the best of these hidden gems, along with all the classics, the editors of Cook's Country magazine scoured the country, then tested and perfected their favorites. HEre traditions large and small are brought into the backyard, from Hawaii's rotisserie favorite, the golden-hued Huli Huli Chicken, to fall-off-the-bone Chicago Barbecued Ribs. In Kansas City, they're all about the sauce, and for our saucy Kansas City Sticky Ribs, we found a surprise ingredient-root beer. We also tackle all the best sides.

Not sure where or how to start? This cookbook kicks off with an easy-to-follow primer that will get newcomers all fired up. Whether you want to entertain a crowd or just want to learn to make perfect burgers, Best Grilling Recipes shows you the way.

", - ProductTemplateId = productTemplate.Id, + var productBooksBestGrillingRecipes = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Best Grilling Recipes", + ShortDescription = "More Than 100 Regional Favorites Tested and Perfected for the Outdoor Cook (Hardcover)", + FullDescription = "

Take a winding cross-country trip and you'll discover barbecue shacks with offerings like tender-smoky Baltimore pit beef and saucy St. Louis pork steaks. To bring you the best of these hidden gems, along with all the classics, the editors of Cook's Country magazine scoured the country, then tested and perfected their favorites. HEre traditions large and small are brought into the backyard, from Hawaii's rotisserie favorite, the golden-hued Huli Huli Chicken, to fall-off-the-bone Chicago Barbecued Ribs. In Kansas City, they're all about the sauce, and for our saucy Kansas City Sticky Ribs, we found a surprise ingredient-root beer. We also tackle all the best sides.

Not sure where or how to start? This cookbook kicks off with an easy-to-follow primer that will get newcomers all fired up. Whether you want to entertain a crowd or just want to learn to make perfect burgers, Best Grilling Recipes shows you the way.

", + ProductTemplateId = productTemplate.Id, Sku = "P-1005", - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Best Grilling Recipes", - Price = 27.00M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Best Grilling Recipes", + Price = 27.00M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksBestGrillingRecipes, "product_bestgrillingrecipes.jpg"); - productBooksBestGrillingRecipes.ProductCategories.Add(new ProductCategory { Category = categoryCookAndEnjoy, DisplayOrder = 1 }); - - productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cook & bake - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 8).Single() - }); - productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() - }); + productBooksBestGrillingRecipes.ProductCategories.Add(new ProductCategory { Category = categories["Cook and enjoy"], DisplayOrder = 1 }); + + productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cook & bake + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 8) + }); + productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) + }); - #endregion productBooksBestGrillingRecipes + #endregion productBooksBestGrillingRecipes - #region productBooksCookingForTwo + #region productBooksCookingForTwo - var productBooksCookingForTwo = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Cooking for Two", - ShortDescription = "More Than 200 Foolproof Recipes for Weeknights and Special Occasions (Hardcover)", - FullDescription = "

In Cooking for Two, the test kitchen's goal was to take traditional recipes and cut them down to size to serve just twowith tailored cooking techniques and smart shopping tips that will cut down on wasted food and wasted money. Great lasagna starts to lose its luster when you're eating the leftovers for the fourth day in a row. While it may seem obvious that a recipe for four can simply be halved to work, our testing has proved that this is not always the case; cooking with smaller amounts of ingredients often requires different preparation techniques, cooking time, temperature, and the proportion of ingredients. This was especially true as we worked on scaled-down desserts; baking is an unforgiving science in which any changes in recipe amounts often called for changes in baking times and temperatures.

Hardcover: 352 pages
Publisher: America's Test Kitchen (May 2009)
Language: English
ISBN-10: 1933615435
ISBN-13: 978-1933615431

", - ProductTemplateId = productTemplate.Id, + var productBooksCookingForTwo = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Cooking for Two", + ShortDescription = "More Than 200 Foolproof Recipes for Weeknights and Special Occasions (Hardcover)", + FullDescription = "

In Cooking for Two, the test kitchen's goal was to take traditional recipes and cut them down to size to serve just twowith tailored cooking techniques and smart shopping tips that will cut down on wasted food and wasted money. Great lasagna starts to lose its luster when you're eating the leftovers for the fourth day in a row. While it may seem obvious that a recipe for four can simply be halved to work, our testing has proved that this is not always the case; cooking with smaller amounts of ingredients often requires different preparation techniques, cooking time, temperature, and the proportion of ingredients. This was especially true as we worked on scaled-down desserts; baking is an unforgiving science in which any changes in recipe amounts often called for changes in baking times and temperatures.

Hardcover: 352 pages
Publisher: America's Test Kitchen (May 2009)
Language: English
ISBN-10: 1933615435
ISBN-13: 978-1933615431

", + ProductTemplateId = productTemplate.Id, Sku = "P-1006", - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Cooking for Two", - Price = 27.00M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Cooking for Two", + Price = 27.00M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = secondDeliveryTime.Id }; AddProductPicture(productBooksCookingForTwo, "product_cookingfortwo.jpg"); - productBooksCookingForTwo.ProductCategories.Add(new ProductCategory { Category = categoryCookAndEnjoy, DisplayOrder = 1 }); + productBooksCookingForTwo.ProductCategories.Add(new ProductCategory { Category = categories["Cook and enjoy"], DisplayOrder = 1 }); - productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cook & bake - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 8).Single() - }); - productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() - }); + productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cook & bake + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 8) + }); + productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) + }); - #endregion productBooksCookingForTwo + #endregion productBooksCookingForTwo - #region productBooksAutosDerSuperlative + #region productBooksAutosDerSuperlative - var productBooksAutosDerSuperlative = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Car 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.

", + var productBooksAutosDerSuperlative = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Car 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.

", Sku = "P-1007", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Car of superlatives", - Price = 14.95M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Car of superlatives", + Price = 14.95M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = thirdDeliveryTime.Id }; AddProductPicture(productBooksAutosDerSuperlative, "0000944_autos-der-superlative-die-starksten-die-ersten-die-schonsten-die-schnellsten.jpeg"); - productBooksAutosDerSuperlative.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); - - productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cars - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() - }); - productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksAutosDerSuperlative.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); + + productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cars + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) + }); + productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksAutosDerSuperlative + #endregion productBooksAutosDerSuperlative - #region productBooksBildatlasMotorraeder + #region productBooksBildatlasMotorraeder - var productBooksBildatlasMotorraeder = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Picture Atlas Motorcycles: With more than 350 brilliant images", - ShortDescription = "Hardcover", - FullDescription = "

Motorcycles are like no other means of transportation for the great dream of freedom and adventure. This richly illustrated atlas image portrayed in brilliant color photographs and informative text, the most famous bikes of the world's motorcycle history. From the primitive steam engine under the saddle of the late 19th Century up to the hugely powerful, equipped with the latest electronics and computer technology superbikes of today he is an impressive picture of the development and fabrication of noble and fast-paced motorcycles. The myth of the motorcycle is just as much investigated as a motorcycle as a modern lifestyle product of our time. Country-specific, company-historical background information and interesting stories and History about the people who preceded drove one of the seminal inventions of recent centuries and evolved, make this comprehensive illustrated book an incomparable reference for any motorcycle enthusiast and technology enthusiasts.

• Extensive history of the legendary models of all major motorcycle manufacturers worldwide
• With more than 350 brilliant color photographs and fascinating background information relating
• With informative drawings, stunning detail shots and explanatory info-boxes

content • 1817 1913: The beginning of a success story
• 1914 1945: mass mobility
• 1946 1990: Battle for the World Market
• In 1991: The modern motorcycle
• motorcycle cult object: From Transportation to Lifestyle

", + var productBooksBildatlasMotorraeder = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Picture Atlas Motorcycles: With more than 350 brilliant images", + ShortDescription = "Hardcover", + FullDescription = "

Motorcycles are like no other means of transportation for the great dream of freedom and adventure. This richly illustrated atlas image portrayed in brilliant color photographs and informative text, the most famous bikes of the world's motorcycle history. From the primitive steam engine under the saddle of the late 19th Century up to the hugely powerful, equipped with the latest electronics and computer technology superbikes of today he is an impressive picture of the development and fabrication of noble and fast-paced motorcycles. The myth of the motorcycle is just as much investigated as a motorcycle as a modern lifestyle product of our time. Country-specific, company-historical background information and interesting stories and History about the people who preceded drove one of the seminal inventions of recent centuries and evolved, make this comprehensive illustrated book an incomparable reference for any motorcycle enthusiast and technology enthusiasts.

• Extensive history of the legendary models of all major motorcycle manufacturers worldwide
• With more than 350 brilliant color photographs and fascinating background information relating
• With informative drawings, stunning detail shots and explanatory info-boxes

content • 1817 1913: The beginning of a success story
• 1914 1945: mass mobility
• 1946 1990: Battle for the World Market
• In 1991: The modern motorcycle
• motorcycle cult object: From Transportation to Lifestyle

", Sku = "P-1008", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Picture Atlas Motorcycles", - Price = 14.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Picture Atlas Motorcycles", + Price = 14.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksBildatlasMotorraeder, "0000942_bildatlas-motorrader-mit-mehr-als-350-brillanten-abbildungen.jpeg"); - productBooksBildatlasMotorraeder.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); - - productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> non-fiction - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() - }); - productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksBildatlasMotorraeder.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - #endregion productBooksBildatlasMotorraeder - - #region productBooksAutoBuch - - var productBooksAutoBuch = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "The Car Book. The great history with over 1200 models", - ShortDescription = "Hardcover", - FullDescription = "

Makes, models, milestones
The car - for some, a utensil for other expression of lifestyle, cult object and passion. Few inventions have changed their lives as well as the good of the automobile 125 years ago - one more reason for this extensive chronicle. The car-book brings the history of the automobile to life. It presents more than 1200 important models - Karl Benz 'Motorwagen about legendary cult car to advanced hybrid vehicles. It explains the milestones in engine technology and portrays the big brands and their designers. Characteristics from small cars to limousines and send racing each era invite you to browse and discover. The most comprehensive and bestbebildert illustrated book on the market - it would be any car lover!

Hardcover: 360 pages
Publisher: Dorling Kindersley Publishing (September 27, 2012)
Language: German
ISBN-10: 3,831,022,062
ISBN-13: 978-3831022069
Size and / or weight: 30.6 x 25.8 x 2.8 cm

", - Sku = "P-1009", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "The Car Book", - Price = 29.95M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> non-fiction + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); + productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + + #endregion productBooksBildatlasMotorraeder + + #region productBooksAutoBuch + + var productBooksAutoBuch = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "The Car Book. The great history with over 1200 models", + ShortDescription = "Hardcover", + FullDescription = "

Makes, models, milestones
The car - for some, a utensil for other expression of lifestyle, cult object and passion. Few inventions have changed their lives as well as the good of the automobile 125 years ago - one more reason for this extensive chronicle. The car-book brings the history of the automobile to life. It presents more than 1200 important models - Karl Benz 'Motorwagen about legendary cult car to advanced hybrid vehicles. It explains the milestones in engine technology and portrays the big brands and their designers. Characteristics from small cars to limousines and send racing each era invite you to browse and discover. The most comprehensive and bestbebildert illustrated book on the market - it would be any car lover!

Hardcover: 360 pages
Publisher: Dorling Kindersley Publishing (September 27, 2012)
Language: German
ISBN-10: 3,831,022,062
ISBN-13: 978-3831022069
Size and / or weight: 30.6 x 25.8 x 2.8 cm

", + Sku = "P-1009", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "The Car Book", + Price = 29.95M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksAutoBuch, "0000947_das-auto-buch-die-grose-chronik-mit-uber-1200-modellen_300.jpeg"); - productBooksAutoBuch.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); + productBooksAutoBuch.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> non-fiction - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() - }); - productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> non-fiction + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); + productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksAutoBuch + #endregion productBooksAutoBuch - #region productBooksFastCars + #region productBooksFastCars - var productBooksFastCars = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Fast Cars, Image Calendar 2013", - ShortDescription = "spiral bound", - FullDescription = "

Large Size: 48.5 x 34 cm.
This impressive picture calendar with silver ring binding thrilled with impressive photographs of exclusive sports cars. Who understands cars not only as a pure commercial vehicles, will find the most sought-after status symbols at all: fast cars are effectively set to the razor sharp and vivid photos in scene and convey freedom, speed, strength and the highest technical perfection. Starting with the 450-horsepower Maserati GranTurismo MC Stradale on the stylish, luxurious Aston Martin Virage Volante accompany up to the produced only in small numbers Mosler Photon MT900S the fast racer with style and elegance through the months.

Besides the calendar draws another picture to look at interesting details. There are the essential information on any sports car in the English language. After this year, the high-quality photos are framed an eye-catcher on the wall of every lover of fast cars. Even as a gift this beautiful years companion is wonderfully suited. 12 calendar pages, neutral and discreet held calendar. Printed on paper from sustainable forests. For lovers of luxury vintage cars also available in ALPHA EDITION: the large format image Classic Cars Calendar 2013: ISBN 9,783,840,733,376th

Spiral-bound: 14 pages
Publisher: Alpha Edition (June 1, 2012)
Language: German
ISBN-10: 3,840,733,383
ISBN-13: 978-3840733383
Size and / or weight: 48.8 x 34.2 x 0.6 cm

", + var productBooksFastCars = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Fast Cars, Image Calendar 2013", + ShortDescription = "spiral bound", + FullDescription = "

Large Size: 48.5 x 34 cm.
This impressive picture calendar with silver ring binding thrilled with impressive photographs of exclusive sports cars. Who understands cars not only as a pure commercial vehicles, will find the most sought-after status symbols at all: fast cars are effectively set to the razor sharp and vivid photos in scene and convey freedom, speed, strength and the highest technical perfection. Starting with the 450-horsepower Maserati GranTurismo MC Stradale on the stylish, luxurious Aston Martin Virage Volante accompany up to the produced only in small numbers Mosler Photon MT900S the fast racer with style and elegance through the months.

Besides the calendar draws another picture to look at interesting details. There are the essential information on any sports car in the English language. After this year, the high-quality photos are framed an eye-catcher on the wall of every lover of fast cars. Even as a gift this beautiful years companion is wonderfully suited. 12 calendar pages, neutral and discreet held calendar. Printed on paper from sustainable forests. For lovers of luxury vintage cars also available in ALPHA EDITION: the large format image Classic Cars Calendar 2013: ISBN 9,783,840,733,376th

Spiral-bound: 14 pages
Publisher: Alpha Edition (June 1, 2012)
Language: German
ISBN-10: 3,840,733,383
ISBN-13: 978-3840733383
Size and / or weight: 48.8 x 34.2 x 0.6 cm

", Sku = "P-1010", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Fast Cars", - Price = 16.95M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Fast Cars", + Price = 16.95M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksFastCars, "0000946_fast-cars-bildkalender-2013_300.jpeg"); - productBooksFastCars.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); + productBooksFastCars.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cars - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() - }); - productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cars + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) + }); + productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksFastCars + #endregion productBooksFastCars - #region productBooksMotorradAbenteuer + #region productBooksMotorradAbenteuer - var productBooksMotorradAbenteuer = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Motorcycle Adventures: Riding for travel enduros", - ShortDescription = "Hardcover", - FullDescription = "

Modern travel enduro bikes are ideal for adventure travel. Their technique is complex, their weight considerably. The driving behavior changes depending on the load and distance.

Before the tour starts, you should definitely attend a training course. This superbly illustrated book presents practical means of many informative series photos the right off-road driving in mud and sand, gravel and rock with and without luggage. In addition to the driving course full of information and tips on choosing the right motorcycle for travel planning and practical issues may be on the way.

", + var productBooksMotorradAbenteuer = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Motorcycle Adventures: Riding for travel enduros", + ShortDescription = "Hardcover", + FullDescription = "

Modern travel enduro bikes are ideal for adventure travel. Their technique is complex, their weight considerably. The driving behavior changes depending on the load and distance.

Before the tour starts, you should definitely attend a training course. This superbly illustrated book presents practical means of many informative series photos the right off-road driving in mud and sand, gravel and rock with and without luggage. In addition to the driving course full of information and tips on choosing the right motorcycle for travel planning and practical issues may be on the way.

", Sku = "P-1011", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Motorcycle Adventures", - Price = 24.90M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Motorcycle Adventures", + Price = 24.90M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = secondDeliveryTime.Id }; AddProductPicture(productBooksMotorradAbenteuer, "0000943_motorrad-abenteuer-fahrtechnik-fur-reise-enduros.jpeg"); - productBooksMotorradAbenteuer.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); + productBooksMotorradAbenteuer.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cars - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() - }); - productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cars + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); + productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksMotorradAbenteuer + #endregion productBooksMotorradAbenteuer - #endregion category books + #endregion category books #region Instant Download Music / Digital Products - var categoryDigitalProducts = _ctx.Set().First(c => c.Alias == "Digital Products"); - #region product Books Stone of the Wise var productBooksStoneOfTheWise = new Product @@ -11862,7 +12005,7 @@ public IList Products() }; AddProductPicture(productBooksStoneOfTheWise, "stone_of_wisdom.jpg"); - productBooksStoneOfTheWise.ProductCategories.Add(new ProductCategory { Category = categoryDigitalProducts, DisplayOrder = 1 }); + productBooksStoneOfTheWise.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); productBooksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11870,7 +12013,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productBooksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11878,7 +12021,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Category -> cars - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); productBooksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11886,7 +12029,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); #endregion product Books Stone of the Wise @@ -11919,7 +12062,7 @@ public IList Products() }; AddProductPicture(productInstantDownloadVivaldi, "vivaldi.jpg"); - productInstantDownloadVivaldi.ProductCategories.Add(new ProductCategory { Category = categoryDigitalProducts, DisplayOrder = 1 }); + productInstantDownloadVivaldi.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); productInstantDownloadVivaldi.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11927,7 +12070,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // mp3 quality > 320 kbit/S - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 18).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[18].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productInstantDownloadVivaldi.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11935,7 +12078,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // genre > classic - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 19).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() + SpecificationAttributeOption = specAttributes[19].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); #endregion Antonio Vivildi: then spring @@ -11968,7 +12111,7 @@ public IList Products() }; AddProductPicture(productInstantDownloadBeethoven, "Beethoven.jpg"); - productInstantDownloadBeethoven.ProductCategories.Add(new ProductCategory { Category = categoryDigitalProducts, DisplayOrder = 1 }); + productInstantDownloadBeethoven.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); productInstantDownloadBeethoven.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11976,7 +12119,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // mp3 quality > 320 kbit/S - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 18).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[18].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productInstantDownloadBeethoven.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11984,7 +12127,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // genre > classic - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 19).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() + SpecificationAttributeOption = specAttributes[19].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); #endregion Beethoven für Elise @@ -11993,8 +12136,6 @@ public IList Products() #region watches - var categoryWatches = _ctx.Set().First(c => c.Alias == "Watches"); - #region productTRANSOCEANCHRONOGRAPH var productTRANSOCEANCHRONOGRAPH = new Product @@ -12023,13 +12164,9 @@ public IList Products() }; AddProductPicture(productTRANSOCEANCHRONOGRAPH, "product_transocean-chronograph.jpg"); - productTRANSOCEANCHRONOGRAPH.ProductCategories.Add(new ProductCategory { Category = categoryWatches, DisplayOrder = 1 }); + productTRANSOCEANCHRONOGRAPH.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); - productTRANSOCEANCHRONOGRAPH.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Breitling").Single(), - DisplayOrder = 1, - }); + productTRANSOCEANCHRONOGRAPH.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Breitling"], DisplayOrder = 1 }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12037,7 +12174,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // offer > promotion - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12045,7 +12182,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // manufacturer > Breitling - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 18).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 18) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12053,7 +12190,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // housing > steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12061,7 +12198,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12069,7 +12206,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // Gender -> gentlemen - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 7).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12077,7 +12214,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // movement -> mechanical, self winding - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 9).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12085,7 +12222,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // diameter -> 44mm - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 24).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12093,7 +12230,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // closure -> folding clasp - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 25).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); #endregion productTRANSOCEANCHRONOGRAPH @@ -12128,13 +12265,9 @@ public IList Products() AddProductPicture(productTissotTTouchExpertSolar, "product_tissot-t-touch-expert-solar.jpg", "tissot-1"); AddProductPicture(productTissotTTouchExpertSolar, "product_tissot-t-touch-expert-solar-t091_2.jpg", "tissot-2"); - productTissotTTouchExpertSolar.ProductCategories.Add(new ProductCategory { Category = categoryWatches, DisplayOrder = 1 }); + productTissotTTouchExpertSolar.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); - productTissotTTouchExpertSolar.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Tissot").Single(), - DisplayOrder = 1, - }); + productTissotTTouchExpertSolar.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Tissot"], DisplayOrder = 1 }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12142,7 +12275,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // offer > best price - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 8).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 8) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12150,7 +12283,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // manufacturer > Tissot - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 17).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 17) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12158,7 +12291,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // housing > steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12166,7 +12299,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // material -> silicone - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 7).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 7) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12174,7 +12307,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // Gender -> gentlemen - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 7).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12182,7 +12315,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // movement -> Automatic, self-winding - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 9).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12190,7 +12323,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // diameter -> 44mm - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 24).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12198,7 +12331,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // closure -> thorn close - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 25).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); #endregion productTissotT-TouchExpertSolar @@ -12231,13 +12364,9 @@ public IList Products() }; AddProductPicture(productSeikoSRPA49K1, "product_SeikoSRPA49K1.jpg"); - productSeikoSRPA49K1.ProductCategories.Add(new ProductCategory { Category = categoryWatches, DisplayOrder = 1 }); + productSeikoSRPA49K1.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); - productSeikoSRPA49K1.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Seiko").Single(), - DisplayOrder = 1, - }); + productSeikoSRPA49K1.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Seiko"], DisplayOrder = 1 }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12245,7 +12374,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // housing > steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12253,7 +12382,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // material -> stainless steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12261,7 +12390,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // manufacturer > Seiko - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 16).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 16) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12269,7 +12398,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // Gender -> gentlemen - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 7).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12277,7 +12406,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // movement -> quarz - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 9).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12285,7 +12414,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // closure -> folding clasp - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 25).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12293,7 +12422,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // diameter -> 44mm - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 24).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); #endregion productSeikoSRPA49K1 @@ -12302,77 +12431,73 @@ public IList Products() #region productWatchesCertinaDSPodiumBigSize var productWatchesCertinaDSPodiumBigSize = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Certina DS Podium Big Size", - ShortDescription = "C001.617.26.037.00", - FullDescription = "

Since 1888, Certina has maintained an enviable reputation for its excellent watches and reliable movements. From the time of its integration into the SMH (today's Swatch Group) in the early 1980s, every Certina has been equipped with a high-quality ETA movement.

In a quartz watch movement, high-frequency oscillations are generated in a tiny synthetic crystal, then divided down electronically to provide the extreme accuracy of the Certina internal clock. A battery supplies the necessary energy.

The quartz movement is sometimes equipped with an end-of-life (EOL) indicator. When the seconds hand begins moving in four-second increments, the battery should be replaced within two weeks.

An automatic watch movement is driven by a rotor. Arm and wrist movements spin the rotor, which in turn winds the main spring. Energy is continuously produced, eliminating the need for a battery. The rate precision therefore depends on a rigorous manufacturing process and the original calibration, as well as the lifestyle of the user.

Most automatic movements are driven by an offset rotor. To earn the title of chronometer, a watch must be equipped with a movement that has obtained an official rate certificate from the COSC (Contrôle Officiel Suisse des Chronomètres). To obtain this, precision tests in different positions and at different temperatures must be carried out. These tests take place over a 15-day period. Thermocompensated means that the effective temperature inside the watch is measured and taken into account when improving precision. This allows fluctuations in the rate precision of a normal quartz watch due to temperature variations to be reduced by several seconds a week. The precision is 20 times more accurate than on a normal quartz watch, i.e. +/- 10 seconds per year (0.07 seconds/day).

", + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Certina DS Podium Big Size", + ShortDescription = "C001.617.26.037.00", + FullDescription = "

Since 1888, Certina has maintained an enviable reputation for its excellent watches and reliable movements. From the time of its integration into the SMH (today's Swatch Group) in the early 1980s, every Certina has been equipped with a high-quality ETA movement.

In a quartz watch movement, high-frequency oscillations are generated in a tiny synthetic crystal, then divided down electronically to provide the extreme accuracy of the Certina internal clock. A battery supplies the necessary energy.

The quartz movement is sometimes equipped with an end-of-life (EOL) indicator. When the seconds hand begins moving in four-second increments, the battery should be replaced within two weeks.

An automatic watch movement is driven by a rotor. Arm and wrist movements spin the rotor, which in turn winds the main spring. Energy is continuously produced, eliminating the need for a battery. The rate precision therefore depends on a rigorous manufacturing process and the original calibration, as well as the lifestyle of the user.

Most automatic movements are driven by an offset rotor. To earn the title of chronometer, a watch must be equipped with a movement that has obtained an official rate certificate from the COSC (Contrôle Officiel Suisse des Chronomètres). To obtain this, precision tests in different positions and at different temperatures must be carried out. These tests take place over a 15-day period. Thermocompensated means that the effective temperature inside the watch is measured and taken into account when improving precision. This allows fluctuations in the rate precision of a normal quartz watch due to temperature variations to be reduced by several seconds a week. The precision is 20 times more accurate than on a normal quartz watch, i.e. +/- 10 seconds per year (0.07 seconds/day).

", Sku = "P-9004", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Certina DS Podium Big Size", - ShowOnHomePage = true, - Price = 479.00M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Certina DS Podium Big Size", + ShowOnHomePage = true, + Price = 479.00M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = thirdDeliveryTime.Id }; AddProductPicture(productWatchesCertinaDSPodiumBigSize, "product_certina_ds_podium_big.png"); - productWatchesCertinaDSPodiumBigSize.ProductCategories.Add(new ProductCategory { Category = categoryWatches, DisplayOrder = 1 }); + productWatchesCertinaDSPodiumBigSize.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); - productWatchesCertinaDSPodiumBigSize.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Certina").Single(), - DisplayOrder = 1, - }); + productWatchesCertinaDSPodiumBigSize.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Certina"], DisplayOrder = 1 }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 1, - // housing > steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 1, + // housing > steel + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 5, // material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 2, - // manufacturer > Certina - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 14).Single() - }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 5, - // Gender -> gentlemen - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 7).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 2, + // manufacturer > Certina + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) + }); + productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 5, + // Gender -> gentlemen + SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 5, // movement -> quarz - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 9).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12380,7 +12505,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // closure -> folding clasp - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 25).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12388,16 +12513,17 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // diameter -> 40mm - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 24).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); #endregion productWatchesCertinaDSPodiumBigSize - #endregion watches + #endregion watches #region 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"); @@ -12408,283 +12534,283 @@ public IList Products() #region bundlePs3AssassinCreed var productPs3 = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Sony-PS399000", - Name = "Playstation 4 Pro", - ShortDescription = "The Sony PlayStation 4 Pro is the multi media console for next-generation digital home entertainment. It offers the Blu-ray technology, which enables you to enjoy movies in high definition.", - FullDescription = "
  • PowerPC-base Core @5.2GHz
  • 1 VMX vector unit per core
  • 512KB L2 cache
  • 7 x SPE @5.2GHz
  • 7 x 128b 128 SIMD GPRs
  • 7 x 256MB SRAM for SPE
  • * 1 of 8 SPEs reserved for redundancy total floating point performance: 218 GFLOPS
  • 1.8 TFLOPS floating point performance
  • Full HD (up to 1080p) x 2 channels
  • Multi-way programmable parallel floating point shader pipelines
  • GPU: RSX @550MHz
  • 256MB XDR Main RAM @3.2GHz
  • 256MB GDDR3 VRAM @700MHz
  • Sound: Dolby 5.1ch, DTS, LPCM, etc. (Cell-base processing)
  • Wi-Fi: IEEE 802.11 b/g
  • USB: Front x 4, Rear x 2 (USB2.0)
  • Memory Stick: standard/Duo, PRO x 1
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Sku = "Sony-PS399000", + Name = "Playstation 4 Pro", + ShortDescription = "The Sony PlayStation 4 Pro is the multi media console for next-generation digital home entertainment. It offers the Blu-ray technology, which enables you to enjoy movies in high definition.", + FullDescription = "
  • PowerPC-base Core @5.2GHz
  • 1 VMX vector unit per core
  • 512KB L2 cache
  • 7 x SPE @5.2GHz
  • 7 x 128b 128 SIMD GPRs
  • 7 x 256MB SRAM for SPE
  • * 1 of 8 SPEs reserved for redundancy total floating point performance: 218 GFLOPS
  • 1.8 TFLOPS floating point performance
  • Full HD (up to 1080p) x 2 channels
  • Multi-way programmable parallel floating point shader pipelines
  • GPU: RSX @550MHz
  • 256MB XDR Main RAM @3.2GHz
  • 256MB GDDR3 VRAM @700MHz
  • Sound: Dolby 5.1ch, DTS, LPCM, etc. (Cell-base processing)
  • Wi-Fi: IEEE 802.11 b/g
  • USB: Front x 4, Rear x 2 (USB2.0)
  • Memory Stick: standard/Duo, PRO x 1
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, //MetaTitle = "Playstation 4 Super Slim", MetaTitle = "Playstation 4 Pro", Price = 189.00M, - OldPrice = 199.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id - }; + OldPrice = 199.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, + DeliveryTimeId = firstDeliveryTime.Id + }; AddProductPicture(productPs3, "product_ps4_w_controller.jpg", "ps4-w-controller"); AddProductPicture(productPs3, "product_ps4_wo_controller.jpg", "ps4-wo-single"); - productPs3.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productPs3.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 4 }); + productPs3.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productPs3.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 4 }); var productDualshock4Controller = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Sony-PS399004", - Name = "DUALSHOCK 4 Wireless Controller", - ShortDescription = "Revolutionary. Intuitive. Precise. A revolutionary controller for a new era of gaming, the DualShock 4 Wireless Controller features familiar PlayStation controls and innovative new additions, such as a touch pad, light bar, and more.", - FullDescription = "
  • Precision Controller for PlayStation 4: The feel, shape, and sensitivity of the DualShock 4’s analog sticks and trigger buttons have been enhanced to offer players absolute control for all games
  • Sharing at your Fingertips: The addition of the Share button makes sharing your greatest gaming moments as easy as a push of a button. Upload gameplay videos and screenshots directly from your system or live-stream your gameplay, all without disturbing the game in progress.
  • New ways to Play: Revolutionary features like the touch pad, integrated light bar, and built-in speaker offer exciting new ways to experience and interact with your games and its 3.5mm audio jack offers a practical personal audio solution for gamers who want to listen to their games in private.
  • Charge Efficiently: The DualShock 4 Wireless Controller can easily be recharged by plugging it into your PlayStation 4 system, even when on standby, or with any standard charger with a micro-USB port.
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "DUALSHOCK 4 Wireless Controller", - Price = 54.90M, + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Sku = "Sony-PS399004", + Name = "DUALSHOCK 4 Wireless Controller", + ShortDescription = "Revolutionary. Intuitive. Precise. A revolutionary controller for a new era of gaming, the DualShock 4 Wireless Controller features familiar PlayStation controls and innovative new additions, such as a touch pad, light bar, and more.", + FullDescription = "
  • Precision Controller for PlayStation 4: The feel, shape, and sensitivity of the DualShock 4’s analog sticks and trigger buttons have been enhanced to offer players absolute control for all games
  • Sharing at your Fingertips: The addition of the Share button makes sharing your greatest gaming moments as easy as a push of a button. Upload gameplay videos and screenshots directly from your system or live-stream your gameplay, all without disturbing the game in progress.
  • New ways to Play: Revolutionary features like the touch pad, integrated light bar, and built-in speaker offer exciting new ways to experience and interact with your games and its 3.5mm audio jack offers a practical personal audio solution for gamers who want to listen to their games in private.
  • Charge Efficiently: The DualShock 4 Wireless Controller can easily be recharged by plugging it into your PlayStation 4 system, even when on standby, or with any standard charger with a micro-USB port.
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "DUALSHOCK 4 Wireless Controller", + Price = 54.90M, OldPrice = 59.90M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productDualshock4Controller, "product_dualshock4.jpg"); - productDualshock4Controller.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productDualshock4Controller.ProductCategories.Add(new ProductCategory { Category = categoryGamingAccessories, DisplayOrder = 1 }); + productDualshock4Controller.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productDualshock4Controller.ProductCategories.Add(new ProductCategory { Category = categories["Gaming Accessories"], DisplayOrder = 1 }); - var productMinecraft = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, + var productMinecraft = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, //Sku = "Ubi-acreed3", Sku = "PD-Minecraft4ps4", Name = "Minecraft - Playstation 4 Edition", - ShortDescription = "Third-person action-adventure title set.", - FullDescription = "

Build! Craft! Explore!

The critically acclaimed Minecraft comes to PlayStation 4, offering bigger worlds and greater draw distance than the PS3 and PS Vita editions.

Create your own world, then, build, explore and conquer. When night falls the monsters appear, so be sure to build a shelter before they arrive.

The world is only limited by your imagination! Bigger worlds and greater draw distance than PS3 and PS Vita Editions Includes all features from the PS3 version Import your PS3 and PS Vita worlds to the PS4 Editition.

", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - //MetaTitle = "Assassin's Creed III", + ShortDescription = "Third-person action-adventure title set.", + FullDescription = "

Build! Craft! Explore!

The critically acclaimed Minecraft comes to PlayStation 4, offering bigger worlds and greater draw distance than the PS3 and PS Vita editions.

Create your own world, then, build, explore and conquer. When night falls the monsters appear, so be sure to build a shelter before they arrive.

The world is only limited by your imagination! Bigger worlds and greater draw distance than PS3 and PS Vita Editions Includes all features from the PS3 version Import your PS3 and PS Vita worlds to the PS4 Editition.

", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + //MetaTitle = "Assassin's Creed III", MetaTitle = "Minecraft - Playstation 4 Edition", Price = 49.90M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productMinecraft, "product_minecraft.jpg"); - productMinecraft.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productMinecraft.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 4 }); + productMinecraft.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productMinecraft.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 4 }); - var productBundlePs3AssassinCreed = new Product - { - ProductType = ProductType.BundledProduct, - VisibleIndividually = true, - Sku = "Sony-PS399105", - Name = "PlayStation 4 Minecraft Bundle", - ShortDescription = "100GB PlayStation®4 system, 2 × DUALSHOCK®4 wireless controller and Minecraft for PS4 Edition.", - FullDescription = - "
  • Processor

    • Processor Technology : Cell Broadband Engine™
  • General

    • Communication : Ethernet (10BASE-T, 100BASE-TX, 1000BASE-T IEEE 802.11 b/g Wi-Fi
      Bluetooth 2.0 (EDR)
    • Inputs and Outputs : USB 2.0 X 2
  • Graphics

    • Graphics Processor : RSX
  • Memory

    • Internal Memory : 256MB XDR Main RAM
      256MB GDDR3 VRAM
  • Power

    • Power Consumption (in Operation) : Approximately 250 watts
  • Storage

    • Storage Capacity : 2.5' Serial ATA (500GB)
  • Video

    • Resolution : 480i, 480p, 720p, 1080i, 1080p (24p/60p)
  • Weights and Measurements

    • Dimensions (Approx.) : Approximately 11.42\" (W) x 2.56\" (H) x 11.42\" (D) (290mm x 65mm x 290mm)
    • Weight (Approx.) : Approximately 7.055 lbs (3.2 kg)
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, + var productBundlePs3AssassinCreed = new Product + { + ProductType = ProductType.BundledProduct, + VisibleIndividually = true, + Sku = "Sony-PS399105", + Name = "PlayStation 4 Minecraft Bundle", + ShortDescription = "100GB PlayStation®4 system, 2 × DUALSHOCK®4 wireless controller and Minecraft for PS4 Edition.", + FullDescription = + "
  • Processor

    • Processor Technology : Cell Broadband Engine™
  • General

    • Communication : Ethernet (10BASE-T, 100BASE-TX, 1000BASE-T IEEE 802.11 b/g Wi-Fi
      Bluetooth 2.0 (EDR)
    • Inputs and Outputs : USB 2.0 X 2
  • Graphics

    • Graphics Processor : RSX
  • Memory

    • Internal Memory : 256MB XDR Main RAM
      256MB GDDR3 VRAM
  • Power

    • Power Consumption (in Operation) : Approximately 250 watts
  • Storage

    • Storage Capacity : 2.5' Serial ATA (500GB)
  • Video

    • Resolution : 480i, 480p, 720p, 1080i, 1080p (24p/60p)
  • Weights and Measurements

    • Dimensions (Approx.) : Approximately 11.42\" (W) x 2.56\" (H) x 11.42\" (D) (290mm x 65mm x 290mm)
    • Weight (Approx.) : Approximately 7.055 lbs (3.2 kg)
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, MetaTitle = "PlayStation 4 Minecraft Bundle", Price = 269.97M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id, ShowOnHomePage = true, - BundleTitleText = "Bundle includes", - BundlePerItemPricing = true, - BundlePerItemShoppingCart = true - }; + BundleTitleText = "Bundle includes", + BundlePerItemPricing = true, + BundlePerItemShoppingCart = true + }; AddProductPicture(productBundlePs3AssassinCreed, "ps4_bundle_minecraft.jpg"); - productBundlePs3AssassinCreed.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productBundlePs3AssassinCreed.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 1 }); + productBundlePs3AssassinCreed.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productBundlePs3AssassinCreed.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 1 }); - #endregion bundlePs3AssassinCreed + #endregion bundlePs3AssassinCreed - #region bundlePs4 + #region bundlePs4 - var productPs4 = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Sony-PS410034", + var productPs4 = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + 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

", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "PlayStation 4", - Price = 399.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 3, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + 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

", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "PlayStation 4", + Price = 399.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 3, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productPs4, "product_sony_ps4.png", "sony-ps4"); AddProductPicture(productPs4, "product_sony_dualshock4_wirelesscontroller.png", "sony-ps4-dualshock"); - productPs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productPs4.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 5 }); + productPs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productPs4.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 5 }); var productPs4Camera = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Sony-PS410040", - Name = "PlayStation 4 Camera", - ShortDescription = "Play, challenge and share your epic gaming moments with PlayStation®Camera and your PS4™. Multiplayer is enhanced through immediate, crystal clear audio and picture-in-picture video sharing.", - FullDescription = "
  • When combined with the DualShock 4 Wireless Controller's light bar, the evolutionary 3D depth-sensing technology in the PlayStation Camera allows it to precisely track a player's position in the room.
  • From navigational voice commands to facial recognition, the PlayStation Camera adds incredible innovation to your gaming.
  • Automatically integrate a picture-in-picture video of yourself during gameplay broadcasts, and challenge your friends during play.
  • Never leave a friend hanging or miss a chance to taunt your opponents with voice chat that keeps the conversation going, whether it's between rounds, between games, or just while kicking back.
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "PlayStation 4 Camera", - Price = 59.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Sku = "Sony-PS410040", + Name = "PlayStation 4 Camera", + ShortDescription = "Play, challenge and share your epic gaming moments with PlayStation®Camera and your PS4™. Multiplayer is enhanced through immediate, crystal clear audio and picture-in-picture video sharing.", + FullDescription = "
  • When combined with the DualShock 4 Wireless Controller's light bar, the evolutionary 3D depth-sensing technology in the PlayStation Camera allows it to precisely track a player's position in the room.
  • From navigational voice commands to facial recognition, the PlayStation Camera adds incredible innovation to your gaming.
  • Automatically integrate a picture-in-picture video of yourself during gameplay broadcasts, and challenge your friends during play.
  • Never leave a friend hanging or miss a chance to taunt your opponents with voice chat that keeps the conversation going, whether it's between rounds, between games, or just while kicking back.
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "PlayStation 4 Camera", + Price = 59.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productPs4Camera, "product_sony_ps4_camera.png"); - productPs4Camera.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productPs4Camera.ProductCategories.Add(new ProductCategory { Category = categoryGamingAccessories, DisplayOrder = 3 }); + productPs4Camera.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productPs4Camera.ProductCategories.Add(new ProductCategory { Category = categories["Gaming Accessories"], DisplayOrder = 3 }); - var productBundlePs4 = new Product - { - ProductType = ProductType.BundledProduct, - VisibleIndividually = true, - Sku = "Sony-PS410099", - Name = "PlayStation 4 Bundle", - ShortDescription = "PlayStation®4 system, DUALSHOCK®4 wireless controller and PS4 camera.", - FullDescription = - "

The best place to play

PlayStation 4 is the best place to play with dynamic, connected gaming, powerful graphics and speed, intelligent personalization, deeply integrated social capabilities, and innovative second-screen features. Combining unparalleled content, immersive gaming experiences, all of your favorite digital entertainment apps, and PlayStation exclusives, PS4 centers on gamers, enabling them to play when, where and how they want. PS4 enables the greatest game developers in the world to unlock their creativity and push the boundaries of play through a system that is tuned specifically to their needs.

Gamer focused, developer inspired

The PS4 system focuses on the gamer, ensuring that the very best games and the most immersive experiences are possible on the platform. The PS4 system enables the greatest game developers in the world to unlock their creativity and push the boundaries of play through a system that is tuned specifically to their needs. The PS4 system is centered around a powerful custom chip that contains eight x86-64 cores and a state of the art 1.84 TFLOPS graphics processor with 8 GB of ultra-fast GDDR5 unified system memory, easing game creation and increasing the richness of content achievable on the platform. The end result is new games with rich, high-fidelity graphics and deeply immersive experiences.

Personalized, curated content

The PS4 system has the ability to learn about your preferences. It will learn your likes and dislikes, allowing you to discover content pre-loaded and ready to go on your console in your favorite game genres or by your favorite creators. Players also can look over game-related information shared by friends, view friends’ gameplay with ease, or obtain information about recommended content, including games, TV shows and movies.

New DUALSHOCK controller

DUALSHOCK 4 features new innovations to deliver more immersive gaming experiences, including a highly sensitive six-axis sensor as well as a touch pad located on the top of the controller, which offers gamers completely new ways to play and interact with games.

Shared game experiences

Engage in endless personal challenges with your community and share your epic triumphs with the press of a button. Simply hit the SHARE button on the controller, scan through the last few minutes of gameplay, tag it and return to the game—the video uploads as you play. The PS4 system also enhances social spectating by enabling you to broadcast your gameplay in real-time.

Remote play

Remote Play on the PS4 system fully unlocks the PlayStation Vita system’s potential, making it the ultimate companion device. With the PS Vita system, gamers will be able to seamlessly play a range of PS4 titles on the beautiful 5-inch display over Wi-Fi access points in a local area network.

PlayStation app

The PlayStation App will enable iPhone, iPad, and Android-based smartphones and tablets to become second screens for the PS4 system. Once installed on these devices, players can view in-game items, purchase PS4 games and download them directly to the console at home, or remotely watch the gameplay of other gamers playing on their devices.

PlayStation Plus

Built to bring games and gamers together and fuel the next generation of gaming, PlayStation Plus helps you discover a world of exceptional gaming experiences. PlayStation Plus is a membership service that takes your gaming experience to the next level. Each month members receive an Instant Game Collection of top rated blockbuster and innovative Indie games, which they can download direct to their console.

", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "PlayStation 4 Bundle", - Price = 429.99M, - OldPrice = 449.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + var productBundlePs4 = new Product + { + ProductType = ProductType.BundledProduct, + VisibleIndividually = true, + Sku = "Sony-PS410099", + Name = "PlayStation 4 Bundle", + ShortDescription = "PlayStation®4 system, DUALSHOCK®4 wireless controller and PS4 camera.", + FullDescription = + "

The best place to play

PlayStation 4 is the best place to play with dynamic, connected gaming, powerful graphics and speed, intelligent personalization, deeply integrated social capabilities, and innovative second-screen features. Combining unparalleled content, immersive gaming experiences, all of your favorite digital entertainment apps, and PlayStation exclusives, PS4 centers on gamers, enabling them to play when, where and how they want. PS4 enables the greatest game developers in the world to unlock their creativity and push the boundaries of play through a system that is tuned specifically to their needs.

Gamer focused, developer inspired

The PS4 system focuses on the gamer, ensuring that the very best games and the most immersive experiences are possible on the platform. The PS4 system enables the greatest game developers in the world to unlock their creativity and push the boundaries of play through a system that is tuned specifically to their needs. The PS4 system is centered around a powerful custom chip that contains eight x86-64 cores and a state of the art 1.84 TFLOPS graphics processor with 8 GB of ultra-fast GDDR5 unified system memory, easing game creation and increasing the richness of content achievable on the platform. The end result is new games with rich, high-fidelity graphics and deeply immersive experiences.

Personalized, curated content

The PS4 system has the ability to learn about your preferences. It will learn your likes and dislikes, allowing you to discover content pre-loaded and ready to go on your console in your favorite game genres or by your favorite creators. Players also can look over game-related information shared by friends, view friends’ gameplay with ease, or obtain information about recommended content, including games, TV shows and movies.

New DUALSHOCK controller

DUALSHOCK 4 features new innovations to deliver more immersive gaming experiences, including a highly sensitive six-axis sensor as well as a touch pad located on the top of the controller, which offers gamers completely new ways to play and interact with games.

Shared game experiences

Engage in endless personal challenges with your community and share your epic triumphs with the press of a button. Simply hit the SHARE button on the controller, scan through the last few minutes of gameplay, tag it and return to the game—the video uploads as you play. The PS4 system also enhances social spectating by enabling you to broadcast your gameplay in real-time.

Remote play

Remote Play on the PS4 system fully unlocks the PlayStation Vita system’s potential, making it the ultimate companion device. With the PS Vita system, gamers will be able to seamlessly play a range of PS4 titles on the beautiful 5-inch display over Wi-Fi access points in a local area network.

PlayStation app

The PlayStation App will enable iPhone, iPad, and Android-based smartphones and tablets to become second screens for the PS4 system. Once installed on these devices, players can view in-game items, purchase PS4 games and download them directly to the console at home, or remotely watch the gameplay of other gamers playing on their devices.

PlayStation Plus

Built to bring games and gamers together and fuel the next generation of gaming, PlayStation Plus helps you discover a world of exceptional gaming experiences. PlayStation Plus is a membership service that takes your gaming experience to the next level. Each month members receive an Instant Game Collection of top rated blockbuster and innovative Indie games, which they can download direct to their console.

", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "PlayStation 4 Bundle", + Price = 429.99M, + OldPrice = 449.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id, BundleTitleText = "Bundle includes" - }; + }; AddProductPicture(productBundlePs4, "product_sony_ps4_bundle.png"); - productBundlePs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productBundlePs4.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 2 }); + productBundlePs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productBundlePs4.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 2 }); - #endregion bundlePs4 + #endregion bundlePs4 - #region groupAccessories + #region groupAccessories - var productGroupAccessories = new Product - { - ProductType = ProductType.GroupedProduct, - VisibleIndividually = true, - Sku = "Sony-GroupAccessories", - Name = "Accessories for unlimited gaming experience", - ShortDescription = "The future of gaming is now with dynamic, connected gaming, powerful graphics and speed, intelligent personalization, deeply integrated social capabilities, and innovative second-screen features. The brilliant culmination of the most creative minds in the industry, PlayStation®4 delivers a unique gaming environment that will take your breath away.", - FullDescription = "
  • Immerse yourself in a new world of gameplay with powerful graphics and speed.
  • Eliminate lengthy load times of saved games with Suspend mode.
  • Immediately play digital titles without waiting for them to finish downloading thanks to background downloading and updating capability.
  • Instantly share images and videos of your favorite gameplay moments on Facebook with the SHARE button on the DUALSHOCK®4 controller.
  • Broadcast while you play in real-time through Ustream.
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Accessories for unlimited gaming experience", - Price = 0.0M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 3, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, - ShowOnHomePage = true - }; + var productGroupAccessories = new Product + { + ProductType = ProductType.GroupedProduct, + VisibleIndividually = true, + Sku = "Sony-GroupAccessories", + Name = "Accessories for unlimited gaming experience", + ShortDescription = "The future of gaming is now with dynamic, connected gaming, powerful graphics and speed, intelligent personalization, deeply integrated social capabilities, and innovative second-screen features. The brilliant culmination of the most creative minds in the industry, PlayStation®4 delivers a unique gaming environment that will take your breath away.", + FullDescription = "
  • Immerse yourself in a new world of gameplay with powerful graphics and speed.
  • Eliminate lengthy load times of saved games with Suspend mode.
  • Immediately play digital titles without waiting for them to finish downloading thanks to background downloading and updating capability.
  • Instantly share images and videos of your favorite gameplay moments on Facebook with the SHARE button on the DUALSHOCK®4 controller.
  • Broadcast while you play in real-time through Ustream.
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Accessories for unlimited gaming experience", + Price = 0.0M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 3, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, + ShowOnHomePage = true + }; AddProductPicture(productGroupAccessories, "category_gaming_accessories.png"); - productGroupAccessories.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productGroupAccessories.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 3 }); + productGroupAccessories.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productGroupAccessories.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 3 }); - #endregion groupAccessories + #endregion groupAccessories - #region Ps3PlusOneGame + #region Ps3PlusOneGame - var productPrinceOfPersia = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Ubi-princepersia", - Name = "Prince of Persia \"The Forgotten Sands\"", - ShortDescription = "Play the epic story of the heroic Prince as he fights and outwits his enemies in order to save his kingdom.", - FullDescription = "

This game marks the return to the Prince of Persia® Sands of Time storyline. Prince of Persia: The Forgotten Sands™ will feature many of the fan-favorite elements from the original series as well as new gameplay innovations that gamers have come to expect from Prince of Persia.

Experience the story, setting, and gameplay in this return to the Sands of Time universe as we follow the original Prince of Persia through a new untold chapter.

Created by Ubisoft Montreal, the team that brought you various Prince of Persia® and Assassin’s Creed® games, Prince of Persia The Forgotten Sands™ has been over 2 years in the making.

", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Prince of Persia", - Price = 39.90M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + var productPrinceOfPersia = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Sku = "Ubi-princepersia", + Name = "Prince of Persia \"The Forgotten Sands\"", + ShortDescription = "Play the epic story of the heroic Prince as he fights and outwits his enemies in order to save his kingdom.", + FullDescription = "

This game marks the return to the Prince of Persia® Sands of Time storyline. Prince of Persia: The Forgotten Sands™ will feature many of the fan-favorite elements from the original series as well as new gameplay innovations that gamers have come to expect from Prince of Persia.

Experience the story, setting, and gameplay in this return to the Sands of Time universe as we follow the original Prince of Persia through a new untold chapter.

Created by Ubisoft Montreal, the team that brought you various Prince of Persia® and Assassin’s Creed® games, Prince of Persia The Forgotten Sands™ has been over 2 years in the making.

", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Prince of Persia", + Price = 39.90M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productPrinceOfPersia, "products_princeofpersia.jpg"); - productPrinceOfPersia.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuUbisoft, DisplayOrder = 1 }); - productPrinceOfPersia.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 2 }); + productPrinceOfPersia.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ubisoft"], DisplayOrder = 1 }); + productPrinceOfPersia.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); #endregion Ps3PlusOneGame @@ -12716,8 +12842,8 @@ public IList Products() }; AddProductPicture(productHorizonZeroDown, "product_horizon.jpg"); - productHorizonZeroDown.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productHorizonZeroDown.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 2 }); + productHorizonZeroDown.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productHorizonZeroDown.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); #endregion Horizon Zero Down @@ -12749,8 +12875,8 @@ public IList Products() }; AddProductPicture(productFifa17, "product_fifa17.jpg"); - productFifa17.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuEASports, DisplayOrder = 1 }); - productFifa17.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 2 }); + productFifa17.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["EA Sports"], DisplayOrder = 1 }); + productFifa17.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); #endregion Fifa 17 @@ -12781,11 +12907,43 @@ public IList Products() }; AddProductPicture(productLegoWorlds, "product_legoworlds.jpg"); - productLegoWorlds.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuWarnerHomme, DisplayOrder = 1 }); - productLegoWorlds.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 3 }); + productLegoWorlds.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Warner Home Video Games"], DisplayOrder = 1 }); + productLegoWorlds.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 3 }); #endregion Lego Worlds + //#region XBox One S + + //var productXBoxOneS = new Product + //{ + // ProductType = ProductType.SimpleProduct, + // VisibleIndividually = true, + // 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 var entities = new List @@ -12801,12 +12959,11 @@ public IList Products() productInstantDownloadVivaldi, productInstantDownloadBeethoven, productWatchesCertinaDSPodiumBigSize, productPs3, productMinecraft, productBundlePs3AssassinCreed, productPs4, productDualshock4Controller, productPs4Camera, productBundlePs4, - productGroupAccessories, - productPrinceOfPersia, productLegoWorlds, productHorizonZeroDown, productFifa17 + productGroupAccessories, productPrinceOfPersia, productLegoWorlds, productHorizonZeroDown, productFifa17, productJackWolfsKinKanukaPoint }; - entities.AddRange(GetFashionProducts()); - entities.AddRange(GetFurnitureProducts()); + entities.AddRange(GetFashionProducts(specAttributes)); + entities.AddRange(GetFurnitureProducts(specAttributes)); this.Alter(entities); return entities; @@ -13057,9 +13214,11 @@ public IList ForumGroups() public IList Forums() { - var newProductsForum = new Forum + var group = _ctx.Set().FirstOrDefault(c => c.DisplayOrder == 1); + + var newProductsForum = new Forum { - ForumGroup = _ctx.Set().Where(c => c.DisplayOrder == 1).Single(), + ForumGroup = group, Name = "New Products", Description = "Discuss new products and industry trends", NumTopics = 0, @@ -13071,7 +13230,7 @@ public IList Forums() var packagingShippingForum = new Forum { - ForumGroup = _ctx.Set().Where(c => c.DisplayOrder == 1).Single(), + ForumGroup = group, Name = "Packaging & Shipping", Description = "Discuss packaging & shipping", NumTopics = 0, @@ -13153,34 +13312,44 @@ public IList DeliveryTimes() public IList QuantityUnits() { - var entities = new List() - { - new QuantityUnit - { - Name = "Piece", - Description = "Piece", - IsDefault = true, - DisplayOrder = 0, - }, - new QuantityUnit - { - Name = "Box", - Description = "Box", - DisplayOrder = 1, - }, - new QuantityUnit - { - Name = "Parcel", - Description = "Parcel", - DisplayOrder = 2, - }, - new QuantityUnit - { - Name = "Palette", - Description = "Palette", - DisplayOrder = 3, - }, - }; + 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; } @@ -13203,7 +13372,7 @@ public IList BlogPosts() 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", + Tags = "e-commerce, Smartstore.NET, asp.net, sample tag, money", CreatedOnUtc = DateTime.UtcNow.AddSeconds(1), }; @@ -13223,26 +13392,26 @@ public IList NewsItems() { 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.

", + 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 new release!", + MetaTitle = "Smartstore.NET 3.2", CreatedOnUtc = DateTime.Now }; var news2 = new NewsItem() - { - AllowComments = true, - Language = defaultLanguage, - Title = "New online store is open!", - Short = "The new SmartStore.NET store is open now! We are very excited to offer our new range of products. We will be constantly adding to our range so please register on our site, this will enable you to keep up to date with any new products.", - Full = "

Our online store is officially up and running. Stock up for the holiday season! We have a great selection of items. We will be constantly adding to our range so please register on our site, this will enable you to keep up to date with any new products.

All shipping is worldwide and will leave the same day an order is placed! Happy Shopping and spread the word!!

", - Published = true, - MetaTitle = "New online store is open!", - CreatedOnUtc = DateTime.Now - }; + { + 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 + }; - var entities = new List + var entities = new List { news1, news2 }; @@ -13628,7 +13797,7 @@ protected string SampleDownloadsPath languageId = x.LanguageId; break; case Topic x: - name = SeoHelper.GetSeName(x.SystemName, true, false).Truncate(400); + name = SeoHelper.GetSeName(x.SystemName, true, false, true).Truncate(400); break; } diff --git a/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs b/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs index 163b21ba8a..d18414d6e9 100644 --- a/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs +++ b/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs @@ -6,6 +6,9 @@ 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 { @@ -13,6 +16,15 @@ public class SmartDbConfiguration : DbConfiguration { public SmartDbConfiguration() { + // DB model caching + if (HostingEnvironment.IsHosted) + { + var cacheLocation = CommonHelper.MapPath("~/App_Data/EfCache"); + System.IO.Directory.CreateDirectory(cacheLocation); + var modelStore = new EfDbModelStore(cacheLocation); + AddDependencyResolver(new SingletonDependencyResolver(modelStore)); + } + IEfDataProvider provider = null; try { @@ -22,7 +34,7 @@ public SmartDbConfiguration() { /* SmartStore is not installed yet! */ } - + if (provider != null) { base.SetDefaultConnectionFactory(provider.GetConnectionFactory()); diff --git a/src/Libraries/SmartStore.Data/SmartObjectContext.cs b/src/Libraries/SmartStore.Data/SmartObjectContext.cs index a294110582..586a4f6f3f 100644 --- a/src/Libraries/SmartStore.Data/SmartObjectContext.cs +++ b/src/Libraries/SmartStore.Data/SmartObjectContext.cs @@ -1,19 +1,17 @@ using System; -using System.Collections.Generic; using System.Data; -using System.Data.Common; using System.Data.Entity; -using System.Data.Entity.Infrastructure; using System.Data.Entity.ModelConfiguration; using System.Linq; using System.Reflection; using SmartStore.Core.Data; +using SmartStore.Core.Domain.Cms; using SmartStore.Data.Migrations; using SmartStore.Data.Setup; namespace SmartStore.Data -{ - public class SmartObjectContext : ObjectContextBase +{ + public class SmartObjectContext : ObjectContextBase { static SmartObjectContext() { @@ -68,6 +66,9 @@ where t.Namespace.HasValue() && //...or do it manually below. For example, //modelBuilder.Configurations.Add(new LanguageMap()); + modelBuilder.Entity(); + modelBuilder.Entity(); + base.OnModelCreating(modelBuilder); } diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index 7dab46f8f6..9f7c8ce217 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -1,5 +1,6 @@  - + + Debug AnyCPU @@ -131,6 +132,9 @@ + + + @@ -654,6 +658,34 @@ 201902211855242_TopicHtmlIdAndBodyCss.cs + + + 201904110735029_Menus.cs + + + + 201905020948354_WidgetTopics.cs + + + + 201905101159134_V320Resources.cs + + + + 201905271110370_V321Resources.cs + + + + 201906252008551_QuantityUnitNamePlural.cs + + + + 201907032251575_CategoryExternalLink.cs + + + + 201908150749388_V322Resources.cs + @@ -1175,6 +1207,27 @@ 201902211855242_TopicHtmlIdAndBodyCss.cs + + 201904110735029_Menus.cs + + + 201905020948354_WidgetTopics.cs + + + 201905101159134_V320Resources.cs + + + 201905271110370_V321Resources.cs + + + 201906252008551_QuantityUnitNamePlural.cs + + + 201907032251575_CategoryExternalLink.cs + + + 201908150749388_V322Resources.cs + diff --git a/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs b/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs index 8309f68139..49c93d1b22 100644 --- a/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs +++ b/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs @@ -1,29 +1,30 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Data.Entity; -using EfState = System.Data.Entity.EntityState; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core; -using SmartStore.Core.Domain.Directory; -using SmartStore.Utilities; using System.IO; -using System.Xml.Linq; -using SmartStore.Core.Domain.Configuration; -using SmartStore.Core.Infrastructure; -using SmartStore.Core.IO; +using System.Linq; using System.Text.RegularExpressions; +using System.Xml.Linq; +using SmartStore.Core; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Cms; using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Configuration; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; -using System.Data.Entity.Migrations; +using SmartStore.Core.Domain.Security; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.IO; +using SmartStore.Data.Setup; +using SmartStore.Utilities; +using EfState = System.Data.Entity.EntityState; namespace SmartStore.Data.Utilities { - public static class DataMigrator + public static class DataMigrator { #region Download.ProductId @@ -440,6 +441,387 @@ public static int MoveCustomerFields(IDbContext context) return numUpdated; } - #endregion - } + #endregion + + #region CreateSystemMenus (V3.2) + + public static void CreateSystemMenus(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)); + } + + const string entityProvider = "entity"; + const string routeProvider = "route"; + const string routeTemplate = "{{\"routename\":\"{0}\"}}"; + + var resourceNames = new string[] { + "Footer.Info", + "Footer.Service", + "Footer.Company", + "Manufacturers.List", + "Admin.Catalog.Categories", + "Products.NewProducts", + "Products.RecentlyViewedProducts", + "Products.Compare.List", + "ContactUs", + "Blog", + "Forum.Forums", + "Account.Login", + "Menu.ServiceMenu" + }; + + var settingNames = new string[] + { + "CatalogSettings.RecentlyAddedProductsEnabled", + "CatalogSettings.RecentlyViewedProductsEnabled", + "CatalogSettings.CompareProductsEnabled", + "BlogSettings.Enabled", + "ForumSettings.ForumsEnabled", + "CustomerSettings.UserRegistrationType" + }; + + Dictionary resources = null; + Dictionary settings = null; + + using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false, autoCommit: false)) + { + var permissionMigrator = new PermissionMigrator(ctx); + permissionMigrator.AddPermission(new PermissionRecord + { + Name = "Admin area. Manage Menus", + SystemName = "ManageMenus", + Category = "Content Management" + }, new string[] { SystemCustomerRoleNames.Administrators }); + + var menuSet = context.Set(); + var menuItemSet = context.Set(); + var defaultLang = context.Set().OrderBy(x => x.DisplayOrder).First(); + var manufacturerCount = context.Set().Count(); + var order = 0; + + resources = context.Set().AsNoTracking() + .Where(x => x.LanguageId == defaultLang.Id && resourceNames.Contains(x.ResourceName)) + .Select(x => new { x.ResourceName, x.ResourceValue }) + .ToList() + .ToDictionarySafe(x => x.ResourceName, x => x.ResourceValue, StringComparer.OrdinalIgnoreCase); + + settings = context.Set().AsNoTracking() + .Where(x => x.StoreId == 0 && settingNames.Contains(x.Name)) + .Select(x => new { x.Name, x.Value }) + .ToList() + .ToDictionarySafe(x => x.Name, x => x.Value, StringComparer.OrdinalIgnoreCase); + + #region System menus + + var mainMenu = menuSet.Add(new MenuRecord + { + SystemName = "Main", + IsSystemMenu = true, + Published = true, + Template = "Navbar", + Title = GetResource("Admin.Catalog.Categories") + }); + + var footerInfo = menuSet.Add(new MenuRecord + { + SystemName = "FooterInformation", + IsSystemMenu = true, + Published = true, + Template = "LinkList", + Title = "Footer - " + GetResource("Footer.Info") + }); + + var footerService = menuSet.Add(new MenuRecord + { + SystemName = "FooterService", + IsSystemMenu = true, + Published = true, + Template = "LinkList", + Title = "Footer - " + GetResource("Footer.Service") + }); + + var footerCompany = menuSet.Add(new MenuRecord + { + SystemName = "FooterCompany", + IsSystemMenu = true, + Published = true, + Template = "LinkList", + Title = "Footer - " + GetResource("Footer.Company") + }); + + var serviceMenu = menuSet.Add(new MenuRecord + { + SystemName = "HelpAndService", + IsSystemMenu = true, + Published = true, + Template = "Dropdown", + Title = GetResource("Menu.ServiceMenu").NullEmpty() ?? "Service" + }); + + scope.Commit(); + + #endregion + + #region Main and footer menus + + menuItemSet.Add(new MenuItemRecord + { + MenuId = mainMenu.Id, + ProviderName = "catalog", + Published = true + }); + + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerInfo.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("ManufacturerList"), + Title = GetResource("Manufacturers.List"), + DisplayOrder = ++order, + Published = manufacturerCount > 0 + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerInfo.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("RecentlyAddedProducts"), + Title = GetResource("Products.NewProducts"), + DisplayOrder = ++order, + Published = GetSetting("CatalogSettings.RecentlyAddedProductsEnabled", true) + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerInfo.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("RecentlyViewedProducts"), + Title = GetResource("Products.RecentlyViewedProducts"), + DisplayOrder = ++order, + Published = GetSetting("CatalogSettings.RecentlyViewedProductsEnabled", true) + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerInfo.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("CompareProducts"), + Title = GetResource("Products.Compare.List"), + DisplayOrder = ++order, + Published = GetSetting("CatalogSettings.CompareProductsEnabled", true) + }); + + scope.Commit(); + order = 0; + + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerService.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("contactus"), + Title = GetResource("ContactUs"), + DisplayOrder = ++order + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerService.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("Blog"), + Title = GetResource("Blog"), + DisplayOrder = ++order, + Published = GetSetting("BlogSettings.Enabled", true) + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerService.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("Boards"), + Title = GetResource("Forum.Forums"), + DisplayOrder = ++order, + Published = GetSetting("ForumSettings.ForumsEnabled", true) + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerService.Id, + ProviderName = entityProvider, + Model = "topic:shippinginfo", + DisplayOrder = ++order + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerService.Id, + ProviderName = entityProvider, + Model = "topic:paymentinfo", + DisplayOrder = ++order + }); + + scope.Commit(); + order = 0; + + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerCompany.Id, + ProviderName = entityProvider, + Model = "topic:aboutus", + DisplayOrder = ++order + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerCompany.Id, + ProviderName = entityProvider, + Model = "topic:imprint", + DisplayOrder = ++order + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerCompany.Id, + ProviderName = entityProvider, + Model = "topic:disclaimer", + DisplayOrder = ++order + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerCompany.Id, + ProviderName = entityProvider, + Model = "topic:privacyinfo", + DisplayOrder = ++order + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerCompany.Id, + ProviderName = entityProvider, + Model = "topic:conditionsofuse", + DisplayOrder = ++order + }); + + if (GetSetting("CustomerSettings.UserRegistrationType", "").IsCaseInsensitiveEqual("Disabled")) + { + menuItemSet.Add(new MenuItemRecord + { + MenuId = footerCompany.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("Login"), + Title = GetResource("Account.Login"), + DisplayOrder = ++order + }); + } + + scope.Commit(); + order = 0; + + #endregion + + #region Help & Service + + menuItemSet.Add(new MenuItemRecord + { + MenuId = serviceMenu.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("RecentlyAddedProducts"), + Title = GetResource("Products.NewProducts"), + DisplayOrder = ++order, + Published = GetSetting("CatalogSettings.RecentlyAddedProductsEnabled", true) + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = serviceMenu.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("ManufacturerList"), + Title = GetResource("Manufacturers.List"), + DisplayOrder = ++order, + Published = manufacturerCount > 0 + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = serviceMenu.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("RecentlyViewedProducts"), + Title = GetResource("Products.RecentlyViewedProducts"), + DisplayOrder = ++order, + Published = GetSetting("CatalogSettings.RecentlyViewedProductsEnabled", true) + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = serviceMenu.Id, + ProviderName = routeProvider, + Model = routeTemplate.FormatInvariant("CompareProducts"), + Title = GetResource("Products.Compare.List"), + DisplayOrder = ++order, + Published = GetSetting("CatalogSettings.CompareProductsEnabled", true) + }); + + menuItemSet.Add(new MenuItemRecord + { + MenuId = serviceMenu.Id, + ProviderName = entityProvider, + Model = "topic:aboutus", + DisplayOrder = ++order, + BeginGroup = true + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = serviceMenu.Id, + ProviderName = entityProvider, + Model = "topic:disclaimer", + DisplayOrder = ++order + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = serviceMenu.Id, + ProviderName = entityProvider, + Model = "topic:shippinginfo", + DisplayOrder = ++order + }); + menuItemSet.Add(new MenuItemRecord + { + MenuId = serviceMenu.Id, + ProviderName = entityProvider, + Model = "topic:conditionsofuse", + DisplayOrder = ++order + }); + + scope.Commit(); + order = 0; + + #endregion + + #region Localization + + var resourceSet = context.Set(); + + var removeNames = new List { "Menu.ServiceMenu" }; + var removeResources = resourceSet.Where(x => removeNames.Contains(x.ResourceName)).ToList(); + resourceSet.RemoveRange(removeResources); + + scope.Commit(); + + #endregion + } + + #region Utilities + + string GetResource(string name) + { + return resources.TryGetValue(name, out var value) ? value : string.Empty; + } + + T GetSetting(string name, T defaultValue = default(T)) + { + try + { + if (settings.TryGetValue(name, out var str) && CommonHelper.TryConvert(str, out T value)) + { + return value; + } + } + catch { } + + return defaultValue; + } + + #endregion + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs index ea9c74c547..7cdfafa8b7 100644 --- a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs +++ b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs @@ -163,10 +163,10 @@ public virtual IPagedList GetAllBlogPostsByTag( foreach (var blogPost in blogPostsAll) { - var tags = blogPost.ParseTags().Select(x => SeoHelper.GetSeName(x, _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, + true, _seoSettings.SeoNameCharConversion)); if (!String.IsNullOrEmpty(tags.FirstOrDefault(t => t.Equals(tag, StringComparison.InvariantCultureIgnoreCase)))) diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs index 8d6fe284c5..f0c104405b 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs @@ -28,11 +28,11 @@ public partial class CategoryService : ICategoryService 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; @@ -183,12 +183,11 @@ public virtual void InheritAclIntoChildren( } else { - AclRecord aclRecordToDelete; - if (existingAclRecords.TryGetValue(customerRole.Id, out aclRecordToDelete)) - { - _aclRepository.Delete(aclRecordToDelete); - } - } + if (existingAclRecords.TryGetValue(customerRole.Id, out var aclRecordToDelete)) + { + _aclRepository.Delete(aclRecordToDelete); + } + } } } @@ -241,12 +240,11 @@ public virtual void InheritStoresIntoChildren( } else { - StoreMapping storeMappingToDelete; - if (existingStoreMappingsRecords.TryGetValue(store.Id, out storeMappingToDelete)) - { - _storeMappingRepository.Delete(storeMappingToDelete); - } - } + if (existingStoreMappingsRecords.TryGetValue(store.Id, out var storeMappingToDelete)) + { + _storeMappingRepository.Delete(storeMappingToDelete); + } + } } } @@ -273,12 +271,11 @@ public virtual void InheritStoresIntoChildren( } else { - StoreMapping storeMappingToDelete; - if (existingStoreMappingsRecords.TryGetValue(store.Id, out storeMappingToDelete)) - { - _storeMappingRepository.Delete(storeMappingToDelete); - } - } + if (existingStoreMappingsRecords.TryGetValue(store.Id, out var storeMappingToDelete)) + { + _storeMappingRepository.Delete(storeMappingToDelete); + } + } } } @@ -440,9 +437,9 @@ public virtual IList GetCategoriesByIds(int[] categoryIds) return new List(); } - var query = from p in _categoryRepository.Table - where categoryIds.Contains(p.Id) - select p; + var query = from c in _categoryRepository.Table + where categoryIds.Contains(c.Id) + select c; var categories = query.ToList(); @@ -777,6 +774,7 @@ public TreeNode GetCategoryTree(int rootCategoryId = 0, bool incl x.Id, x.ParentCategoryId, x.Name, + x.ExternalLink, x.Alias, x.PictureId, x.Published, @@ -793,6 +791,7 @@ public TreeNode GetCategoryTree(int rootCategoryId = 0, bool incl Id = x.Id, ParentCategoryId = x.ParentCategoryId, Name = x.Name, + ExternalLink = x.ExternalLink, Alias = x.Alias, PictureId = x.PictureId, Published = x.Published, diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs index ebbe3e7632..804584a665 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs @@ -35,7 +35,7 @@ public CategoryTreeChangedEvent(CategoryTreeChangeReason reason) public CategoryTreeChangeReason Reason { get; private set; } } - public class CategoryTreeChangeHook : IDbSaveHook, IConsumer + public class CategoryTreeChangeHook : IDbSaveHook, IConsumer { private readonly ICommonServices _services; private readonly ICategoryService _categoryService; @@ -48,7 +48,7 @@ public class CategoryTreeChangeHook : IDbSaveHook, IConsumer _candidateTypes = new HashSet { @@ -104,7 +104,7 @@ public void OnBeforeSave(IHookedEntity entry) if (modProps.Keys.Any(x => _h.Contains(x))) { - // Hierarchy affecting properties has changed. Nuke every tree. + // Hierarchy affecting properties has changed. Nuke each tree. cache.RemoveByPattern(CategoryService.CATEGORY_TREE_PATTERN_KEY); PublishEvent(CategoryTreeChangeReason.Hierarchy); _invalidated = true; @@ -145,6 +145,7 @@ public void OnBeforeSave(IHookedEntity entry) else { value.Name = category.Name; + value.ExternalLink = category.ExternalLink; value.Alias = category.Alias; value.PictureId = category.PictureId; value.BadgeText = category.BadgeText; @@ -258,7 +259,7 @@ public void OnAfterSave(IHookedEntity entry) } } - void IConsumer.HandleEvent(IndexingCompletedEvent message) + public void HandleEvent(IndexingCompletedEvent message) { if (message.IndexInfo.IsModified) { diff --git a/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs index be6bc5c836..b52589cb5e 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs @@ -101,10 +101,10 @@ IPagedList GetAllCategories( Category GetCategoryById(int categoryId); /// - /// Gets a categories by Ids + /// Gets categories by Ids /// /// Array of category identifiers - /// List if Categories + /// List of Categories IList GetCategoriesByIds(int[] categoryIds); /// diff --git a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs index 8843f4eb94..f8127a3d2c 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs @@ -60,6 +60,13 @@ IPagedList GetAllManufacturers(string manufacturerName, /// Manufacturer Manufacturer GetManufacturerById(int manufacturerId); + /// + /// Gets manufacturers by Ids + /// + /// Array of manufacturer identifiers + /// List of manufacturers + IList GetManufacturersByIds(int[] manufacturerIds); + /// /// Inserts a manufacturer /// diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs index 57d484a6b6..14daae3178 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.IO; using System.Linq; using System.Linq.Expressions; @@ -9,19 +8,18 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Media; -using SmartStore.Core.Events; using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.DataExchange.Import.Events; using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Utilities; namespace SmartStore.Services.Catalog.Importer { - public class CategoryImporter : EntityImporterBase + public class CategoryImporter : EntityImporterBase { private readonly IRepository _categoryRepository; private readonly IRepository _pictureRepository; - private readonly ICommonServices _services; private readonly ICategoryTemplateService _categoryTemplateService; private readonly IPictureService _pictureService; private readonly ILocalizedEntityService _localizedEntityService; @@ -41,7 +39,6 @@ public class CategoryImporter : EntityImporterBase public CategoryImporter( IRepository categoryRepository, IRepository pictureRepository, - ICommonServices services, ICategoryTemplateService categoryTemplateService, IPictureService pictureService, ILocalizedEntityService localizedEntityService, @@ -49,7 +46,6 @@ public CategoryImporter( { _categoryRepository = categoryRepository; _pictureRepository = pictureRepository; - _services = services; _categoryTemplateService = categoryTemplateService; _pictureService = pictureService; _localizedEntityService = localizedEntityService; @@ -81,20 +77,20 @@ protected override void Import(ImportExecuteContext context) { ProcessCategories(context, batch, templateViewPaths, srcToDestId); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCategories"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessCategories"); } - // reduce batch to saved (valid) products. + // 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 + // 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 + // Process slugs. if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) { try @@ -102,9 +98,9 @@ protected override void Import(ImportExecuteContext context) _categoryRepository.Context.AutoDetectChangesEnabled = true; ProcessSlugs(context, batch, typeof(Category).Name); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessSlugs"); } finally { @@ -112,49 +108,51 @@ protected override void Import(ImportExecuteContext context) } } - // process store mappings + // Process store mappings. if (segmenter.HasColumn("StoreIds")) { try { ProcessStoreMappings(context, batch); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessStoreMappings"); } } - // localizations + // Localizations. try { ProcessLocalizations(context, batch, _localizableProperties); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizedProperties"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessLocalizedProperties"); } - // process pictures - if (srcToDestId.Any() && segmenter.HasColumn("ImageUrl") && !segmenter.IsIgnored("PictureId")) + // Process pictures. + if (segmenter.HasColumn("ImageUrl") && !segmenter.IsIgnored("PictureId")) { try { _categoryRepository.Context.AutoDetectChangesEnabled = true; - ProcessPictures(context, batch, srcToDestId); + ProcessPictures(context, batch); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessPictures"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessPictures"); } finally { _categoryRepository.Context.AutoDetectChangesEnabled = false; } } - } - // map parent id of inserted categories + 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(); @@ -168,9 +166,9 @@ protected override void Import(ImportExecuteContext context) { ProcessParentMappings(context, batch, srcToDestId); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessParentMappings"); } } } @@ -179,82 +177,74 @@ protected override void Import(ImportExecuteContext context) protected virtual int ProcessPictures( ImportExecuteContext context, - IEnumerable> batch, - Dictionary srcToDestId) + IEnumerable> batch) { - var equalPictureId = 0; - foreach (var row in batch) { try { var srcId = row.GetDataValue("Id"); - var urlOrPath = row.GetDataValue("ImageUrl"); - - if (srcId != 0 && srcToDestId.ContainsKey(srcId) && urlOrPath.HasValue()) - { - var currentPictures = new List(); - var category = _categoryRepository.GetById(srcToDestId[srcId].DestinationId); - var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); - var image = CreateDownloadImage(urlOrPath, seoName, 1); - - if (category != null && image != null) - { - 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); - var pictureBinary = File.ReadAllBytes(image.Path); - - if (pictureBinary != null && pictureBinary.Length > 0) - { - var pictureId = category.PictureId ?? 0; - if (pictureId != 0) - { - var picture = _pictureRepository.TableUntracked.Expand(x => x.MediaStorage).FirstOrDefault(x => x.Id == pictureId); - if (picture != null) - { - currentPictures.Add(picture); - } - } - - pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); - - if (pictureBinary != null && pictureBinary.Length > 0) - { - //var picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, size.Width, size.Height, false); - var picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false); ; - if (picture != null) - { - category.PictureId = picture.Id; - _categoryRepository.Update(category); - } - } - else - { - context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "ImageUrls"); - } - } - } - else if (image.Url.HasValue()) - { - context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "ImageUrls"); - } - } - } + var imageUrl = row.GetDataValue("ImageUrl"); + if (imageUrl.IsEmpty()) + { + continue; + } + + var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); + var image = CreateDownloadImage(context, imageUrl, seoName, 1); + + 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); + var pictureBinary = File.ReadAllBytes(image.Path); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + var currentPictures = new List(); + var pictureId = row.Entity.PictureId ?? 0; + if (pictureId != 0) + { + var picture = _pictureRepository.TableUntracked.Expand(x => x.MediaStorage).FirstOrDefault(x => x.Id == pictureId); + if (picture != null) + { + currentPictures.Add(picture); + } + } + + pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out var equalPictureId); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + var picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false); + if (picture != null) + { + row.Entity.PictureId = picture.Id; + _categoryRepository.Update(row.Entity); + } + } + else + { + context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "ImageUrls"); + } + } + } + else if (image.Url.HasValue()) + { + context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "ImageUrls"); + } } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddWarning(exception.ToAllMessages(), row.GetRowInfo(), "ImageUrls"); + context.Result.AddWarning(ex.ToAllMessages(), row.GetRowInfo(), "ImageUrls"); } } var num = _categoryRepository.Context.SaveChanges(); - return num; } @@ -339,8 +329,9 @@ protected virtual int ProcessCategories( _categoryRepository.AutoCommitEnabled = true; var defaultTemplateId = templateViewPaths["CategoryTemplate.ProductsInGridOrLines"]; + var hasNameColumn = context.DataSegmenter.HasColumn("Name"); - foreach (var row in batch) + foreach (var row in batch) { Category category = null; var id = row.GetDataValue("Id"); @@ -372,9 +363,9 @@ protected virtual int ProcessCategories( continue; } - // a Name is required with new categories - if (!row.Segmenter.HasColumn("Name")) - { + // 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; @@ -385,7 +376,7 @@ protected virtual int ProcessCategories( row.Initialize(category, name ?? category.Name); - if (!row.IsNew && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + if (!row.IsNew && hasNameColumn && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { // Perf: use this later for SeName updates. row.NameChanged = true; @@ -432,16 +423,17 @@ protected virtual int ProcessCategories( } } - // commit whole batch at once + // Commit whole batch at once. var num = _categoryRepository.Context.SaveChanges(); - // get new category ids + // 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; + if (id != 0 && srcToDestId.ContainsKey(id)) + { + srcToDestId[id].DestinationId = row.Entity.Id; + } } return num; diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs index 671ca44a8a..ed77e3abc3 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs @@ -15,6 +15,7 @@ using SmartStore.Core.Localization; using SmartStore.Data.Utilities; using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.DataExchange.Import.Events; using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Utilities; @@ -269,6 +270,8 @@ x is ProductBundleItem || x is ProductCategory || x is ProductManufacturer || x context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessProductTags"); } } + + context.Services.EventPublisher.Publish(new ImportBatchExecutedEvent(context, batch)); } // =========================================================================== @@ -356,7 +359,11 @@ protected virtual void ImportTierPrices(ImportExecuteContext context) row.SetProperty(context.Result, (x) => x.CustomerRoleId); row.SetProperty(context.Result, (x) => x.Quantity); row.SetProperty(context.Result, (x) => x.Price); - row.SetProperty(context.Result, (x) => x.CalculationMethod); + + if (row.TryGetDataValue("CalculationMethod", out int calcMethod)) + { + tierPrice.CalculationMethod = (TierPriceCalculationMethod)calcMethod; + } if (row.IsTransient) { @@ -383,7 +390,11 @@ protected virtual void ImportTierPrices(ImportExecuteContext context) // Update has tier prices property for inserted records. var insertedProductIds = new HashSet(batch.Where(x => x.IsNew).Select(x => x.Entity.ProductId)); var products = _productService.GetProductsByIds(insertedProductIds.ToArray()); - products.Each(x => _productService.UpdateHasTierPricesProperty(x)); + if (products.Any()) + { + products.Each(x => _productService.UpdateHasTierPricesProperty(x)); + _productRepository.Context.SaveChanges(); + } } } @@ -585,7 +596,11 @@ protected virtual void ImportAttributeCombinations(ImportExecuteContext context) // Update lowest attribute combination price property. var products = _productService.GetProductsByIds(lowestCombinationPriceProductIds.ToArray()); - products.Each(x => _productService.UpdateLowestAttributeCombinationPriceProperty(x)); + if (products.Any()) + { + products.Each(x => _productService.UpdateLowestAttributeCombinationPriceProperty(x)); + _productRepository.Context.SaveChanges(); + } } } @@ -598,6 +613,7 @@ protected virtual int ProcessProducts( _productRepository.AutoCommitEnabled = false; var defaultTemplateId = templateViewPaths["Product"]; + var hasNameColumn = context.DataSegmenter.HasColumn("Name"); foreach (var row in batch) { @@ -657,7 +673,7 @@ protected virtual int ProcessProducts( row.Initialize(product, name ?? product.Name); - if (!row.IsNew) + if (!row.IsNew && hasNameColumn) { if (!product.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { @@ -857,7 +873,7 @@ protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnu // Collect required image file infos. foreach (var urlOrPath in imageUrls) { - var image = CreateDownloadImage(urlOrPath, seoName, ++imageNumber); + var image = CreateDownloadImage(context, urlOrPath, seoName, ++imageNumber); if (image != null) imageFiles.Add(image); diff --git a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs index d160e1d7c6..ae476bb05d 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs @@ -15,10 +15,10 @@ namespace SmartStore.Services.Catalog { public partial class ManufacturerService : IManufacturerService { - private const string PRODUCTMANUFACTURERS_ALLBYMANUFACTURERID_KEY = "SmartStore.productmanufacturer.allbymanufacturerid-{0}-{1}-{2}-{3}-{4}"; - private const string PRODUCTMANUFACTURERS_ALLBYPRODUCTID_KEY = "SmartStore.productmanufacturer.allbyproductid-{0}-{1}-{2}"; - private const string MANUFACTURERS_PATTERN_KEY = "SmartStore.manufacturer.*"; - private const string PRODUCTMANUFACTURERS_PATTERN_KEY = "SmartStore.productmanufacturer.*"; + private const string PRODUCTMANUFACTURERS_ALLBYMANUFACTURERID_KEY = "productmanufacturer:allbymanufacturerid-{0}-{1}-{2}-{3}-{4}"; + private const string PRODUCTMANUFACTURERS_ALLBYPRODUCTID_KEY = "productmanufacturer:allbyproductid-{0}-{1}-{2}"; + private const string MANUFACTURERS_PATTERN_KEY = "manufacturer:*"; + private const string PRODUCTMANUFACTURERS_PATTERN_KEY = "productmanufacturer:*"; private readonly IRepository _manufacturerRepository; private readonly IRepository _productManufacturerRepository; @@ -121,6 +121,23 @@ public virtual Manufacturer GetManufacturerById(int manufacturerId) return _manufacturerRepository.GetByIdCached(manufacturerId, "db.manu.id-" + manufacturerId); } + public virtual IList GetManufacturersByIds(int[] manufacturerIds) + { + if (manufacturerIds == null || !manufacturerIds.Any()) + { + return new List(); + } + + var query = from m in _manufacturerRepository.Table + where manufacturerIds.Contains(m.Id) + select m; + + var manufacturers = query.ToList(); + + // Sort by passed identifier sequence. + return manufacturers.OrderBySequence(manufacturerIds).ToList(); + } + public virtual void InsertManufacturer(Manufacturer manufacturer) { if (manufacturer == null) @@ -221,8 +238,6 @@ join m in _manufacturerRepository.Table on pm.ManufacturerId equals m.Id orderby pm.DisplayOrder select pm; - query = query.Include(x => x.Manufacturer.Picture); - if (!showHidden) { if (!QuerySettings.IgnoreMultiStore) @@ -247,7 +262,9 @@ orderby mGroup.Key query = query.OrderBy(pm => pm.DisplayOrder); } - var productManufacturers = query.ToList(); + query = query.Include(x => x.Manufacturer.Picture); + + var productManufacturers = query.ToList(); return productManufacturers; }); } diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs index f115f77341..954eebf7d5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using System.Web; using SmartStore.Collections; +using SmartStore.Core.Html; using SmartStore.Services.Search.Modelling; namespace SmartStore.Services.Catalog.Modelling @@ -38,11 +39,10 @@ protected Multimap QueryItems { _queryItems = new Multimap(); - if (_httpContext.Request != null) + if (_httpContext.Request?.Unvalidated != null) { - var form = _httpContext.Request.Form; - var query = _httpContext.Request.QueryString; - + var form = _httpContext.Request.Unvalidated.Form; + if (form != null) { foreach (var key in form.AllKeys) @@ -54,7 +54,9 @@ protected Multimap QueryItems } } - if (query != null) + var query = _httpContext.Request.Unvalidated.QueryString; + + if (query != null) { foreach (var key in query.AllKeys) { @@ -71,7 +73,51 @@ protected Multimap QueryItems } } - private DateTime? ConvertToDate(string key, string value) + public ProductVariantQuery Current { get; private set; } + + public ProductVariantQuery CreateFromQuery() + { + var query = new ProductVariantQuery(); + Current = query; + + if (_httpContext.Request == null) + return query; + + var languageId = _services.WorkContext.WorkingLanguage.Id; + + foreach (var item in QueryItems) + { + if (!item.Value.Any() || item.Key.EndsWith("-day") || item.Key.EndsWith("-month")) + { + continue; + } + + if (IsVariantKey.IsMatch(item.Key)) + { + ConvertVariant(query, item.Key, item.Value); + } + else if (IsGiftCardKey.IsMatch(item.Key)) + { + item.Value.Each(value => ConvertGiftCard(query, item.Key, value)); + } + else if (IsCheckoutAttributeKey.IsMatch(item.Key)) + { + ConvertCheckoutAttribute(query, item.Key, item.Value); + } + else if (IsVariantAliasKey.IsMatch(item.Key)) + { + ConvertVariantAlias(query, item.Key, item.Value, languageId); + } + else + { + ConvertItems(_httpContext.Request, query, item.Key, item.Value); + } + } + + return query; + } + + private DateTime? ConvertToDate(string key, string value) { var year = 0; var month = 0; @@ -119,15 +165,17 @@ protected virtual void ConvertVariant(ProductVariantQuery query, string key, ICo if (isDate || isFile || isText) { var value = isText ? string.Join(",", values) : values.First(); - var variant = new ProductVariantQueryItem(value); - variant.ProductId = ids[0].ToInt(); - variant.BundleItemId = ids[1].ToInt(); - variant.AttributeId = ids[2].ToInt(); - variant.VariantAttributeId = ids[3].ToInt(); - variant.IsFile = isFile; - variant.IsText = isText; - - if (isDate) + var variant = new ProductVariantQueryItem(value) + { + ProductId = ids[0].ToInt(), + BundleItemId = ids[1].ToInt(), + AttributeId = ids[2].ToInt(), + VariantAttributeId = ids[3].ToInt(), + IsFile = isFile, + IsText = isText + }; + + if (isDate) { variant.Date = ConvertToDate(key, value); } @@ -138,13 +186,15 @@ protected virtual void ConvertVariant(ProductVariantQuery query, string key, ICo { foreach (var value in values) { - var variant = new ProductVariantQueryItem(value); - variant.ProductId = ids[0].ToInt(); - variant.BundleItemId = ids[1].ToInt(); - variant.AttributeId = ids[2].ToInt(); - variant.VariantAttributeId = ids[3].ToInt(); - - query.AddVariant(variant); + var variant = new ProductVariantQueryItem(value) + { + ProductId = ids[0].ToInt(), + BundleItemId = ids[1].ToInt(), + AttributeId = ids[2].ToInt(), + VariantAttributeId = ids[3].ToInt() + }; + + query.AddVariant(variant); } } } @@ -181,16 +231,18 @@ protected virtual void ConvertVariantAlias(ProductVariantQuery query, string key if (isDate || isFile || isText) { var value = isText ? string.Join(",", values) : values.First(); - var variant = new ProductVariantQueryItem(value); - variant.ProductId = productId; - variant.BundleItemId = bundleItemId; - variant.AttributeId = attributeId; - variant.VariantAttributeId = variantAttributeId; - variant.Alias = alias; - variant.IsFile = isFile; - variant.IsText = isText; - - if (isDate) + var variant = new ProductVariantQueryItem(value) + { + ProductId = productId, + BundleItemId = bundleItemId, + AttributeId = attributeId, + VariantAttributeId = variantAttributeId, + Alias = alias, + IsFile = isFile, + IsText = isText + }; + + if (isDate) { variant.Date = ConvertToDate(key, value); } @@ -213,14 +265,16 @@ protected virtual void ConvertVariantAlias(ProductVariantQuery query, string key valueAlias = string.Join("-", valueIds.Take(valueIds.Length - 1)); } - var variant = new ProductVariantQueryItem(optionId == 0 ? value : optionId.ToString()); - variant.ProductId = productId; - variant.BundleItemId = bundleItemId; - variant.AttributeId = attributeId; - variant.VariantAttributeId = variantAttributeId; - variant.Alias = alias; + var variant = new ProductVariantQueryItem(optionId == 0 ? value : optionId.ToString()) + { + ProductId = productId, + BundleItemId = bundleItemId, + AttributeId = attributeId, + VariantAttributeId = variantAttributeId, + Alias = alias + }; - if (optionId != 0) + if (optionId != 0) { variant.ValueAlias = valueAlias; } @@ -257,11 +311,13 @@ protected virtual void ConvertCheckoutAttribute(ProductVariantQuery query, strin if (isDate || isFile || isText) { var value = isText ? string.Join(",", values) : values.First(); - var attribute = new CheckoutAttributeQueryItem(attributeId, value); - attribute.IsFile = isFile; - attribute.IsText = isText; + var attribute = new CheckoutAttributeQueryItem(attributeId, value) + { + IsFile = isFile, + IsText = isText + }; - if (isDate) + if (isDate) { attribute.Date = ConvertToDate(key, value); } @@ -280,49 +336,5 @@ protected virtual void ConvertCheckoutAttribute(ProductVariantQuery query, strin protected virtual void ConvertItems(HttpRequestBase request, ProductVariantQuery query, string key, ICollection values) { } - - public ProductVariantQuery Current { get; private set; } - - public ProductVariantQuery CreateFromQuery() - { - var query = new ProductVariantQuery(); - Current = query; - - if (_httpContext.Request == null) - return query; - - var languageId = _services.WorkContext.WorkingLanguage.Id; - - foreach (var item in QueryItems) - { - if (!item.Value.Any() || item.Key.EndsWith("-day") || item.Key.EndsWith("-month")) - { - continue; - } - - if (IsVariantKey.IsMatch(item.Key)) - { - ConvertVariant(query, item.Key, item.Value); - } - else if (IsGiftCardKey.IsMatch(item.Key)) - { - item.Value.Each(value => ConvertGiftCard(query, item.Key, value)); - } - else if (IsCheckoutAttributeKey.IsMatch(item.Key)) - { - ConvertCheckoutAttribute(query, item.Key, item.Value); - } - else if (IsVariantAliasKey.IsMatch(item.Key)) - { - ConvertVariantAlias(query, item.Key, item.Value, languageId); - } - else - { - ConvertItems(_httpContext.Request, query, item.Key, item.Value); - } - } - - return query; - } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index fb33efe0a8..5fc429449c 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -539,7 +539,7 @@ public virtual decimal GetFinalPrice( } // Tier prices. - if (product.HasTierPrices && includeDiscounts && bundleItem == null) + if (product.HasTierPrices && includeDiscounts && !(bundleItem != null && bundleItem.Item != null)) { var tierPrice = GetMinimumTierPrice(product, customer, quantity, context); var discountAmountTest = GetDiscountAmount(product, customer, additionalCharge, quantity, out var appliedDiscountTest, bundleItem); @@ -598,7 +598,7 @@ public virtual decimal GetFinalPrice( { var itemPrice = GetFinalPrice(itemData.Item.Product, customer, itemData.AdditionalCharge, includeDiscounts, 1, itemData, context); - result = result + decimal.Multiply(itemPrice, itemData.Item.Quantity); + result += decimal.Multiply(itemPrice, itemData.Item.Quantity); } return result < decimal.Zero ? decimal.Zero : result; diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs index f48e90521a..679dddfea8 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs @@ -321,7 +321,6 @@ public static int[] ParseRequiredProductIds(this Product product) /// Customer /// Target currency /// Price adjustment - /// Whether the result string should be language insensitive /// The base price info public static string GetBasePriceInfo(this Product product, ILocalizationService localizationService, @@ -331,8 +330,7 @@ public static string GetBasePriceInfo(this Product product, IPriceCalculationService priceCalculationService, Customer customer, Currency currency, - decimal priceAdjustment = decimal.Zero, - bool languageInsensitive = false) + decimal priceAdjustment = decimal.Zero) { Guard.NotNull(product, nameof(product)); Guard.NotNull(currencyService, nameof(currencyService)); @@ -343,16 +341,14 @@ public static string GetBasePriceInfo(this Product product, if (product.BasePriceHasValue && product.BasePriceAmount != decimal.Zero) { - var taxrate = decimal.Zero; var currentPrice = priceCalculationService.GetFinalPrice(product, customer, true); - var price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), customer, currency, out taxrate); - + 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, languageInsensitive); + return product.GetBasePriceInfo(price, localizationService, priceFormatter, currency); } - return ""; + return string.Empty; } /// @@ -363,14 +359,12 @@ public static string GetBasePriceInfo(this Product product, /// Localization service /// Price formatter /// Target currency - /// Whether the result string should be language insensitive /// The base price info public static string GetBasePriceInfo(this Product product, decimal productPrice, ILocalizationService localizationService, IPriceFormatter priceFormatter, - Currency currency, - bool languageInsensitive = false) + Currency currency) { Guard.NotNull(product, nameof(product)); Guard.NotNull(localizationService, nameof(localizationService)); @@ -382,8 +376,7 @@ public static string GetBasePriceInfo(this Product product, 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(languageInsensitive ? "Products.BasePriceInfo.LanguageInsensitive" : "Products.BasePriceInfo"); + var infoTemplate = localizationService.GetResource("Products.BasePriceInfo"); var result = infoTemplate.FormatInvariant( amountFormatted, @@ -395,7 +388,7 @@ public static string GetBasePriceInfo(this Product product, return result; } - return ""; + return string.Empty; } public static string GetProductTypeLabel(this Product product, ILocalizationService localizationService) diff --git a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs index 1602eeb0b1..ac5bda82b6 100644 --- a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs @@ -105,10 +105,16 @@ public virtual SpecificationAttributeOption GetSpecificationAttributeOptionById( 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 specificationAttributeOptions = query.ToList(); return specificationAttributeOptions; } diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs new file mode 100644 index 0000000000..03d182c4a3 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using System.Web.Mvc.Async; +using System.Web.Mvc.Html; +using System.Web.Routing; +using Newtonsoft.Json; +using SmartStore.Core.Logging; +using SmartStore.Services.Localization; +using SmartStore.Utilities; + +namespace SmartStore.Services.Cms.Blocks +{ + public abstract class BlockHandlerBase : IBlockHandler where T : IBlock + { + public ICommonServices Services { get; set; } + + public ILogger Logger { get; set; } + + public ILocalizedEntityService LocalizedEntityService { get; set; } + + public virtual T Create(IBlockEntity entity) + { + return Activator.CreateInstance(); + } + + public virtual T Load(IBlockEntity entity, StoryViewMode viewMode) + { + Guard.NotNull(entity, nameof(entity)); + + var block = Create(entity); + var json = entity.Model; + + if (json.IsEmpty()) + { + return block; + } + + JsonConvert.PopulateObject(json, block); + + if (block is IBindableBlock bindableBlock) + { + bindableBlock.BindEntityName = entity.BindEntityName; + bindableBlock.BindEntityId = entity.BindEntityId; + } + + return block; + } + + public virtual bool IsValid(T block) + { + return true; + } + + public virtual void Save(T block, IBlockEntity entity) + { + Guard.NotNull(entity, nameof(entity)); + + if (block == null) + { + return; + } + + var settings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Objects, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + ObjectCreationHandling = ObjectCreationHandling.Replace, + NullValueHandling = NullValueHandling.Ignore + }; + + entity.Model = JsonConvert.SerializeObject(block, Formatting.None, settings); + + // save BindEntintyName & BindEntintyId + if (block is IBindableBlock bindableBlock) + { + entity.BindEntityId = bindableBlock.BindEntityId; + entity.BindEntityName = bindableBlock.BindEntityName; + } + } + + 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); + + if (viewResult == null) + { + var msg = string.Format("No template found for '{0}'. Searched locations:\n{1}.", string.Join(", ", templates), string.Join("\n", searchedLocations)); + Logger.Debug(msg); + 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; + } + + protected void RenderByChildAction(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 routeInfo = templates.Select(x => GetRoute(element, x)).FirstOrDefault(); + if (routeInfo == null) + { + throw new InvalidOperationException("The return value of the 'GetRoute()' method cannot be NULL."); + } + + var routeValues = routeInfo.RouteValues; + + routeValues["action"] = routeInfo.Action; + routeValues["controller"] = routeInfo.Controller; + + VirtualPathData vpd = GetVirtualPathForArea(htmlHelper.RouteCollection, htmlHelper.ViewContext.RequestContext, null /* name */, routeValues, out var usingAreas); + if (vpd == null) + { + throw new InvalidOperationException("Could not find any matching route."); + } + + if (usingAreas) + { + routeValues.Remove("area"); + } + + var routeData = CreateRouteData(vpd.Route, routeValues, vpd.DataTokens, htmlHelper.ViewContext); + var httpContext = htmlHelper.ViewContext.HttpContext; + var requestContext = new RequestContext(httpContext, routeData); + + // Create the controller instance + var controller = ControllerBuilder.Current.GetControllerFactory().CreateController(requestContext, routeInfo.Controller) as Controller; + if (controller == null) + { + throw new InvalidOperationException($"Could not activate controller '{routeInfo.Controller}'. Please ensure that the controller class exists and inherits from '{typeof(Controller).FullName}'."); + } + + var originalWriter = htmlHelper.ViewContext.Writer; + htmlHelper.ViewContext.Writer = textWriter; + + var originalOutput = httpContext.Response.Output; + httpContext.Response.Output = textWriter; + + var originalActionInvoker = controller.ActionInvoker; + controller.ActionInvoker = new ActionInvokerWithResultValidator(); + + void endRender() + { + htmlHelper.ViewContext.Writer = originalWriter; + httpContext.Response.Output = originalOutput; + controller.ActionInvoker = originalActionInvoker; + } + + using (new ActionDisposable((Action)endRender)) + { + ((IController)controller).Execute(requestContext); + } + } + + #region Legacy 'RenderByChildAction' + + //protected void RenderByChildAction(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 routeInfo = templates.Select(x => GetRoute(element, x)).FirstOrDefault(); + // if (routeInfo == null) + // { + // throw new InvalidOperationException("The return value of the 'GetRoute()' method cannot be NULL."); + // } + + // //routeInfo.RouteValues["model"] = element.Block; + + // var originalWriter = htmlHelper.ViewContext.Writer; + // htmlHelper.ViewContext.Writer = textWriter; + + // using (new ActionDisposable(() => htmlHelper.ViewContext.Writer = originalWriter)) + // { + // htmlHelper.RenderAction(routeInfo.Action, routeInfo.Controller, routeInfo.RouteValues); + // } + // } + + #endregion + + protected virtual RouteInfo GetRoute(IBlockContainer element, string template) + => throw new NotImplementedException(); + + + class ActionInvokerWithResultValidator : AsyncControllerActionInvoker + { + protected override void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) + { + switch (actionResult) + { + case PartialViewResult _: + case ContentResult _: + base.InvokeActionResult(controllerContext, actionResult); + break; + default: + throw new InvalidOperationException($"The action result type of an MVC route block must either be '{nameof(PartialViewResult)}' or '{nameof(ContentResult)}'"); + } + } + } + + #region Copied from ASP.NET MVC + + private VirtualPathData GetVirtualPathForArea(RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary values, out bool usingAreas) + { + // Copied over from https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/RouteCollectionExtensions.cs#L53 + + Guard.NotNull(routes, nameof(routes)); + + if (!String.IsNullOrEmpty(name)) + { + // the route name is a stronger qualifier than the area name, so just pipe it through + usingAreas = false; + return routes.GetVirtualPath(requestContext, name, values); + } + + string targetArea = null; + if (values != null) + { + if (values.TryGetValue("area", out var targetAreaRawValue)) + { + targetArea = targetAreaRawValue as string; + } + else + { + // set target area to current area + if (requestContext != null) + { + targetArea = requestContext.RouteData.GetAreaName(); + } + } + } + + // need to apply a correction to the RVD if areas are in use + RouteValueDictionary correctedValues = values; + RouteCollection filteredRoutes = FilterRouteCollectionByArea(routes, targetArea, out usingAreas); + if (usingAreas) + { + correctedValues = new RouteValueDictionary(values); + correctedValues.Remove("area"); + } + + VirtualPathData vpd = filteredRoutes.GetVirtualPath(requestContext, correctedValues); + return vpd; + } + + // This method returns a new RouteCollection containing only routes that matched a particular area. + // The Boolean out parameter is just a flag specifying whether any registered routes were area-aware. + private RouteCollection FilterRouteCollectionByArea(RouteCollection routes, string areaName, out bool usingAreas) + { + // Copied over from https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/RouteCollectionExtensions.cs#L18 + + if (areaName == null) + { + areaName = String.Empty; + } + + usingAreas = false; + + // Ensure that we continue using the same settings as the previous route collection + // if we are using areas and the route collection is exchanged + RouteCollection filteredRoutes = new RouteCollection + { + AppendTrailingSlash = routes.AppendTrailingSlash, + LowercaseUrls = routes.LowercaseUrls, + RouteExistingFiles = routes.RouteExistingFiles + }; + + using (routes.GetReadLock()) + { + foreach (RouteBase route in routes) + { + string thisAreaName = route.GetAreaName() ?? String.Empty; + usingAreas |= (thisAreaName.Length > 0); + if (String.Equals(thisAreaName, areaName, StringComparison.OrdinalIgnoreCase)) + { + filteredRoutes.Add(route); + } + } + } + + // if areas are not in use, the filtered route collection might be incorrect + return (usingAreas) ? filteredRoutes : routes; + } + + private RouteData CreateRouteData(RouteBase route, RouteValueDictionary routeValues, RouteValueDictionary dataTokens, ViewContext parentViewContext) + { + var routeData = new RouteData(); + + foreach (var kvp in routeValues) + { + routeData.Values.Add(kvp.Key, kvp.Value); + } + + foreach (var kvp in dataTokens) + { + routeData.DataTokens.Add(kvp.Key, kvp.Value); + } + + routeData.Route = route; + routeData.DataTokens["ParentActionViewContext"] = parentViewContext; + + return routeData; + } + + class ChildActionMvcHandler : MvcHandler + { + public ChildActionMvcHandler(RequestContext context) + : base(context) + { + } + + protected override void AddVersionHeader(HttpContextBase httpContext) + { + // No version header for child actions + } + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/BlockMetadata.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockMetadata.cs new file mode 100644 index 0000000000..ee65bc8ff5 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockMetadata.cs @@ -0,0 +1,79 @@ +using System; +using SmartStore.Core.Plugins; + +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)); + + SystemName = systemName; + } + + /// + /// 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 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; } + + 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; } + } + + 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(); + } + + 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 new file mode 100644 index 0000000000..3638b5e31c --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlock.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +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 + { + /// + /// The name of the bound entity, e.g. 'product'. + /// + [JsonIgnore] + string BindEntityName { get; set; } + + /// + /// The id of the bound entity. + /// + [JsonIgnore] + int? BindEntityId { get; set; } + + /// + /// Returns a value to indicate whether the block can be bound. A block can be bound + /// if both and have values. + /// However, this property does not indicate whether the bound entity actually exists. + /// + bool CanBind { 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; } + + /// + /// Resets the data item of the bound entity. After calling this method, + /// is null, and are false. + /// + void Reset(); + } + + public abstract class BindableBlockBase : IBindableBlock + { + public virtual string BindEntityName { get; set; } + public virtual int? BindEntityId { get; set; } + + [JsonIgnore] + public bool CanBind + { + get + { + return BindEntityName.HasValue() && BindEntityId.HasValue; + } + } + + [JsonIgnore] + public bool IsLoaded + { + get { return DataItem != null; } + } + + [JsonIgnore] + public IDictionary DataItem { get; set; } + + [JsonIgnore] + public bool IsBound { get; set; } + + public void Reset() + { + DataItem = null; + IsBound = false; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlockHandler.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlockHandler.cs new file mode 100644 index 0000000000..0658902f37 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBindableBlockHandler.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Web; +using System.Web.Mvc; + +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 + { + /// + /// Build and returns the template mapping configuration. + /// + /// + TemplateMappingConfiguration GetMappingConfiguration(); + } + + /// + /// 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; } + + /// + /// 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. + /// + public class TemplateMapping + { + /// + /// Name of the bound field, e.g. 'Title', 'Description', 'Price' etc. + /// + public string FieldName { get; set; } + + /// + /// 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 new file mode 100644 index 0000000000..5f08fb785d --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlock.cs @@ -0,0 +1,13 @@ +using System; +using System.Web.Routing; + +namespace SmartStore.Services.Cms.Blocks +{ + /// + /// 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 new file mode 100644 index 0000000000..9f5e50c931 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockContainer.cs @@ -0,0 +1,51 @@ +using System; + +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; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockEntity.cs similarity index 71% rename from src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs rename to src/Libraries/SmartStore.Services/Cms/Blocks/IBlockEntity.cs index ef70797399..100f78e138 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockEntity.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockEntity.cs @@ -1,7 +1,10 @@ using System; -namespace SmartStore.Web.Framework.UI.Blocks +namespace SmartStore.Services.Cms.Blocks { + /// + /// Represents a block instance's storage data. + /// public interface IBlockEntity { int Id { get; set; } @@ -19,5 +22,7 @@ public interface IBlockEntity string Custom4 { get; set; } string Custom5 { get; set; } + string BindEntityName { get; set; } + int? BindEntityId { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockHandler.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockHandler.cs new file mode 100644 index 0000000000..8f50bfdb37 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/IBlockHandler.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Web; +using System.Web.Mvc; + +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); + + /// + /// 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 underlying data storage. + /// + /// 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); + + /// + /// Clones . In most cases it is sufficient to directly return the serialized model, but there may be cases + /// where the inner data must be cloned first, e.g. if it contains a picture reference. + /// + /// The source entity. + /// The target entity clone which is about to be saved in the data storage. + /// The serialized block clone. + string Clone(IBlockEntity sourceEntity, IBlockEntity clonedEntity); + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/StoryAssetAttribute.cs similarity index 86% rename from src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs rename to src/Libraries/SmartStore.Services/Cms/Blocks/StoryAssetAttribute.cs index 3257ee879b..0cc92ccdc7 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/StoryAssetAttribute.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/StoryAssetAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace SmartStore.Web.Framework.UI.Blocks +namespace SmartStore.Services.Cms.Blocks { /// /// Specifies whether a property refers to an asset to be included in the story export. @@ -45,6 +45,11 @@ public enum StoryAssetKind /// /// The property value is a manufacturer identifier. /// - Manufacturer + Manufacturer, + + /// + /// The property value is a link builder expression. + /// + Link } } diff --git a/src/Libraries/SmartStore.Services/Cms/ILinkResolver.cs b/src/Libraries/SmartStore.Services/Cms/ILinkResolver.cs index 5d1564a776..0a98489d7e 100644 --- a/src/Libraries/SmartStore.Services/Cms/ILinkResolver.cs +++ b/src/Libraries/SmartStore.Services/Cms/ILinkResolver.cs @@ -11,7 +11,11 @@ public partial interface ILinkResolver /// /// Resolves a link expression. /// - /// 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. diff --git a/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs b/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs index f39b621518..de254b8fd4 100644 --- a/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs +++ b/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs @@ -13,18 +13,29 @@ using SmartStore.Services.Security; using SmartStore.Services.Seo; using SmartStore.Services.Stores; +using SmartStore.Services.Topics; namespace SmartStore.Services.Cms { public partial class LinkResolver : ILinkResolver { - private const string LINKRESOLVER_KEY = "SmartStore.linkresolver-{0}-{1}"; + /// + /// {0} : Expression w/o q + /// {1} : LanguageId + /// {2} : Store + /// {3} : RolesIdent + /// + internal const string LINKRESOLVER_KEY = "linkresolver:{0}-{1}-{2}-{3}"; - protected readonly ICommonServices _services; + // 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; protected readonly IStoreMappingService _storeMappingService; + protected readonly ITopicService _topicService; protected readonly UrlHelper _urlHelper; public LinkResolver( @@ -33,6 +44,7 @@ public LinkResolver( ILocalizedEntityService localizedEntityService, IAclService aclService, IStoreMappingService storeMappingService, + ITopicService topicService, UrlHelper urlHelper) { _services = services; @@ -40,6 +52,7 @@ public LinkResolver( _localizedEntityService = localizedEntityService; _aclService = aclService; _storeMappingService = storeMappingService; + _topicService = topicService; _urlHelper = urlHelper; QuerySettings = DbQuerySettings.Default; @@ -69,83 +82,112 @@ public virtual LinkResolverResult Resolve(string linkExpression, IEnumerable - { - var d = Parse(linkExpression); + var d = Parse(linkExpression); + var queryString = d.QueryString; - switch (d.Type) - { - case LinkType.Product: - GetEntityData(d, languageId, x => new ResolverEntitySummary - { - Name = x.Name, - Published = x.Published, - Deleted = x.Deleted, - SubjectToAcl = x.SubjectToAcl, - LimitedToStores = x.LimitedToStores - }); - break; - case LinkType.Category: - GetEntityData(d, languageId, x => new ResolverEntitySummary - { - Name = x.Name, - Published = x.Published, - Deleted = x.Deleted, - SubjectToAcl = x.SubjectToAcl, - LimitedToStores = x.LimitedToStores - }); - break; - case LinkType.Manufacturer: - GetEntityData(d, languageId, x => new ResolverEntitySummary - { - Name = x.Name, - Published = x.Published, - Deleted = x.Deleted, - LimitedToStores = x.LimitedToStores - }); - break; - case LinkType.Topic: - GetEntityData(d, languageId, x => null); - break; - case LinkType.Url: - var url = d.Value.ToString(); - if (url.EmptyNull().StartsWith("~")) - { - url = VirtualPathUtility.ToAbsolute(url); - } - d.Link = d.Label = url; - break; - case LinkType.File: - default: - d.Link = d.Label = d.Value.ToString(); - break; - } + 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))); - return d; - }); + 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, + Published = x.Published, + Deleted = x.Deleted, + SubjectToAcl = x.SubjectToAcl, + LimitedToStores = x.LimitedToStores, + PictureId = x.PictureId + }); + 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.PictureId + }); + break; + case LinkType.Topic: + GetEntityData(d2, storeId, languageId, x => null); + break; + default: + throw new SmartException("Unknown link builder type."); + } + + return d2; + }); + } var result = new LinkResolverResult { - Type = data.Type, - Status = data.Status, - Value = data.Value, - Link = data.Link, - Label = data.Label + Type = d.Type, + Status = d.Status, + Value = d.Value, + Link = d.Link, + QueryString = queryString, + Label = d.Label, + Id = d.Id, + PictureId = d.PictureId }; // Check ACL and limited to stores. - switch (data.Type) + switch (d.Type) { case LinkType.Product: case LinkType.Category: case LinkType.Manufacturer: case LinkType.Topic: - var entityName = data.Type.ToString(); - if (data.LimitedToStores && data.Status == LinkStatus.Ok && !QuerySettings.IgnoreMultiStore && !_storeMappingService.Authorize(entityName, data.Id, storeId)) + var entityName = d.Type.ToString(); + + if (d.CheckLimitedToStores && + d.LimitedToStores && + d.Status == LinkStatus.Ok && + !QuerySettings.IgnoreMultiStore && + !_storeMappingService.Authorize(entityName, d.Id, storeId)) { result.Status = LinkStatus.NotFound; } - else if (data.SubjectToAcl && data.Status == LinkStatus.Ok && !QuerySettings.IgnoreAcl && !_aclService.Authorize(entityName, data.Id, roles)) + else if (d.SubjectToAcl && + d.Status == LinkStatus.Ok && + !QuerySettings.IgnoreAcl && + !_aclService.Authorize(entityName, d.Id, roles)) { result.Status = LinkStatus.Forbidden; } @@ -155,36 +197,94 @@ 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; + } + + protected virtual string GetLocalized(int entityId, string localeKeyGroup, string localeKey, int languageId, string defaultValue) + { + return _localizedEntityService.GetLocalizedValue(languageId, entityId, localeKeyGroup, localeKey).NullEmpty() ?? defaultValue.NullEmpty(); + } + protected virtual LinkResolverData Parse(string linkExpression) { - if (!string.IsNullOrWhiteSpace(linkExpression)) - { - var index = linkExpression.IndexOf(':'); + 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) }; - if (index != -1 && Enum.TryParse(linkExpression.Substring(0, index), true, out LinkType type)) - { - var value = linkExpression.Substring(index + 1); + 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; + } - switch (type) - { - case LinkType.Product: - case LinkType.Category: - case LinkType.Manufacturer: - case LinkType.Topic: - var id = value.ToInt(); - return new LinkResolverData { Type = type, Value = id != 0 ? (object)id : value }; - case LinkType.Url: - case LinkType.File: - default: - return new LinkResolverData { Type = type, Value = value }; - } - } - } + 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 new LinkResolverData { Type = LinkType.Url, Value = linkExpression.EmptyNull() }; + return result; + } + } + + return new LinkResolverData { Type = LinkType.Url, Value = linkExpression.EmptyNull() }; } - internal void GetEntityData(LinkResolverData data, int languageId, Expression> selector) where T : BaseEntity + internal void GetEntityData( + LinkResolverData data, + int storeId, + int languageId, + Expression> selector) where T : BaseEntity { ResolverEntitySummary summary = null; string systemName = null; @@ -201,23 +301,33 @@ internal void GetEntityData(LinkResolverData data, int languageId, Expression if (data.Type == LinkType.Topic) { - var query = _services.DbContext.Set() - .AsNoTracking() - .AsQueryable(); + Topic topic = null; - query = string.IsNullOrEmpty(systemName) - ? query.Where(x => x.Id == data.Id) - : query.Where(x => x.SystemName == systemName); + if (string.IsNullOrEmpty(systemName)) + { + topic = _services.DbContext.Set() + .AsNoTracking() + .FirstOrDefault(x => x.Id == data.Id); + } + else + { + topic = _topicService.GetTopicBySystemName(systemName, storeId, false); + data.CheckLimitedToStores = false; + } - summary = query.Select(x => new ResolverEntitySummary + if (topic != null) { - Id = x.Id, - Name = x.SystemName, - Published = x.IsPublished, - SubjectToAcl = x.SubjectToAcl, - LimitedToStores = x.LimitedToStores - }) - .FirstOrDefault(); + summary = new ResolverEntitySummary + { + Id = topic.Id, + Name = topic.SystemName, + Title = topic.Title, + ShortTitle = topic.ShortTitle, + Published = topic.IsPublished, + SubjectToAcl = topic.SubjectToAcl, + LimitedToStores = topic.LimitedToStores + }; + } } else { @@ -235,25 +345,28 @@ internal void GetEntityData(LinkResolverData data, int languageId, Expression data.Id = summary.Id != 0 ? summary.Id : data.Id; data.SubjectToAcl = summary.SubjectToAcl; data.LimitedToStores = summary.LimitedToStores; + data.PictureId = summary.PictureId; data.Status = summary.Deleted ? LinkStatus.NotFound : summary.Published ? LinkStatus.Ok : LinkStatus.Hidden; - + if (data.Type == LinkType.Topic) { - data.Label = _localizedEntityService.GetLocalizedValue(languageId, data.Id, entityName, "ShortTitle").NullEmpty() ?? - _localizedEntityService.GetLocalizedValue(languageId, data.Id, entityName, "Title").NullEmpty() ?? - summary.Name; + data.Label = GetLocalized(data.Id, entityName, "ShortTitle", languageId, null) + ?? GetLocalized(data.Id, entityName, "Title", languageId, null) + ?? summary.ShortTitle.NullEmpty() + ?? summary.Title.NullEmpty() + ?? summary.Name; } else { - data.Label = _localizedEntityService.GetLocalizedValue(languageId, data.Id, entityName, "Name").NullEmpty() ?? summary.Name; + data.Label = GetLocalized(data.Id, entityName, "Name", languageId, summary.Name); } - var slug = _urlRecordService.GetActiveSlug(data.Id, entityName, languageId).NullEmpty() ?? _urlRecordService.GetActiveSlug(data.Id, entityName, 0); - if (!string.IsNullOrEmpty(slug)) + data.Slug = _urlRecordService.GetActiveSlug(data.Id, entityName, languageId).NullEmpty() ?? _urlRecordService.GetActiveSlug(data.Id, entityName, 0); + if (!string.IsNullOrEmpty(data.Slug)) { - data.Link = _urlHelper.RouteUrl(entityName, new { SeName = slug }); + data.Link = _urlHelper.RouteUrl(entityName, new { SeName = data.Slug }); } } else @@ -264,14 +377,16 @@ internal void GetEntityData(LinkResolverData data, int languageId, Expression } } - internal class ResolverEntitySummary { public int Id { get; set; } public string Name { get; set; } + public string Title { get; set; } + public string ShortTitle { get; set; } public bool Deleted { get; set; } public bool Published { get; set; } public bool SubjectToAcl { get; set; } public bool LimitedToStores { get; set; } + public int? PictureId { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/Cms/LinkResolverInvalidator.cs b/src/Libraries/SmartStore.Services/Cms/LinkResolverInvalidator.cs new file mode 100644 index 0000000000..58cda2a320 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/LinkResolverInvalidator.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Core; +using SmartStore.Core.Data; +using SmartStore.Core.Data.Hooks; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Seo; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Topics; +using SmartStore.Data; + +namespace SmartStore.Services.Cms +{ + 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), + nameof(Product.Name), + nameof(Product.Deleted), + nameof(Product.MainPictureId), + nameof(Category.Published), + nameof(Category.PictureId), + nameof(StoreMapping.StoreId), + nameof(UrlRecord.IsActive), + nameof(UrlRecord.Slug) + }; + + public LinkResolverInvalidator(ICommonServices services) + { + _services = services; + } + + 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)); + } + + if (!evict) + return; + + int? evictTopicId = null; + + 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; + } + + 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 0619f41693..71002103a9 100644 --- a/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs +++ b/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs @@ -20,29 +20,126 @@ public enum LinkStatus Hidden } - - public partial class LinkResolverResult + public class LinkResolverResult { - public LinkType Type { get; set; } - public object Value { get; set; } - public LinkStatus Status { get; set; } + private string _link; - public string Link { get; set; } - public string Label { get; set; } - } + /// + /// The raw expression without query string. + /// + public string Expression { get; set; } - [Serializable] - public partial class LinkResolverData - { - public LinkType Type { get; set; } + /// + /// The query string part. + /// + public string QueryString { get; set; } + + public LinkType Type { get; set; } public object Value { get; set; } - public LinkStatus Status { get; set; } - public string Link { get; set; } + public LinkStatus Status { get; set; } public string Label { get; set; } - public int Id { get; set; } + public int? PictureId { get; set; } + public string Slug { get; set; } + + public string Link + { + get + { + if (Type != LinkType.Url && !string.IsNullOrWhiteSpace(_link) && !string.IsNullOrWhiteSpace(QueryString)) + { + return string.Concat(_link, "?", QueryString); + } + + return _link; + } + set + { + _link = value; + + if (_link != null && Type != LinkType.Url) + { + var index = _link.IndexOf('?'); + if (index != -1) + { + QueryString = _link.Substring(index + 1); + _link = _link.Substring(0, index); + } + } + } + } + + public override string ToString() + { + return this.Link.EmptyNull(); + } + } + + [Serializable] + public partial class LinkResolverData : LinkResolverResult, ICloneable + { public bool SubjectToAcl { get; set; } public bool LimitedToStores { get; set; } + public bool CheckLimitedToStores { get; set; } = true; + + public LinkResolverData Clone() + { + return (LinkResolverData)this.MemberwiseClone(); + } + + object ICloneable.Clone() + { + return this.MemberwiseClone(); + } + } + + + public static class LinkResolverExtensions + { + public static (string Icon, string ResKey) GetLinkTypeInfo(this LinkType type) + { + switch (type) + { + case LinkType.Product: + return ("fa fa-cube", "Common.Entity.Product"); + case LinkType.Category: + return ("fa fa-sitemap", "Common.Entity.Category"); + case LinkType.Manufacturer: + return ("far fa-building", "Common.Entity.Manufacturer"); + case LinkType.Topic: + return ("far fa-file-alt", "Common.Entity.Topic"); + case LinkType.Url: + return ("fa fa-link", "Common.Url"); + case LinkType.File: + return ("far fa-folder-open", "Common.File"); + default: + throw new SmartException("Unknown link builder type."); + } + } + + /// + /// Creates the full link expression including type, value and query string. + /// + /// Whether to include the query string. + /// Link expression. + public static string CreateExpression(this LinkResolverResult data, bool includeQueryString = true) + { + if (data?.Value == null) + { + return string.Empty; + } + + var result = data.Type == LinkType.Url + ? data.Value.ToString() + : string.Concat(data.Type.ToString().ToLower(), ":", data.Value.ToString()); + + if (includeQueryString && data.Type != LinkType.Url && !string.IsNullOrWhiteSpace(data.QueryString)) + { + return string.Concat(result, "?", data.QueryString); + } + + return result; + } } } diff --git a/src/Libraries/SmartStore.Services/Cms/Menus/IMenuStorage.cs b/src/Libraries/SmartStore.Services/Cms/Menus/IMenuStorage.cs new file mode 100644 index 0000000000..6218e2c7b2 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Menus/IMenuStorage.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using SmartStore.Core; +using SmartStore.Core.Domain.Cms; +using SmartStore.Core.Domain.Customers; + +namespace SmartStore.Services.Cms +{ + public partial interface IMenuStorage + { + /// + /// Inserts a menu. + /// + /// Menu entity. + void InsertMenu(MenuRecord menu); + + /// + /// Updates a menu. + /// + /// Menu entity. + void UpdateMenu(MenuRecord menu); + + /// + /// Deletes a menu. + /// + /// Menu entity. + void DeleteMenu(MenuRecord menu); + + /// + /// Gets all menus. + /// + /// Menu system name. + /// Store identifier. 0 to get menus for all stores. + /// Whether to include hidden menus. + /// Page index. + /// Page size. + /// Menu entities. + IPagedList GetAllMenus( + string systemName = null, + int storeId = 0, + bool includeHidden = false, + int pageIndex = 0, + int pageSize = int.MaxValue); + + /// + /// Gets cached infos about all user menus. + /// + /// Customer roles to check access for. null to use current customer's roles. + /// Store identifier. 0 to use current store. + /// Menu infos. + IEnumerable GetUserMenuInfos(IEnumerable roles = null, int storeId = 0); + + /// + /// Gets a menu by identifier. + /// + /// Menu identifier. + /// Menu item entity. + MenuRecord GetMenuById(int id); + + /// + /// Checks whether the menu exists. + /// + /// Menu system name. + /// true the menu exists, false the menu doesn't exist. + bool MenuExists(string systemName); + + #region Menu items + + /// + /// Inserts a menu item. + /// + /// Menu item entity. + void InsertMenuItem(MenuItemRecord item); + + /// + /// Updates a menu item. + /// + /// Menu item entity. + void UpdateMenuItem(MenuItemRecord item); + + /// + /// Deletes a menu item. + /// + /// Menu item entity. + /// Whether to delete all child items too. + void DeleteMenuItem(MenuItemRecord item, bool deleteChilds = true); + + /// + /// Gets a menu item by identifier. + /// + /// Menu item identifier. + /// Menu item entity. + MenuItemRecord GetMenuItemById(int id); + + /// + /// Gets menu items. + /// + /// Menu item identifier. + /// Store identifier. 0 to get menus for all stores. + /// Whether to include hidden menus. + /// Menu item entities. + IList GetMenuItems(int menuId, int storeId = 0, bool includeHidden = false); + + /// + /// Gets menu items. + /// + /// Menu system name. + /// Store identifier. 0 to get menus for all stores. + /// Whether to include hidden menus. + /// Menu item entities. + IList GetMenuItems(string systemName, int storeId = 0, bool includeHidden = false); + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Services/Cms/Menus/MenuInfo.cs b/src/Libraries/SmartStore.Services/Cms/Menus/MenuInfo.cs new file mode 100644 index 0000000000..afabc97e8b --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Menus/MenuInfo.cs @@ -0,0 +1,11 @@ +namespace SmartStore.Services.Cms +{ + public class MenuInfo + { + public int Id { get; set; } + public string SystemName { get; set; } + public string Template { get; set; } + public string[] WidgetZones { get; set; } + public int DisplayOrder { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/Cms/Menus/MenuStorage.cs b/src/Libraries/SmartStore.Services/Cms/Menus/MenuStorage.cs new file mode 100644 index 0000000000..ff0332720c --- /dev/null +++ b/src/Libraries/SmartStore.Services/Cms/Menus/MenuStorage.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using SmartStore.Core; +using SmartStore.Core.Caching; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Cms; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Security; +using SmartStore.Core.Domain.Stores; +using SmartStore.Data.Caching; + +namespace SmartStore.Services.Cms +{ + public partial class MenuStorage : IMenuStorage + { + private const string MENU_ALLSYSTEMNAMEs_CACHE_KEY = "MenuStorage:SystemNames"; + private const string MENU_USER_CACHE_KEY = "MenuStorage:Menus:User-{0}-{1}"; + private const string MENU_PATTERN_KEY = "MenuStorage:Menus:*"; + + private readonly IRepository _menuRepository; + private readonly IRepository _menuItemRepository; + private readonly IRepository _storeMappingRepository; + private readonly IRepository _aclRepository; + private readonly ICommonServices _services; + + public MenuStorage( + IRepository menuRepository, + IRepository menuItemRepository, + IRepository storeMappingRepository, + IRepository aclRepository, + ICommonServices services) + { + _menuRepository = menuRepository; + _menuItemRepository = menuItemRepository; + _storeMappingRepository = storeMappingRepository; + _aclRepository = aclRepository; + _services = services; + + QuerySettings = DbQuerySettings.Default; + } + + public DbQuerySettings QuerySettings { get; set; } + + public virtual void InsertMenu(MenuRecord menu) + { + Guard.NotNull(menu, nameof(MenuRecord)); + + menu.SystemName = menu.SystemName.ToValidPath(); + + _menuRepository.Insert(menu); + + var systemNames = GetMenuSystemNames(false); + if (systemNames != null && menu.Published) + { + systemNames.Add(menu.SystemName); + } + + _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); + } + + public virtual void UpdateMenu(MenuRecord menu) + { + Guard.NotNull(menu, nameof(MenuRecord)); + + menu.SystemName = menu.SystemName.ToValidPath(); + + var modProps = _services.DbContext.GetModifiedProperties(menu); + + _menuRepository.Update(menu); + + var systemNames = GetMenuSystemNames(false); + if (systemNames != null) + { + if (modProps.TryGetValue(nameof(menu.Published), out var original)) + { + if (original.Convert() == true) + { + systemNames.Remove(menu.SystemName); + } + else + { + systemNames.Add(menu.SystemName); + } + } + else if (modProps.TryGetValue(nameof(menu.SystemName), out original)) + { + systemNames.Remove((string)original); + systemNames.Add(menu.SystemName); + } + } + + _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); + } + + public virtual void DeleteMenu(MenuRecord menu) + { + if (menu == null) + return; + + _menuRepository.Delete(menu); + + var systemNames = GetMenuSystemNames(false); + if (systemNames != null) + { + systemNames.Remove(menu.SystemName); + } + + _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); + } + + public virtual IPagedList GetAllMenus( + string systemName = null, + int storeId = 0, + bool includeHidden = false, + int pageIndex = 0, + int pageSize = int.MaxValue) + { + var query = BuildMenuQuery(0, storeId, systemName, null, includeHidden); + return new PagedList(query, pageIndex, pageSize); + } + + public virtual IEnumerable GetUserMenuInfos(IEnumerable roles = null, int storeId = 0) + { + if (roles == null) + { + roles = _services.WorkContext.CurrentCustomer.CustomerRoles; + } + + if (storeId == 0) + { + storeId = _services.StoreContext.CurrentStore.Id; + } + + var cacheKey = MENU_USER_CACHE_KEY.FormatInvariant(storeId, string.Join(",", roles.Where(x => x.Active).Select(x => x.Id))); + + var userMenusInfo = _services.Cache.Get(cacheKey, () => + { + var query = BuildMenuQuery(0, storeId, null, false, false, true, false, true); + + var data = query.Select(x => new + { + x.Id, + x.SystemName, + x.Template, + x.WidgetZone, + x.DisplayOrder + }) + .ToList(); + + var result = data.Select(x => new MenuInfo + { + Id = x.Id, + SystemName = x.SystemName, + Template = x.Template, + DisplayOrder = x.DisplayOrder, + WidgetZones = x.WidgetZone.EmptyNull().Trim() + .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(y => y.Trim()) + .ToArray() + }) + .ToList(); + + return result; + }); + + return userMenusInfo; + } + + public virtual MenuRecord GetMenuById(int id) + { + if (id == 0) + { + return null; + } + + return _menuRepository.GetByIdCached(id, "db.menurecord.id-" + id); + } + + public virtual bool MenuExists(string systemName) + { + if (systemName.IsEmpty()) + { + return false; + } + + return GetMenuSystemNames(true).Contains(systemName); + } + + #region Menu items + + public virtual void InsertMenuItem(MenuItemRecord item) + { + Guard.NotNull(item, nameof(MenuRecord)); + + // Prevent inconsistent tree structure. + if (item.ParentItemId != 0 && item.ParentItemId == item.Id) + { + item.ParentItemId = 0; + } + + _menuItemRepository.Insert(item); + + _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); + } + + public virtual void UpdateMenuItem(MenuItemRecord item) + { + Guard.NotNull(item, nameof(MenuRecord)); + + // Prevent inconsistent tree structure. + if (item.ParentItemId != 0 && item.ParentItemId == item.Id) + { + item.ParentItemId = 0; + } + + _menuItemRepository.Update(item); + + _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); + } + + public virtual void DeleteMenuItem(MenuItemRecord item, bool deleteChilds = true) + { + if (item == null) + { + return; + } + + if (!deleteChilds) + { + _menuItemRepository.Delete(item); + } + 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); + } + } + + _services.Cache.RemoveByPattern(MENU_PATTERN_KEY); + + void GetChildIds(int parentId, HashSet ids) + { + var childIds = _menuItemRepository.TableUntracked + .Where(x => x.ParentItemId == parentId) + .Select(x => x.Id) + .ToList(); + + if (childIds.Any()) + { + ids.AddRange(childIds); + childIds.Each(x => GetChildIds(x, ids)); + } + } + } + + public virtual MenuItemRecord GetMenuItemById(int id) + { + if (id == 0) + { + return null; + } + + return _menuItemRepository.GetByIdCached(id, "db.menuitemrecord.id-" + id); + } + + public virtual IList GetMenuItems(int menuId, int storeId = 0, bool includeHidden = false) + { + if (menuId == 0) + { + return new List(); + } + + var query = BuildMenuItemQuery(menuId, null, storeId, includeHidden); + return query.ToList(); + } + + public virtual IList GetMenuItems(string systemName, int storeId = 0, bool includeHidden = false) + { + if (systemName.IsEmpty()) + { + return new List(); + } + + var query = BuildMenuItemQuery(0, systemName, storeId, includeHidden); + return query.ToList(); + } + + #endregion + + #region Utilities + + private ISet GetMenuSystemNames(bool create) + { + if (create || _services.Cache.Contains(MENU_ALLSYSTEMNAMEs_CACHE_KEY)) + { + return _services.Cache.GetHashSet(MENU_ALLSYSTEMNAMEs_CACHE_KEY, () => + { + return _menuRepository.TableUntracked + .Where(x => x.Published) + .Select(x => x.SystemName) + .ToArray(); + }); + } + + return null; + } + + protected virtual IQueryable BuildMenuQuery( + int id, + int storeId, + string systemName, + bool? isSystemMenu, + bool includeHidden, + bool groupBy = true, + bool sort = true, + bool untracked = false) + { + var applied = false; + var entityName = nameof(MenuRecord); + var query = untracked ? _menuRepository.TableUntracked : _menuRepository.Table; + + query = query.Where(x => includeHidden || x.Published); + + if (id != 0) + { + query = query.Where(x => x.Id == id); + } + + if (systemName.HasValue()) + { + query = query.Where(x => x.SystemName == systemName); + } + + if (isSystemMenu.HasValue) + { + query = query.Where(x => x.IsSystemMenu == isSystemMenu.Value); + } + + if (storeId > 0 && !QuerySettings.IgnoreMultiStore) + { + 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 + from m in sm.DefaultIfEmpty() + where !x.LimitedToStores || storeId == m.StoreId + select x; + + applied = true; + } + + if (!includeHidden && !QuerySettings.IgnoreAcl) + { + var allowedRoleIds = _services.WorkContext.CurrentCustomer.CustomerRoles.Where(x => x.Active).Select(x => x.Id).ToList(); + + 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 + from a in ac.DefaultIfEmpty() + where !x.SubjectToAcl || allowedRoleIds.Contains(a.CustomerRoleId) + select x; + + applied = true; + } + + if (applied && groupBy) + { + query = + from x in query + group x by x.Id into grp + orderby grp.Key + select grp.FirstOrDefault(); + } + + if (sort) + { + query = query.OrderBy(x => x.SystemName).ThenBy(x => x.Title); + } + + return query; + } + + protected virtual IQueryable BuildMenuItemQuery( + int menuId, + string systemName, + int storeId, + bool includeHidden) + { + var singleMenu = menuId != 0 || (systemName.HasValue() && storeId != 0); + var menuQuery = BuildMenuQuery(menuId, storeId, systemName, null, includeHidden, !singleMenu, !singleMenu); + + if (singleMenu) + { + menuQuery = menuQuery.Take(1); + } + + var query = + from m in menuQuery + join mi in _menuItemRepository.Table on m.Id equals mi.MenuId + where includeHidden || mi.Published + orderby mi.ParentItemId, mi.DisplayOrder + select mi; + + return query; + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Services/Common/TempFileCleanupTask.cs b/src/Libraries/SmartStore.Services/Common/TempFileCleanupTask.cs index a07dfe8cce..e02429fba3 100644 --- a/src/Libraries/SmartStore.Services/Common/TempFileCleanupTask.cs +++ b/src/Libraries/SmartStore.Services/Common/TempFileCleanupTask.cs @@ -10,7 +10,7 @@ public partial class TempFileCleanupTask : ITask { public void Execute(TaskExecutionContext ctx) { - FileSystemHelper.TempCleanup(); + FileSystemHelper.ClearTempDirectories(); } } } diff --git a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs index 5eab6f9f14..1caeae24d0 100644 --- a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs +++ b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs @@ -57,7 +57,7 @@ protected virtual IDictionary GetAllCachedSettings() } return dictionary; - }); + }, independent: true); } protected virtual PropertyInfo GetPropertyInfo(Expression> keySelector) @@ -152,7 +152,7 @@ public virtual Setting GetSettingById(int settingId) public virtual T GetSettingByKey( string key, - T defaultValue = default(T), + T defaultValue = default, int storeId = 0, bool loadSharedValueIfNotFound = false) { @@ -162,9 +162,7 @@ public virtual T GetSettingByKey( var cacheKey = CreateCacheKey(key, storeId); - CachedSetting cachedSetting; - - if (settings.TryGetValue(cacheKey, out cachedSetting)) + if (settings.TryGetValue(cacheKey, out CachedSetting cachedSetting)) { return cachedSetting.Value.Convert(); } @@ -224,32 +222,33 @@ public ISettings LoadSetting(Type settingType, int storeId = 0) protected virtual ISettings LoadSettingCore(Type settingType, int storeId = 0) { - if (settingType.HasAttribute(true)) - { - return LoadSettingsJson(settingType, storeId); - } - - var settings = (ISettings)Activator.CreateInstance(settingType); - + 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 + // Get properties we can read and write to if (!prop.CanWrite) continue; - var key = prefix + "." + prop.Name; - // load by store - string setting = GetSettingByKey(key, storeId: storeId, loadSharedValueIfNotFound: true); + 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(settings) as IEnumerable) != null) + 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. @@ -294,7 +293,7 @@ protected virtual ISettings LoadSettingCore(Type settingType, int storeId = 0) object value = converter.ConvertFrom(setting); // Set property - fastProp.SetValue(settings, value); + fastProp.SetValue(instance, value); } catch (Exception ex) { @@ -303,7 +302,7 @@ protected virtual ISettings LoadSettingCore(Type settingType, int storeId = 0) } } - return settings; + return instance; } public virtual void SetSetting(string key, T value, int storeId = 0, bool clearCache = true) @@ -313,10 +312,9 @@ public virtual void SetSetting(string key, T value, int storeId = 0, bool cle var str = value.Convert(); var allSettings = GetAllCachedSettings(); var cacheKey = CreateCacheKey(key, storeId); - CachedSetting cachedSetting; var insert = false; - if (allSettings.TryGetValue(cacheKey, out cachedSetting)) + if (allSettings.TryGetValue(cacheKey, out CachedSetting cachedSetting)) { var setting = GetSettingById(cachedSetting.Id); if (setting != null) @@ -370,12 +368,6 @@ protected virtual void SaveSettingCore(ISettings settings, int storeId = 0) var settingType = settings.GetType(); var prefix = settingType.Name; - if (settingType.HasAttribute(true)) - { - SaveSettingsJson(settings); - return; - } - /* 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 */ @@ -446,12 +438,6 @@ public virtual void DeleteSetting(Setting setting) { using (BeginScope()) { - if (typeof(T).HasAttribute(true)) - { - DeleteSettingsJson(); - return; - } - var settingsToDelete = new List(); var allSettings = GetAllSettings(); foreach (var prop in typeof(T).GetProperties()) diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs index 3709c7a3e2..b6890718c9 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs @@ -20,6 +20,7 @@ using SmartStore.Data.Caching; using SmartStore.Services.Common; using SmartStore.Services.Localization; +using System.Threading.Tasks; namespace SmartStore.Services.Customers { @@ -528,7 +529,11 @@ public virtual void ResetCheckoutData( UpdateCustomer(customer); } - public virtual int DeleteGuestCustomers(DateTime? registrationFrom, DateTime? registrationTo, bool onlyWithoutShoppingCart, int maxItemsToDelete = 5000) + public virtual async Task DeleteGuestCustomersAsync( + DateTime? registrationFrom, + DateTime? registrationTo, + bool onlyWithoutShoppingCart, + int maxItemsToDelete = 5000) { var ctx = _customerRepository.Context; @@ -565,7 +570,7 @@ public virtual int DeleteGuestCustomers(DateTime? registrationFrom, DateTime? re // no forum topics query = JoinWith(query, x => x.CustomerId); - //don't delete system accounts + // don't delete system accounts query = query.Where(c => !c.IsSystemAccount); // only distinct items @@ -576,7 +581,7 @@ orderby cGroup.Key select cGroup.FirstOrDefault(); query = query.OrderBy(c => c.Id); - var customers = query.Take(() => maxItemsToDelete).ToList(); + var customers = await query.Take(() => maxItemsToDelete).ToListAsync(); int numberOfDeletedCustomers = 0; foreach (var c in customers) @@ -585,10 +590,9 @@ orderby cGroup.Key { // 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" + where ga.EntityId == c.Id && ga.KeyGroup == "Customer" select ga; - var attributes = gaQuery.ToList(); + var attributes = await gaQuery.ToListAsync(); _gaRepository.DeleteRange(attributes); @@ -601,7 +605,7 @@ orderby cGroup.Key // save changes all 1000th item try { - scope.Commit(); + await ctx.SaveChangesAsync(); } catch (Exception ex) { @@ -616,7 +620,7 @@ orderby cGroup.Key } // save the rest - scope.Commit(); + await ctx.SaveChangesAsync(); return numberOfDeletedCustomers; } diff --git a/src/Libraries/SmartStore.Services/Customers/DeleteGuestsTask.cs b/src/Libraries/SmartStore.Services/Customers/DeleteGuestsTask.cs index 35dbb85d43..1238009e0d 100644 --- a/src/Libraries/SmartStore.Services/Customers/DeleteGuestsTask.cs +++ b/src/Libraries/SmartStore.Services/Customers/DeleteGuestsTask.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using SmartStore.Services.Tasks; namespace SmartStore.Services.Customers @@ -6,23 +7,20 @@ namespace SmartStore.Services.Customers /// /// Represents a task for deleting guest customers /// - public partial class DeleteGuestsTask : ITask + public partial class DeleteGuestsTask : AsyncTask { private readonly ICustomerService _customerService; public DeleteGuestsTask(ICustomerService customerService) { - this._customerService = customerService; + _customerService = customerService; } - /// - /// Executes a task - /// - public void Execute(TaskExecutionContext ctx) - { - //60*24 = 1 day - var olderThanMinutes = 1440; // TODO: move to settings - _customerService.DeleteGuestCustomers(null, DateTime.UtcNow.AddMinutes(-olderThanMinutes), true); - } - } + 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); + } + } } diff --git a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs index 7921eb281d..5736bf5fa0 100644 --- a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; @@ -140,14 +141,14 @@ void ResetCheckoutData(Customer customer, int storeId, bool clearPaymentMethod = true, 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 - int DeleteGuestCustomers(DateTime? registrationFrom, DateTime? registrationTo, bool onlyWithoutShoppingCart, int maxItemsToDelete = 5000); + /// + /// 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); #endregion diff --git a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs index 62836732ce..f806f232fe 100644 --- a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs +++ b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.IO; using System.Linq; using SmartStore.Core.Async; @@ -10,10 +9,10 @@ using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Forums; using SmartStore.Core.Domain.Media; -using SmartStore.Core.Events; using SmartStore.Services.Affiliates; using SmartStore.Services.Common; using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.DataExchange.Import.Events; using SmartStore.Services.Directory; using SmartStore.Services.Helpers; using SmartStore.Services.Media; @@ -22,7 +21,7 @@ namespace SmartStore.Services.Customers.Importer { - public class CustomerImporter : EntityImporterBase + public class CustomerImporter : EntityImporterBase { private const string _attributeKeyGroup = "Customer"; @@ -195,7 +194,9 @@ protected override void Import(ImportExecuteContext context) { _services.DbContext.AutoDetectChangesEnabled = false; } - } + + context.Services.EventPublisher.Publish(new ImportBatchExecutedEvent(context, batch)); + } } } @@ -581,7 +582,7 @@ protected virtual int ProcessAvatars( var currentPictures = new List(); var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); - var image = CreateDownloadImage(urlOrPath, seoName, 1); + var image = CreateDownloadImage(context, urlOrPath, seoName, 1); if (image == null) continue; diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index 811dfa2abe..6d1b16defc 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -333,7 +333,7 @@ private void StreamToFile(DataExporterContext ctx, Stream stream, string path, A if (ctx.ExecuteContext.Abort == DataExchangeAbortion.Hard && ctx.IsFileBasedExport && path.HasValue()) { - FileSystemHelper.Delete(path); + FileSystemHelper.DeleteFile(path); } } @@ -1786,8 +1786,8 @@ private void ExportCoreOuter(DataExporterContext ctx) var logPath = profile.GetExportLogPath(); var zipPath = profile.GetExportZipPath(); - FileSystemHelper.Delete(logPath); - FileSystemHelper.Delete(zipPath); + FileSystemHelper.DeleteFile(logPath); + FileSystemHelper.DeleteFile(zipPath); FileSystemHelper.ClearDirectory(ctx.FolderContent, false); using (var logger = new TraceLogger(logPath)) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index 7258f5d6c9..8c89087b72 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -22,6 +22,7 @@ using SmartStore.Core.Domain.Seo; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Tax; using SmartStore.Core.Html; using SmartStore.Services.Catalog; using SmartStore.Services.Catalog.Modelling; @@ -31,6 +32,7 @@ using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Services.Seo; +using SmartStore.Utilities; namespace SmartStore.Services.DataExchange.Export { @@ -106,7 +108,7 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec string[] appendText = ctx.Projection.AppendDescriptionText.SplitSafe(","); if (appendText.Length > 0) { - var rnd = new Random().Next(0, appendText.Length - 1); + var rnd = CommonHelper.GenerateRandomInteger(0, appendText.Length - 1); description = description.Grow(appendText.SafeGet(rnd), " "); } } @@ -459,10 +461,12 @@ private dynamic ToDynamic(DataExporterContext ctx, QuantityUnit quantityUnit) var translations = ctx.Translations[nameof(QuantityUnit)]; result.Name = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.Name)) ?? quantityUnit.Name; - result.Description = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.Description)) ?? quantityUnit.Description; + 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, + x => x.NamePlural, x => x.Description); return result; @@ -666,12 +670,28 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product, string seNam result._ProductTemplateViewPath = ctx.ProductTemplates.ContainsKey(product.ProductTemplateId) ? ctx.ProductTemplates[product.ProductTemplateId] - : ""; + : string.Empty; + + // A standard, language independent base price formatting: / + var basePriceInfo = string.Empty; + + if (product.BasePriceHasValue && product.BasePriceAmount != decimal.Zero) + { + var finalPrice = _priceCalculationService.Value.GetFinalPrice(product, ctx.ContextCustomer, true); + var productPrice = _taxService.Value.GetProductPrice(product, finalPrice, ctx.ContextCustomer, ctx.ContextCurrency, out var taxrate); + productPrice = _currencyService.Value.ConvertFromPrimaryStoreCurrency(productPrice, ctx.ContextCurrency); + + var priceIncludesTax = _services.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax; + var priceTemplate = _services.Localization.GetResource("Products.BasePriceInfo.LanguageInsensitive"); + var formattedPriceBase = System.Convert.ToDecimal((productPrice / product.BasePriceAmount) * product.BasePriceBaseAmount); + var formattedPrice = _priceFormatter.Value.FormatPrice(formattedPriceBase, true, ctx.ContextCurrency, ctx.ContextLanguage, priceIncludesTax, false); + + basePriceInfo = priceTemplate.FormatInvariant(formattedPrice, product.BasePriceBaseAmount, product.BasePriceMeasureUnit); + } - result._BasePriceInfo = product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, - _priceCalculationService.Value, ctx.ContextCustomer, ctx.ContextCurrency, decimal.Zero, true); + result._BasePriceInfo = basePriceInfo; - ToDeliveryTime(ctx, result, product.DeliveryTimeId); + ToDeliveryTime(ctx, result, product.DeliveryTimeId); ToQuantityUnit(ctx, result, product.QuantityUnitId); result._Localized = GetLocalized(ctx, translations, urlRecords, product, diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs index 8c5f870ab2..4c9fd6fdb1 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs @@ -140,11 +140,11 @@ public static string ResolveFileNamePattern(this ExportProfile profile, Store st if (profile.FileNamePattern.Contains("%Profile.SeoName%")) { - sb.Replace("%Profile.SeoName%", SeoHelper.GetSeName(profile.Name, true, false).Replace("/", "").Replace("-", "")); + sb.Replace("%Profile.SeoName%", SeoHelper.GetSeName(profile.Name, true, false, false).Replace("-", "")); } if (profile.FileNamePattern.Contains("%Store.SeoName%")) { - sb.Replace("%Store.SeoName%", profile.PerStore ? SeoHelper.GetSeName(store.Name, true, false) : "allstores"); + sb.Replace("%Store.SeoName%", profile.PerStore ? SeoHelper.GetSeName(store.Name, true, false, true) : "allstores"); } if (profile.FileNamePattern.Contains("%Random.Number%")) { diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs index 0d003e9497..e3a4f541b6 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs @@ -148,7 +148,7 @@ public virtual ExportProfile InsertExportProfile( .Replace("/", "") .Replace("-", ""); - var folderName = SeoHelper.GetSeName(cleanedSystemName, true, false) + var folderName = SeoHelper.GetSeName(cleanedSystemName, true, false, false) .ToValidPath() .Truncate(_dataExchangeSettings.MaxFileNameLength); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs index df53b4cb3f..4af3ac1836 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs @@ -318,7 +318,8 @@ public void WriteQuantityUnit(dynamic quantityUnit, string node) _writer.Write("Id", entity.Id.ToString()); _writer.Write("Name", (string)quantityUnit.Name); - _writer.Write("Description", (string)quantityUnit.Description); + _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()); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs index a464efaf8a..e413acdde9 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs @@ -257,7 +257,7 @@ private void ImportCoreOuter(DataImporterContext ctx) var customer = _services.WorkContext.CurrentCustomer; var profile = ctx.Request.Profile; var logPath = profile.GetImportLogPath(); - FileSystemHelper.Delete(logPath); + FileSystemHelper.DeleteFile(logPath); using (var logger = new TraceLogger(logPath)) { diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs index f4c963a045..4452f00382 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs @@ -86,7 +86,7 @@ protected void Initialize(ImportExecuteContext context) context.Result.TotalRecords = context.DataSegmenter.TotalRows; } - public FileDownloadManagerItem CreateDownloadImage(string urlOrPath, string seoName, int displayOrder) + public FileDownloadManagerItem CreateDownloadImage(ImportExecuteContext context, string urlOrPath, string seoName, int displayOrder) { var item = new FileDownloadManagerItem { @@ -102,10 +102,10 @@ public FileDownloadManagerItem CreateDownloadImage(string urlOrPath, string seoN if (urlOrPath.IsWebUrl()) { - item.Url = urlOrPath; - item.FileName = "{0}-{1}".FormatInvariant(seoName, item.Id).ToValidFileName(); + item.Url = context.Services.WebHelper.ModifyQueryString(urlOrPath, "q=100", null); + item.FileName = "{0}-{1}".FormatInvariant(seoName, item.Id).ToValidFileName(); - if (DownloadedItems.ContainsKey(urlOrPath)) + if (DownloadedItems.ContainsKey(urlOrPath)) { item.Path = Path.Combine(ImageDownloadFolder, DownloadedItems[urlOrPath]); item.Success = true; diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportBatchExecutedEvent.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportBatchExecutedEvent.cs new file mode 100644 index 0000000000..b903b505cc --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/Events/ImportBatchExecutedEvent.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using SmartStore.Core; + +namespace SmartStore.Services.DataExchange.Import.Events +{ + /// + /// An event that is fired after an import of a data batch. + /// + /// The entity type to be imported. + public class ImportBatchExecutedEvent where TEntity : BaseEntity + { + public ImportBatchExecutedEvent(ImportExecuteContext context, IEnumerable> batch) + { + Guard.NotNull(context, nameof(context)); + Guard.NotNull(batch, nameof(batch)); + + Context = context; + Batch = batch; + } + + /// + /// Context of the import. + /// + public ImportExecuteContext Context { get; private set; } + + /// + /// Current batch of import data. + /// + public IEnumerable> Batch { get; private set; } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs index fbded791c7..da6ad91076 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs @@ -223,7 +223,7 @@ public virtual ImportProfile InsertImportProfile(string fileName, string name, I profile.KeyFieldNames = string.Join(",", keyFieldNames); - profile.FolderName = SeoHelper.GetSeName(name, true, false) + profile.FolderName = SeoHelper.GetSeName(name, true, false, false) .ToValidPath() .Truncate(_dataExchangeSettings.MaxFileNameLength); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs index a8a29366f7..a0de84eae2 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs @@ -171,26 +171,26 @@ public string LastError object ICloneable.Clone() { - return this.Clone(); + return this.MemberwiseClone(); } public SerializableImportResult Clone() { - var result = new SerializableImportResult(); - result.StartDateUtc = StartDateUtc; - result.EndDateUtc = EndDateUtc; - result.TotalRecords = TotalRecords; - result.SkippedRecords = SkippedRecords; - result.NewRecords = NewRecords; - result.ModifiedRecords = ModifiedRecords; - result.AffectedRecords = AffectedRecords; - result.Cancelled = Cancelled; - result.Warnings = Warnings; - result.Errors = Errors; - result.LastError = LastError; - - return result; - } + var result = new SerializableImportResult(); + result.StartDateUtc = StartDateUtc; + result.EndDateUtc = EndDateUtc; + result.TotalRecords = TotalRecords; + result.SkippedRecords = SkippedRecords; + result.NewRecords = NewRecords; + result.ModifiedRecords = ModifiedRecords; + result.AffectedRecords = AffectedRecords; + result.Cancelled = Cancelled; + result.Warnings = Warnings; + result.Errors = Errors; + result.LastError = LastError; + + return result; + } } diff --git a/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs b/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs index 497cd4a8a7..71ec189d47 100644 --- a/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs +++ b/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs @@ -1,123 +1,58 @@ using System; using System.Net; +using MaxMind.GeoIP2; using SmartStore.Core; using SmartStore.Core.Caching; +using SmartStore.Utilities; using SmDir = SmartStore.Core.Domain.Directory; namespace SmartStore.Services.Directory { - public partial class GeoCountryLookup : IGeoCountryLookup + public partial class GeoCountryLookup : DisposableObject, IGeoCountryLookup { - private readonly IWebHelper _webHelper; - private readonly ICountryService _countryService; - private readonly IRequestCache _requestCache; - private readonly ICacheManager _cache; + private readonly DatabaseReader _reader; + private readonly object _lock = new object(); - public GeoCountryLookup(IWebHelper webHelper, IRequestCache requestCache, ICacheManager cache, ICountryService countryService) + public GeoCountryLookup() { - this._webHelper = webHelper; - this._requestCache = requestCache; - this._cache = cache; - this._countryService = countryService; + _reader = new DatabaseReader(CommonHelper.MapPath("~/App_Data/GeoLite2/GeoLite2-Country.mmdb")); } - private MaxMind.GeoIP.LookupService GetLookupService() - { - return _cache.Get("GeoCountryLookup", () => - { - var lookupService = new MaxMind.GeoIP.LookupService(_webHelper.MapPath("~/App_Data/GeoIP.dat")); - return lookupService; - }); - } - - public virtual string LookupCountryCode(string str) + public LookupCountryResponse LookupCountry(string addr) { - if (String.IsNullOrEmpty(str)) - return string.Empty; - - IPAddress addr; - try + if (addr.HasValue() && IPAddress.TryParse(addr, out var ipAddress)) { - addr = IPAddress.Parse(str); + return LookupCountry(ipAddress); } - catch - { - return string.Empty; - } - return LookupCountryCode(addr); - } - public virtual string LookupCountryCode(IPAddress addr) - { - try - { - var lookupService = GetLookupService(); - var country = lookupService.getCountry(addr); - var code = country.getCode(); - if (code == "--") - return string.Empty; - - return code; - } - catch - { - return string.Empty; - } + return null; } - public virtual string LookupCountryName(string str) + public LookupCountryResponse LookupCountry(IPAddress addr) { - if (String.IsNullOrEmpty(str)) - return string.Empty; + Guard.NotNull(addr, nameof(addr)); - IPAddress addr; - try + if (_reader.TryCountry(addr, out var response) && response.Country != null) { - addr = IPAddress.Parse(str); + var country = response.Country; + return new LookupCountryResponse + { + GeoNameId = country.GeoNameId, + IsoCode = country.IsoCode, + Name = country.Name, + IsInEu = country.IsInEuropeanUnion + }; } - catch - { - return string.Empty; - } - return LookupCountryName(addr); + + return null; } - public virtual string LookupCountryName(IPAddress addr) + protected override void OnDispose(bool disposing) { - try - { - var lookupService = GetLookupService(); - var country = lookupService.getCountry(addr); - return country.getName(); - } - catch - { - return string.Empty; - } + if (disposing && _reader != null) + { + _reader.Dispose(); + } } - - public virtual bool IsEuIpAddress(string ipAddress, out SmDir.Country euCountry) - { - euCountry = null; - - if (ipAddress.IsEmpty()) - return false; - - euCountry = _requestCache.Get("GeoCountryLookup.EuCountry.{0}".FormatInvariant(ipAddress), () => - { - var countryCode = LookupCountryCode(ipAddress); - if (countryCode.IsEmpty()) - return (SmDir.Country)null; - - var country = _countryService.GetCountryByTwoLetterIsoCode(countryCode); - return country; - }); - - if (euCountry == null) - return false; - - return euCountry.SubjectToVat; - } - } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/IGeoCountryLookup.cs b/src/Libraries/SmartStore.Services/Directory/IGeoCountryLookup.cs index 815a030b2c..cfe4cae186 100644 --- a/src/Libraries/SmartStore.Services/Directory/IGeoCountryLookup.cs +++ b/src/Libraries/SmartStore.Services/Directory/IGeoCountryLookup.cs @@ -3,27 +3,36 @@ namespace SmartStore.Services.Directory { - /// - /// Country lookup helper - /// - public partial interface IGeoCountryLookup + public sealed class LookupCountryResponse { - string LookupCountryCode(string str); + /// + /// The GeoName ID for the country. + /// + public int? GeoNameId { get; set; } - string LookupCountryCode(IPAddress addr); + /// + /// The english name of the country. + /// + public string Name { get; set; } - string LookupCountryName(string str); + /// + /// The two-letter ISO 3166-1 alpha code for the country + /// + public string IsoCode { get; set; } + + /// + /// This is true if the country is a member state of the European Union. + /// + public bool IsInEu { get; set; } + } - string LookupCountryName(IPAddress addr); + /// + /// Country lookup helper for IPv4/6 addresses + /// + public partial interface IGeoCountryLookup + { + LookupCountryResponse LookupCountry(string addr); - /// - /// Gets a value indicating whether the given IP address originates from an EU country - /// - /// IP address - /// An instance of if the IP originates from a EU country - /// - /// true if the IP address originates from an EU country, false if not - /// - bool IsEuIpAddress(string ipAddress, out Country euCountry); + LookupCountryResponse LookupCountry(IPAddress addr); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/MeasureService.cs b/src/Libraries/SmartStore.Services/Directory/MeasureService.cs index 1e8c052807..1ff51e67a9 100644 --- a/src/Libraries/SmartStore.Services/Directory/MeasureService.cs +++ b/src/Libraries/SmartStore.Services/Directory/MeasureService.cs @@ -150,14 +150,14 @@ public virtual MeasureWeight GetMeasureWeightById(int measureWeightId) public virtual MeasureWeight GetMeasureWeightBySystemKeyword(string systemKeyword) { - if (String.IsNullOrEmpty(systemKeyword)) + if (!systemKeyword.HasValue()) return null; var measureWeights = GetAllMeasureWeights(); - foreach (var measureWeight in measureWeights) - if (measureWeight.SystemKeyword.ToLowerInvariant() == systemKeyword.ToLowerInvariant()) - return measureWeight; - return null; + + return measureWeights + .Where(x => x.SystemKeyword.ToLowerInvariant() == systemKeyword.ToLowerInvariant()) + .FirstOrDefault(); } public virtual IList GetAllMeasureWeights() @@ -186,8 +186,7 @@ public virtual void UpdateMeasureWeight(MeasureWeight measure) _measureWeightRepository.Update(measure); } - public virtual decimal ConvertWeight(decimal quantity, - MeasureWeight sourceMeasureWeight, MeasureWeight targetMeasureWeight, bool round = true) + public virtual decimal ConvertWeight(decimal quantity, MeasureWeight sourceMeasureWeight, MeasureWeight targetMeasureWeight, bool round = true) { decimal result = quantity; if (result != decimal.Zero && sourceMeasureWeight.Id != targetMeasureWeight.Id) @@ -195,8 +194,10 @@ public virtual decimal ConvertWeight(decimal quantity, result = ConvertToPrimaryMeasureWeight(result, sourceMeasureWeight); result = ConvertFromPrimaryMeasureWeight(result, targetMeasureWeight); } + if (round) result = Math.Round(result, 2); + return result; } @@ -214,11 +215,11 @@ public virtual decimal ConvertToPrimaryMeasureWeight(decimal quantity, MeasureWe return result; } - public virtual decimal ConvertFromPrimaryMeasureWeight(decimal quantity, - MeasureWeight targetMeasureWeight) + public virtual decimal ConvertFromPrimaryMeasureWeight(decimal quantity, MeasureWeight targetMeasureWeight) { decimal result = quantity; var baseWeightIn = GetMeasureWeightById(_measureSettings.BaseWeightId); + if (result != decimal.Zero && targetMeasureWeight.Id != baseWeightIn.Id) { decimal exchangeRatio = targetMeasureWeight.Ratio; @@ -226,6 +227,7 @@ public virtual decimal ConvertFromPrimaryMeasureWeight(decimal quantity, throw new SmartException(string.Format("Exchange ratio not set for weight [{0}]", targetMeasureWeight.Name)); result = result * exchangeRatio; } + return result; } diff --git a/src/Libraries/SmartStore.Services/Events/ConsumerRegistry.cs b/src/Libraries/SmartStore.Services/Events/ConsumerRegistry.cs new file mode 100644 index 0000000000..3f89138f56 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Events/ConsumerRegistry.cs @@ -0,0 +1,126 @@ +using System; +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(); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs b/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs new file mode 100644 index 0000000000..ebab9eff71 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs @@ -0,0 +1,73 @@ +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; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.Infrastructure.DependencyManagement; +using SmartStore.Core.Plugins; +using SmartStore.Services; +using SmartStore.Services.Configuration; + +namespace SmartStore.Services.Events +{ + public class ConsumerResolver : IConsumerResolver + { + private readonly Work _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); + } + + return null; + } + + 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; + } + + if (storeId == 0) + { + return true; + } + + var settingService = _container.Value.Resolve(); + + 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; + } + + return true; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Events/DefaultConsumerFactory.cs b/src/Libraries/SmartStore.Services/Events/DefaultConsumerFactory.cs deleted file mode 100644 index 72382fd1c7..0000000000 --- a/src/Libraries/SmartStore.Services/Events/DefaultConsumerFactory.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; -using SmartStore.Core.Events; -using SmartStore.Core.Plugins; - -namespace SmartStore.Services.Events -{ - public class DefaultConsumerFactory : IConsumerFactory - { - private readonly IEnumerable, EventConsumerMetadata>> _consumers; - private readonly ICommonServices _services; - - public DefaultConsumerFactory( - IEnumerable, EventConsumerMetadata>> consumers, - ICommonServices services) - { - _consumers = consumers; - _services = services; - } - - public IEnumerable> GetConsumers(bool? resolveAsyncs = null) - { - foreach (var consumer in _consumers) - { - var isActive = consumer.Metadata.IsActive; - var isAsync = consumer.Metadata.ExecuteAsync; - var pluginDescriptor = consumer.Metadata.PluginDescriptor; - - if (isActive && (resolveAsyncs == null || (resolveAsyncs.Value == isAsync))) - { - if (pluginDescriptor == null || IsActiveForStore(pluginDescriptor, _services.StoreContext.CurrentStore.Id)) - { - yield return consumer.Value; - } - } - } - } - - - public bool HasAsyncConsumer - { - get - { - return _consumers.Any(c => c.Metadata.IsActive == true && c.Metadata.ExecuteAsync == true); - } - } - - 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/Libraries/SmartStore.Services/Events/EventPublisher.cs b/src/Libraries/SmartStore.Services/Events/EventPublisher.cs index eddb7b5a9e..e6529d9469 100644 --- a/src/Libraries/SmartStore.Services/Events/EventPublisher.cs +++ b/src/Libraries/SmartStore.Services/Events/EventPublisher.cs @@ -1,102 +1,51 @@ using System; -using System.Linq; using System.Collections.Generic; -using System.Collections.Concurrent; -using System.Threading; -using SmartStore.Core.Infrastructure; -using SmartStore.Core.Logging; -using SmartStore.Core.Async; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SmartStore.ComponentModel; using SmartStore.Core.Events; -using Autofac; +using SmartStore.Core.Logging; namespace SmartStore.Services.Events { - public class EventPublisher : IEventPublisher - { - private readonly ConcurrentDictionary _queue = new ConcurrentDictionary(); + public class EventPublisher : IEventPublisher + { + private readonly IConsumerRegistry _registry; + private readonly IConsumerResolver _resolver; + private readonly IConsumerInvoker _invoker; - public EventPublisher() + 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 eventMessage) - { - if (eventMessage != null) - { - // Enable event throttling by allowing the very same event to be published only all 150 ms. - Timer timer; - if (_queue.TryGetValue(eventMessage, out timer)) - { - // do nothing. The same event was published a tick ago. - return; - } - - _queue[eventMessage] = new Timer(RemoveFromQueue, eventMessage, 150, Timeout.Infinite); - } - - var consumerFactory = EngineContext.Current.Resolve>(); - if (consumerFactory == null) - return; - - IEnumerable> consumers = null; - - // first fire/forget all async consumers - if (consumerFactory.HasAsyncConsumer) - { - AsyncRunner.Run((c, ct) => - { - // for wiring up dependencies correctly - var newFactory = c.Resolve>(); - consumers = newFactory.GetConsumers(true).ToArray(); - foreach (var consumer in consumers) - { - consumer.HandleEvent(eventMessage); - } - }).ContinueWith(t => - { - if (t.IsFaulted) - { - var ex = t.Exception; - if (ex != null) - { - ex.InnerExceptions.Each(x => Logger.Error(x)); - } - } - }); - } + public void Publish(T message) where T : class + { + var descriptors = _registry.GetConsumers(message); - // now execute all sync consumers - consumers = consumerFactory.GetConsumers(false).ToArray(); - foreach (var consumer in consumers) + if (!descriptors.Any()) { - PublishEvent(consumer, eventMessage); + return; } - } - private void PublishEvent(IConsumer x, T eventMessage) - { - try - { - x.HandleEvent(eventMessage); - } - catch (Exception ex) - { - Logger.Error(ex); - throw; - } - } + var envelopeType = typeof(ConsumeContext<>).MakeGenericType(typeof(T)); + var envelope = (ConsumeContext)FastActivator.CreateInstance(envelopeType, message); - private void RemoveFromQueue(object eventMessage) - { - Timer timer; - if (_queue.TryRemove(eventMessage, out timer)) + foreach (var descriptor in descriptors) { - timer.Dispose(); + var consumer = _resolver.Resolve(descriptor); + if (consumer != null) + { + _invoker.Invoke(descriptor, consumer, envelope); + } } } - - } + } } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs index 60a56d3b8e..d85eafb668 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs @@ -319,7 +319,7 @@ public virtual void ImportPluginResourcesFromXml( bool updateTouchedResources = true, IList filterLanguages = null) { - var directory = new DirectoryInfo(Path.Combine(pluginDescriptor.OriginalAssemblyFile.Directory.FullName, "Localization")); + var directory = new DirectoryInfo(Path.Combine(pluginDescriptor.Assembly.OriginalFile.Directory.FullName, "Localization")); if (!directory.Exists) return; diff --git a/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs b/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs index 613192513a..8761739ab8 100644 --- a/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs +++ b/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs @@ -13,6 +13,7 @@ namespace SmartStore.Services.Media public class CachedImageResult { private bool? _exists; + private string _mimeType; public CachedImageResult(IFile file) { @@ -59,11 +60,19 @@ public long FileSize /// The file extension (without 'dot') /// public string Extension { get; set; } - - /// - /// The path relative to the cache root folder - /// - public string Path { 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 diff --git a/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs b/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs index 3e94172aec..1c5e8dc893 100644 --- a/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs +++ b/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs @@ -9,6 +9,7 @@ using SmartStore.Core.Logging; using ImageProcessor.Configuration; using SmartStore.Core.Events; +using SmartStore.Utilities; namespace SmartStore.Services.Media { @@ -48,6 +49,7 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) ValidateQuery(query); var watch = new Stopwatch(); + byte[] inBuffer = null; try { @@ -58,28 +60,36 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) var source = query.Source; // Load source - if (source is byte[]) + if (source is byte[] b) { - processor.Load((byte[])source); + inBuffer = b; } - else if (source is Stream) + else if (source is Stream s) { - processor.Load((Stream)source); + inBuffer = s.ToByteArray(); } - else if (source is Image) + else if (source is Image img) { - processor.Load((Image)source); + processor.Load(img); } - else if (source is string) + else if (source is string str) { - // TODO: (mc) map virtual pathes - processor.Load((string)source); + var path = NormalizePath(str); + using (var fs = File.OpenRead(path)) + { + 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)); @@ -87,25 +97,60 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) { Query = query, SourceWidth = processor.Image.Width, - SourceHeight = processor.Image.Height + SourceHeight = processor.Image.Height, + SourceMimeType = processor.CurrentImageFormat.MimeType }; // Core processing - ProcessImageCore(query, processor); + 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; - result.FileExtension = processor.CurrentImageFormat.DefaultExtension; - result.MimeType = processor.CurrentImageFormat.MimeType; - result.OutputStream = outStream; + + 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.GetBuffer().LongLength) + { + // 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; @@ -119,9 +164,9 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) } finally { - if (query.DisposeSource && query.Source is IDisposable) + if (query.DisposeSource && query.Source is IDisposable source) { - ((IDisposable)query.Source).Dispose(); + source.Dispose(); } watch.Stop(); @@ -134,8 +179,14 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) /// /// Query /// Processor instance - protected virtual void ProcessImageCore(ProcessImageQuery query, ImageFactory processor) + /// + /// 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) @@ -143,13 +194,17 @@ protected virtual void ProcessImageCore(ProcessImageQuery query, ImageFactory pr if (!size.IsEmpty) { - var scaleMode = ConvertScaleMode(query.ScaleMode); - processor.Resize(new ResizeLayer(size, resizeMode: scaleMode, upscale: false)); + 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 @@ -188,6 +243,21 @@ protected virtual void ProcessImageCore(ProcessImageQuery query, ImageFactory pr } } + 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) @@ -219,5 +289,30 @@ private ResizeMode ConvertScaleMode(string mode) 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/ImageCache.cs b/src/Libraries/SmartStore.Services/Media/ImageCache.cs index b6f005e920..68c98f8926 100644 --- a/src/Libraries/SmartStore.Services/Media/ImageCache.cs +++ b/src/Libraries/SmartStore.Services/Media/ImageCache.cs @@ -19,21 +19,12 @@ public class ImageCache : IImageCache private readonly MediaSettings _mediaSettings; private readonly string _thumbsRootDir; - private readonly IStoreContext _storeContext; - private readonly HttpContextBase _httpContext; private readonly IMediaFileSystem _fileSystem; private readonly IImageProcessor _imageProcessor; - public ImageCache( - MediaSettings mediaSettings, - IStoreContext storeContext, - HttpContextBase httpContext, - IMediaFileSystem fileSystem, - IImageProcessor imageProcessor) + public ImageCache(MediaSettings mediaSettings, IMediaFileSystem fileSystem, IImageProcessor imageProcessor) { _mediaSettings = mediaSettings; - _storeContext = storeContext; - _httpContext = httpContext; _fileSystem = fileSystem; _imageProcessor = imageProcessor; @@ -61,25 +52,19 @@ public void Put(CachedImageResult cachedImage, byte[] buffer) } } - public Task PutAsync(CachedImageResult cachedImage, byte[] buffer) + public async Task PutAsync(CachedImageResult cachedImage, byte[] buffer) { if (PreparePut(cachedImage, buffer)) { var path = BuildPath(cachedImage.Path); // save file - var t = _fileSystem.WriteAllBytesAsync(path, buffer); - t.ContinueWith(x => - { - // Refresh info - cachedImage.Exists = true; - cachedImage.File = _fileSystem.GetFile(path); - }); - - return t; - } + await _fileSystem.WriteAllBytesAsync(path, buffer); - return Task.FromResult(false); + // Refresh info + cachedImage.Exists = true; + cachedImage.File = _fileSystem.GetFile(path); + } } private bool PreparePut(CachedImageResult cachedImage, byte[] buffer) @@ -202,7 +187,7 @@ public virtual void Delete(IFile file) public virtual void Clear() { - for (int i = 0; i < 10; i++) + for (int i = 0; i < 10; i++) { try { diff --git a/src/Libraries/SmartStore.Services/Media/PictureService.cs b/src/Libraries/SmartStore.Services/Media/PictureService.cs index 850ea22bdb..9e421212eb 100644 --- a/src/Libraries/SmartStore.Services/Media/PictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/PictureService.cs @@ -154,8 +154,7 @@ protected virtual string GetFallbackImageFileName(FallbackPictureType defaultPic public virtual byte[] ValidatePicture(byte[] pictureBinary, string mimeType) { - var size = Size.Empty; - return ValidatePicture(pictureBinary, mimeType, out size); + return ValidatePicture(pictureBinary, mimeType, out _); } public virtual byte[] ValidatePicture(byte[] pictureBinary, string mimeType, out Size size) @@ -275,7 +274,7 @@ public IDictionary GetPictureInfos(IEnumerable pictureIds { PictureId = id, CacheKey = cacheKey, - Info = _cacheManager.Contains(cacheKey) ? _cacheManager.Get(cacheKey) : (PictureInfo)null + Info = _cacheManager.Contains(cacheKey) ? _cacheManager.Get(cacheKey, independent: true) : (PictureInfo)null }).ToList(); var result = new Dictionary(allRequestedInfos.Count); @@ -315,7 +314,7 @@ public PictureInfo GetPictureInfo(int? pictureId) var info = _cacheManager.Get(cacheKey, () => { return CreatePictureInfo(GetPictureById(pictureId.GetValueOrDefault())); - }); + }, independent: true); return info; } @@ -329,7 +328,7 @@ public PictureInfo GetPictureInfo(Picture picture) var info = _cacheManager.Get(cacheKey, () => { return CreatePictureInfo(picture); - }); + }, independent: true); return info; } @@ -513,7 +512,7 @@ public int ClearUrlCache() public virtual string GetPictureSeName(string name) { - return SeoHelper.GetSeName(name, true, false); + return SeoHelper.GetSeName(name, true, false, false); } public virtual Picture SetSeoFilename(int pictureId, string seoFilename) diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs index 1cb2038497..d16095ff8c 100644 --- a/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs +++ b/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs @@ -21,23 +21,25 @@ public ProcessImageException(string message, ProcessImageQuery query) } public ProcessImageException(ProcessImageQuery query, Exception innerException) - : base(CreateMessage(query), innerException) + : base(CreateMessage(query, innerException), innerException) { Query = query; } - private static string CreateMessage(ProcessImageQuery query) + private static string CreateMessage(ProcessImageQuery query, Exception innerException) { var fileName = query?.FileName; - if (fileName.HasValue()) - { - return "Error while processing image '{0}'.".FormatCurrent(fileName); - } - else + var msg = fileName.HasValue() + ? "Error while processing image '{0}'".FormatCurrent(fileName) + : "Error while processing image"; + + if (innerException != null) { - return "Error while processing image."; + 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 index 392fd6da7a..9b3c5998e2 100644 --- a/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs +++ b/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs @@ -117,13 +117,24 @@ public int? Quality set { Set("q", value); } } - // TODO: (mc) make Enum + /// + /// 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"); } @@ -159,13 +170,19 @@ public bool NeedsProcessing(bool ignoreQualityFlag = false) { if (base.Count == 0) 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; } diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs index e520fcfefd..9ff224d143 100644 --- a/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs +++ b/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs @@ -11,12 +11,19 @@ public class ProcessImageResult : DisposableObject 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) diff --git a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs index b67c04fbe5..4810d8f0bc 100644 --- a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs +++ b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs @@ -1,6 +1,7 @@ using System; +using System.Data.Entity; using System.Linq; -using System.Linq.Expressions; +using System.Threading.Tasks; using SmartStore.Core.Data; using SmartStore.Core.Domain.Media; using SmartStore.Services.Tasks; @@ -9,61 +10,71 @@ namespace SmartStore.Services.Media { /// /// Represents a task for deleting transient media from the database - /// (pictures and downloads which have been uploaded but never assigned to an entity) + /// (pictures and downloads which have been uploaded but never assigned to an entity). /// - public partial class TransientMediaClearTask : ITask + public partial class TransientMediaClearTask : AsyncTask { private readonly IPictureService _pictureService; + private readonly IDownloadService _downloadService; private readonly IRepository _pictureRepository; private readonly IRepository _downloadRepository; public TransientMediaClearTask( IPictureService pictureService, - IRepository pictureRepository, + IDownloadService downloadService, + IRepository pictureRepository, IRepository downloadRepository) { - this._pictureService = pictureService; - this._pictureRepository = pictureRepository; - this._downloadRepository = downloadRepository; + _pictureService = pictureService; + _downloadService = downloadService; + _pictureRepository = pictureRepository; + _downloadRepository = downloadRepository; } - public void Execute(TaskExecutionContext ctx) + public override async Task ExecuteAsync(TaskExecutionContext ctx) { - // delete all media records which are in transient state since at least 3 hours + // Delete all media records which are in transient state since at least 3 hours. var olderThan = DateTime.UtcNow.AddHours(-3); - - // delete Downloads - _downloadRepository.DeleteAll(x => x.IsTransient && x.UpdatedOnUtc < olderThan); - - // delete Pictures - var autoCommit = _pictureRepository.AutoCommitEnabled; - _pictureRepository.AutoCommitEnabled = false; + var pictureAutoCommit = _pictureRepository.AutoCommitEnabled; + var downloadAutoCommit = _downloadRepository.AutoCommitEnabled; + + _pictureRepository.AutoCommitEnabled = false; + _downloadRepository.AutoCommitEnabled = false; try { using (var scope = new DbContextScope(autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) { - var pictures = _pictureRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToList(); + var pictures = await _pictureRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToListAsync(); foreach (var picture in pictures) { _pictureService.DeletePicture(picture); } - _pictureRepository.Context.SaveChanges(); + await _pictureRepository.Context.SaveChangesAsync(); - if (DataSettings.Current.IsSqlServer) - { - try - { - _pictureRepository.Context.ExecuteSqlCommand("DBCC SHRINKDATABASE(0)", true); - } - catch { } - } - } - } - finally + var downloads = await _downloadRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToListAsync(); + foreach (var download in downloads) + { + _downloadService.DeleteDownload(download); + } + + await _downloadRepository.Context.SaveChangesAsync(); + + if (DataSettings.Current.IsSqlServer && (pictures.Any() || downloads.Any())) + { + try + { + _pictureRepository.Context.ExecuteSqlCommand("DBCC SHRINKDATABASE(0)", true); + } + catch { } + } + } + } + finally { - _pictureRepository.AutoCommitEnabled = autoCommit; + _pictureRepository.AutoCommitEnabled = pictureAutoCommit; + _downloadRepository.AutoCommitEnabled = downloadAutoCommit; } } } diff --git a/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs b/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs index ac9ffb04ab..b1fffacea2 100644 --- a/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs +++ b/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs @@ -13,7 +13,7 @@ namespace SmartStore.Services.Messages { - public class CreateAttachmentsConsumer : IConsumer + public class CreateAttachmentsConsumer : IConsumer { private readonly PdfSettings _pdfSettings; private readonly HttpRequestBase _httpRequest; diff --git a/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs b/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs index 9bbdc2d02d..82a6de9bac 100644 --- a/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs +++ b/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using SmartStore.Core; using SmartStore.Core.Domain.Messages; @@ -52,11 +53,11 @@ public partial interface IQueuedEmailService IPagedList SearchEmails(SearchEmailsQuery query); /// - /// Sends a queued email + /// Sends a queued email asynchronously. /// /// Queued email /// Whether the operation succeeded - bool SendEmail(QueuedEmail queuedEmail); + Task SendEmailAsync(QueuedEmail queuedEmail); /// /// Gets a queued email attachment by identifier diff --git a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs index 3b81f017f6..93ba2c8044 100644 --- a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs +++ b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs @@ -4,19 +4,16 @@ using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Messages; using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.DataExchange.Import.Events; namespace SmartStore.Services.Messages.Importer { public class NewsLetterSubscriptionImporter : IEntityImporter { - private readonly ICommonServices _services; private readonly IRepository _subscriptionRepository; - public NewsLetterSubscriptionImporter( - ICommonServices services, - IRepository subscriptionRepository) + public NewsLetterSubscriptionImporter(IRepository subscriptionRepository) { - _services = services; _subscriptionRepository = subscriptionRepository; } @@ -39,9 +36,9 @@ public static string[] DefaultKeyFields public void Execute(ImportExecuteContext context) { var utcNow = DateTime.UtcNow; - var currentStoreId = _services.StoreContext.CurrentStore.Id; + var currentStoreId = context.Services.StoreContext.CurrentStore.Id; - using (var scope = new DbContextScope(ctx: _services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + using (var scope = new DbContextScope(ctx: context.Services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) { var segmenter = context.DataSegmenter; @@ -147,7 +144,9 @@ public void Execute(ImportExecuteContext context) } // for _subscriptionRepository.Context.SaveChanges(); - } // while + + context.Services.EventPublisher.Publish(new ImportBatchExecutedEvent(context, batch)); + } // while } } } diff --git a/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs b/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs index 61fb676fbc..a70a2b480e 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs @@ -22,6 +22,7 @@ using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Templating; +using SmartStore.Utilities; namespace SmartStore.Services.Messages { @@ -626,7 +627,7 @@ private object GetModelFromExpression(string expression, IDictionary 0) { // Fetch a random one - var skip = new Random().Next(count); + var skip = CommonHelper.GenerateRandomInteger(0, count); result = query.OrderBy(x => x.Id).Skip(() => skip).FirstOrDefault(); } else diff --git a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs index 7d18234bba..8cc14ed9d5 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs @@ -469,7 +469,7 @@ protected virtual object CreateModelPart(Store part, MessageContext messageConte var host = messageContext.BaseUri.ToString(); var logoInfo = _services.PictureService.GetPictureInfo(messageContext.Store.LogoPictureId); - // Issue: https://github.com/smartstoreag/SmartStoreNET/issues/1321 + // Issue: https://github.com/smartstore/SmartStoreNET/issues/1321 var m = new Dictionary { diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs b/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs index 8b7ebd258c..00ee3e397e 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Net.Mail; +using System.Threading.Tasks; using System.Web; using SmartStore.Core; using SmartStore.Core.Data; @@ -184,7 +185,7 @@ public virtual IPagedList SearchEmails(SearchEmailsQuery query) return queuedEmails; } - public virtual bool SendEmail(QueuedEmail queuedEmail) + public virtual async Task SendEmailAsync(QueuedEmail queuedEmail) { var result = false; @@ -193,7 +194,7 @@ public virtual bool SendEmail(QueuedEmail queuedEmail) var smtpContext = new SmtpContext(queuedEmail.EmailAccount); var msg = ConvertEmail(queuedEmail); - _emailSender.SendEmail(smtpContext, msg); + await _emailSender.SendEmailAsync(smtpContext, msg); queuedEmail.SentOnUtc = DateTime.UtcNow; result = true; diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs index a21102901b..1af83916e8 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using SmartStore.Core.Data; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Messages; @@ -9,7 +10,7 @@ namespace SmartStore.Services.Messages /// /// Represents a task for deleting sent emails from the message queue. /// - public partial class QueuedMessagesClearTask : ITask + public partial class QueuedMessagesClearTask : AsyncTask { private readonly IRepository _qeRepository; private readonly CommonSettings _commonSettings; @@ -22,10 +23,10 @@ public QueuedMessagesClearTask( _commonSettings = commonSettings; } - public void Execute(TaskExecutionContext ctx) + public override async Task ExecuteAsync(TaskExecutionContext ctx) { var olderThan = DateTime.UtcNow.AddDays(-Math.Abs(_commonSettings.MaxQueuedMessagesAgeInDays)); - _qeRepository.DeleteAll(x => x.SentOnUtc.HasValue && x.CreatedOnUtc < olderThan); + await _qeRepository.DeleteAllAsync(x => x.SentOnUtc.HasValue && x.CreatedOnUtc < olderThan); _qeRepository.Context.ShrinkDatabase(); } diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs index 0c3e7080a6..abcd1c2315 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs @@ -1,11 +1,13 @@ -using SmartStore.Services.Tasks; +using System.Threading.Tasks; +using System.Data.Entity; +using SmartStore.Services.Tasks; namespace SmartStore.Services.Messages { /// /// Represents a task for sending queued message /// - public partial class QueuedMessagesSendTask : ITask + public partial class QueuedMessagesSendTask : AsyncTask { private readonly IQueuedEmailService _queuedEmailService; @@ -14,7 +16,7 @@ public QueuedMessagesSendTask(IQueuedEmailService queuedEmailService) _queuedEmailService = queuedEmailService; } - public void Execute(TaskExecutionContext ctx) + public override async Task ExecuteAsync(TaskExecutionContext ctx) { const int pageSize = 1000; const int maxTries = 3; @@ -30,11 +32,12 @@ public void Execute(TaskExecutionContext ctx) UnsentOnly = true, SendManually = false }; - var queuedEmails = _queuedEmailService.SearchEmails(q); + + var queuedEmails = await _queuedEmailService.SearchEmails(q).LoadAsync(); foreach (var queuedEmail in queuedEmails) { - _queuedEmailService.SendEmail(queuedEmail); + await _queuedEmailService.SendEmailAsync(queuedEmail); } if (!queuedEmails.HasNextPage) diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs index 9c3f825b03..5ac6d24daf 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs @@ -2,6 +2,7 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Stores; using SmartStore.Services.Payments; namespace SmartStore.Services.Orders @@ -16,7 +17,35 @@ public partial interface IOrderProcessingService /// /// Order to be validated void CheckOrderStatus(Order order); - + + /// + /// Checks whether orders are placed at too short intervals. + /// + /// Customer. + /// Store + /// true valid interval, false invalid interval. + bool IsMinimumOrderPlacementIntervalValid(Customer customer, Store store); + + /// + /// Checks whether an order can be placed. + /// + /// Process payment request. + /// List of warning messages. Empty list if an order can be placed. + IList GetOrderPlacementWarnings(ProcessPaymentRequest processPaymentRequest); + + /// + /// Checks whether an order can be placed. + /// + /// Process payment request. + /// Initial order if any (recurring payment). + /// Customer placing the order. + /// List of warning messages. Empty list if an order can be placed. + IList GetOrderPlacementWarnings( + ProcessPaymentRequest processPaymentRequest, + Order initialOrder, + Customer customer, + out IList cart); + /// /// Places an order /// diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs index 9fca02b17f..e0d8dddfc4 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs @@ -83,6 +83,7 @@ public partial interface IOrderService /// 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, @@ -94,7 +95,8 @@ IQueryable GetOrders( int[] shippingStatusIds, string billingEmail, string orderNumber, - string billingName = null); + string billingName = null, + string[] paymentMethods = null); /// /// Search orders @@ -112,10 +114,12 @@ IQueryable GetOrders( /// Page index /// Page size /// Billing name. Leave empty to load all records. + /// 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); + string billingEmail, string orderGuid, string orderNumber, int pageIndex, int pageSize, string billingName = null, + string[] paymentMethods = null); /// /// Gets all orders by affiliate identifier @@ -126,12 +130,6 @@ IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, /// Orders IPagedList GetAllOrders(int affiliateId, int pageIndex, int pageSize); - /// - /// Load all orders - /// - /// Order collection - IList LoadAllOrders(); - /// /// Gets all orders by affiliate identifier /// diff --git a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs index 492618410a..f09a198d95 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs @@ -12,6 +12,7 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Stores; using SmartStore.Core.Domain.Tax; using SmartStore.Core.Events; using SmartStore.Core.Localization; @@ -426,45 +427,321 @@ public void CheckOrderStatus(Order order) #region Methods - /// - /// Places an order - /// - /// Process payment request - /// Place order result + public virtual bool IsMinimumOrderPlacementIntervalValid(Customer customer, Store store) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(store, nameof(store)); + + // Prevent 2 orders being placed within an X seconds time frame. + if (_orderSettings.MinimumOrderPlacementInterval == 0) + { + return true; + } + + var lastOrder = _orderService.SearchOrders(store.Id, customer.Id, null, null, null, null, null, null, null, null, 0, 1).FirstOrDefault(); + if (lastOrder == null) + { + return true; + } + + var interval = DateTime.UtcNow - lastOrder.CreatedOnUtc; + return interval.TotalSeconds > _orderSettings.MinimumOrderPlacementInterval; + } + + public virtual IList GetOrderPlacementWarnings(ProcessPaymentRequest processPaymentRequest) + { + Guard.NotNull(processPaymentRequest, nameof(processPaymentRequest)); + + var initialOrder = _orderService.GetOrderById(processPaymentRequest.InitialOrderId); + var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); + + return GetOrderPlacementWarnings(processPaymentRequest, initialOrder, customer, out var cart); + } + + public virtual IList GetOrderPlacementWarnings( + ProcessPaymentRequest processPaymentRequest, + Order initialOrder, + Customer customer, + out IList cart) + { + Guard.NotNull(processPaymentRequest, nameof(processPaymentRequest)); + + cart = null; + + var warnings = new List(); + var skipPaymentWorkflow = false; + var isRecurringShoppingCart = false; + var paymentMethodSystemName = processPaymentRequest.PaymentMethodSystemName; + + if (customer == null) + { + warnings.Add(T("Customer.DoesNotExist")); + return warnings; + } + + // Check whether guest checkout is allowed. + if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) + { + warnings.Add(T("Checkout.AnonymousNotAllowed")); + return warnings; + } + + if (!processPaymentRequest.IsRecurringPayment) + { + if (processPaymentRequest.ShoppingCartItemIds.Any()) + { + cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId) + .Where(x => processPaymentRequest.ShoppingCartItemIds.Contains(x.Item.Id)) + .ToList(); + } + else + { + cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId); + } + + if (!cart.Any()) + { + warnings.Add(T("ShoppingCart.CartIsEmpty")); + return warnings; + } + + // Validate the entire shopping cart. + var cartWarnings = _shoppingCartService.GetShoppingCartWarnings(cart, customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes), true); + if (cartWarnings.Any()) + { + warnings.AddRange(cartWarnings); + return warnings; + } + + // Validate individual cart items. + foreach (var sci in cart) + { + var sciWarnings = _shoppingCartService.GetShoppingCartItemWarnings(customer, sci.Item.ShoppingCartType, + sci.Item.Product, processPaymentRequest.StoreId, sci.Item.AttributesXml, + sci.Item.CustomerEnteredPrice, sci.Item.Quantity, false, childItems: sci.ChildItems); + + if (sciWarnings.Any()) + { + warnings.AddRange(sciWarnings); + return warnings; + } + } + + // Min totals validation. + var minOrderSubtotalAmountOk = ValidateMinOrderSubtotalAmount(cart); + if (!minOrderSubtotalAmountOk) + { + var minOrderSubtotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSubtotalAmount, _workContext.WorkingCurrency); + warnings.Add(T("Checkout.MinOrderSubtotalAmount", _priceFormatter.FormatPrice(minOrderSubtotalAmount, true, false))); + return warnings; + } + + var minOrderTotalAmountOk = ValidateMinOrderTotalAmount(cart); + if (!minOrderTotalAmountOk) + { + var minOrderTotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderTotalAmount, _workContext.WorkingCurrency); + warnings.Add(T("Checkout.MinOrderTotalAmount", _priceFormatter.FormatPrice(minOrderTotalAmount, true, false))); + return warnings; + } + + // Total validations. + var orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out var orderShippingTaxRate, out var shippingTotalDiscount); + var orderShippingTotalExclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, false); + + if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue) + { + warnings.Add(T("Order.CannotCalculateShippingTotal")); + return warnings; + } + + var cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); + if (!cartTotal.TotalAmount.HasValue) + { + warnings.Add(T("Order.CannotCalculateOrderTotal")); + return warnings; + } + + skipPaymentWorkflow = cartTotal.TotalAmount.Value == decimal.Zero; + + // Address validations. + if (customer.BillingAddress == null) + { + warnings.Add(T("Order.BillingAddressMissing")); + } + else if (!customer.BillingAddress.Email.IsEmail()) + { + warnings.Add(T("Common.Error.InvalidEmail")); + } + else if (customer.BillingAddress.Country != null && !customer.BillingAddress.Country.AllowsBilling) + { + warnings.Add(T("Order.CountryNotAllowedForBilling", customer.BillingAddress.Country.Name)); + } + + if (cart.RequiresShipping()) + { + if (customer.ShippingAddress == null) + { + warnings.Add(T("Order.ShippingAddressMissing")); + throw new SmartException(); + } + else if (!customer.ShippingAddress.Email.IsEmail()) + { + warnings.Add(T("Common.Error.InvalidEmail")); + } + else if (customer.ShippingAddress.Country != null && !customer.ShippingAddress.Country.AllowsShipping) + { + warnings.Add(T("Order.CountryNotAllowedForShipping", customer.ShippingAddress.Country.Name)); + } + } + } + else + { + // Recurring order. + if (initialOrder == null) + { + warnings.Add(T("Order.InitialOrderDoesNotExistForRecurringPayment")); + return warnings; + } + + var cartTotal = new ShoppingCartTotal(initialOrder.OrderTotal); + skipPaymentWorkflow = cartTotal.TotalAmount.Value == decimal.Zero; + paymentMethodSystemName = initialOrder.PaymentMethodSystemName; + + // Address validations. + if (initialOrder.BillingAddress == null) + { + warnings.Add(T("Order.BillingAddressMissing")); + } + else if (initialOrder.BillingAddress.Country != null && !initialOrder.BillingAddress.Country.AllowsBilling) + { + warnings.Add(T("Order.CountryNotAllowedForBilling", initialOrder.BillingAddress.Country.Name)); + } + + if (initialOrder.ShippingStatus != ShippingStatus.ShippingNotRequired) + { + if (initialOrder.ShippingAddress == null) + { + warnings.Add(T("Order.ShippingAddressMissing")); + } + else if (initialOrder.ShippingAddress.Country != null && !initialOrder.ShippingAddress.Country.AllowsShipping) + { + warnings.Add(T("Order.CountryNotAllowedForShipping", initialOrder.ShippingAddress.Country.Name)); + } + } + } + + // Payment. + if (!warnings.Any() && !skipPaymentWorkflow) + { + var paymentMethod = _paymentService.LoadPaymentMethodBySystemName(paymentMethodSystemName); + if (paymentMethod == null) + { + warnings.Add(T("Payment.CouldNotLoadMethod")); + } + else if (!paymentMethod.IsPaymentMethodActive(_paymentSettings)) + { + warnings.Add(T("Payment.MethodNotAvailable")); + } + } + + // Recurring or standard shopping cart? + if (!warnings.Any() && !processPaymentRequest.IsRecurringPayment) + { + isRecurringShoppingCart = cart.IsRecurring(); + if (isRecurringShoppingCart) + { + var recurringCyclesError = cart.GetRecurringCycleInfo(_localizationService, out var recurringCycleLength, out var recurringCyclePeriod, out var recurringTotalCycles); + if (recurringCyclesError.HasValue()) + { + warnings.Add(recurringCyclesError); + } + } + } + else + { + isRecurringShoppingCart = true; + } + + // Validate recurring payment type. + if (!warnings.Any() && !skipPaymentWorkflow && !processPaymentRequest.IsMultiOrder) + { + RecurringPaymentType? recurringPaymentType = null; + + if (!processPaymentRequest.IsRecurringPayment) + { + if (isRecurringShoppingCart) + { + recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); + } + } + else + { + if (isRecurringShoppingCart) + { + recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); + } + else + { + warnings.Add(T("Order.NoRecurringProducts")); + } + } + + if (recurringPaymentType.HasValue) + { + switch (recurringPaymentType.Value) + { + case RecurringPaymentType.NotSupported: + warnings.Add(T("Payment.RecurringPaymentNotSupported")); + break; + case RecurringPaymentType.Manual: + case RecurringPaymentType.Automatic: + break; + default: + warnings.Add(T("Payment.RecurringPaymentTypeUnknown")); + break; + } + } + } + + return warnings; + } + public virtual PlaceOrderResult PlaceOrder( ProcessPaymentRequest processPaymentRequest, Dictionary extraData) { - // think about moving functionality of processing recurring orders (after the initial order was placed) to ProcessNextRecurringPayment() method - if (processPaymentRequest == null) - throw new ArgumentNullException("processPaymentRequest"); + // Think about moving functionality of processing recurring orders (after the initial order was placed) to ProcessNextRecurringPayment() method. + Guard.NotNull(processPaymentRequest, nameof(processPaymentRequest)); if (processPaymentRequest.OrderGuid == Guid.Empty) + { processPaymentRequest.OrderGuid = Guid.NewGuid(); + } var result = new PlaceOrderResult(); var utcNow = DateTime.UtcNow; try { - #region Order details (customer, totals) - - // Recurring orders. Load initial order var initialOrder = _orderService.GetOrderById(processPaymentRequest.InitialOrderId); - if (processPaymentRequest.IsRecurringPayment) + var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); + + var warnings = GetOrderPlacementWarnings(processPaymentRequest, initialOrder, customer, out var cart); + if (warnings.Any()) { - if (initialOrder == null) - throw new ArgumentException(T("Order.InitialOrderDoesNotExistForRecurringPayment")); + result.Errors.AddRange(warnings); + Logger.Warn(string.Join(" ", result.Errors)); + return result; + } + + #region Order details + if (processPaymentRequest.IsRecurringPayment) + { processPaymentRequest.PaymentMethodSystemName = initialOrder.PaymentMethodSystemName; } - // customer - var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); - if (customer == null) - throw new ArgumentException(T("Customer.DoesNotExist")); - - // affilites + // Affilites. var affiliateId = 0; var affiliate = _affiliateService.GetAffiliateById(customer.AffiliateId); if (affiliate != null && affiliate.Active && !affiliate.Deleted) @@ -472,9 +749,9 @@ public virtual PlaceOrderResult PlaceOrder( affiliateId = affiliate.Id; } - // customer currency - string customerCurrencyCode = ""; - decimal customerCurrencyRate = decimal.Zero; + // Customer currency. + var customerCurrencyCode = string.Empty; + var customerCurrencyRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) { var currencyTmp = _currencyService.GetCurrencyById(customer.GetAttribute(SystemCustomerAttributeNames.CurrencyId, processPaymentRequest.StoreId)); @@ -482,7 +759,6 @@ public virtual PlaceOrderResult PlaceOrder( customerCurrencyCode = customerCurrency.CurrencyCode; var primaryStoreCurrency = _storeContext.CurrentStore.PrimaryStoreCurrency; - customerCurrencyRate = customerCurrency.Rate / primaryStoreCurrency.Rate; } else @@ -491,7 +767,7 @@ public virtual PlaceOrderResult PlaceOrder( customerCurrencyRate = initialOrder.CurrencyRate; } - // customer language + // Customer language. Language customerLanguage = null; if (!processPaymentRequest.IsRecurringPayment) { @@ -506,66 +782,8 @@ public virtual PlaceOrderResult PlaceOrder( { customerLanguage = _workContext.WorkingLanguage; } - - // check whether customer is guest - if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) - throw new SmartException(T("Checkout.AnonymousNotAllowed")); - - var storeId = _storeContext.CurrentStore.Id; - - // load and validate customer shopping cart - IList cart = null; - if (!processPaymentRequest.IsRecurringPayment) - { - //load shopping cart - if (processPaymentRequest.ShoppingCartItemIds.Count > 0) - { - cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId) - .Where(x => processPaymentRequest.ShoppingCartItemIds.Contains(x.Item.Id)) - .ToList(); - } - else - cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId); - - if (cart.Count == 0) - throw new SmartException(T("ShoppingCart.CartIsEmpty")); - - // validate the entire shopping cart - var warnings = _shoppingCartService.GetShoppingCartWarnings(cart, customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes), true); - if (warnings.Count > 0) - throw new SmartException(string.Join(" ", warnings)); - - // validate individual cart items - foreach (var sci in cart) - { - var sciWarnings = _shoppingCartService.GetShoppingCartItemWarnings(customer, sci.Item.ShoppingCartType, - sci.Item.Product, processPaymentRequest.StoreId, sci.Item.AttributesXml, - sci.Item.CustomerEnteredPrice, sci.Item.Quantity, false, childItems: sci.ChildItems); - - if (sciWarnings.Count > 0) - throw new SmartException(string.Join(" ", sciWarnings)); - } - } - - // min totals validation - if (!processPaymentRequest.IsRecurringPayment) - { - var minOrderSubtotalAmountOk = ValidateMinOrderSubtotalAmount(cart); - if (!minOrderSubtotalAmountOk) - { - var minOrderSubtotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSubtotalAmount, _workContext.WorkingCurrency); - throw new SmartException(T("Checkout.MinOrderSubtotalAmount", _priceFormatter.FormatPrice(minOrderSubtotalAmount, true, false))); - } - - var minOrderTotalAmountOk = ValidateMinOrderTotalAmount(cart); - if (!minOrderTotalAmountOk) - { - var minOrderTotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderTotalAmount, _workContext.WorkingCurrency); - throw new SmartException(T("Checkout.MinOrderTotalAmount", _priceFormatter.FormatPrice(minOrderTotalAmount, true, false))); - } - } - - // tax display type + + // Tax display type. var customerTaxDisplayType = TaxDisplayType.IncludingTax; if (!processPaymentRequest.IsRecurringPayment) { @@ -576,7 +794,7 @@ public virtual PlaceOrderResult PlaceOrder( customerTaxDisplayType = initialOrder.CustomerTaxDisplayType; } - // checkout attributes + // Checkout attributes. string checkoutAttributeDescription, checkoutAttributesXml; if (!processPaymentRequest.IsRecurringPayment) { @@ -589,38 +807,28 @@ public virtual PlaceOrderResult PlaceOrder( checkoutAttributeDescription = initialOrder.CheckoutAttributeDescription; } - // applied discount (used to store discount usage history) + // Applied discount (used to store discount usage history). var appliedDiscounts = new List(); - - // sub total decimal orderSubTotalInclTax, orderSubTotalExclTax; decimal orderSubTotalDiscountInclTax = 0, orderSubTotalDiscountExclTax = 0; if (!processPaymentRequest.IsRecurringPayment) { - // sub total (incl tax) - decimal orderSubTotalDiscountAmount1 = decimal.Zero; - Discount orderSubTotalAppliedDiscount1 = null; - decimal subTotalWithoutDiscountBase1 = decimal.Zero; - decimal subTotalWithDiscountBase1 = decimal.Zero; - - _orderTotalCalculationService.GetShoppingCartSubTotal(cart, - true, out orderSubTotalDiscountAmount1, out orderSubTotalAppliedDiscount1, out subTotalWithoutDiscountBase1, out subTotalWithDiscountBase1); + // Sub total (incl tax). + _orderTotalCalculationService.GetShoppingCartSubTotal(cart, true, + out var orderSubTotalDiscountAmount1, out var orderSubTotalAppliedDiscount1, out var subTotalWithoutDiscountBase1, out var subTotalWithDiscountBase1); orderSubTotalInclTax = subTotalWithoutDiscountBase1; orderSubTotalDiscountInclTax = orderSubTotalDiscountAmount1; - // discount history + // Discount history. if (orderSubTotalAppliedDiscount1 != null && !appliedDiscounts.Any(x => x.Id == orderSubTotalAppliedDiscount1.Id)) + { appliedDiscounts.Add(orderSubTotalAppliedDiscount1); + } - // sub total (excl tax) - decimal orderSubTotalDiscountAmount2 = decimal.Zero; - Discount orderSubTotalAppliedDiscount2 = null; - decimal subTotalWithoutDiscountBase2 = decimal.Zero; - decimal subTotalWithDiscountBase2 = decimal.Zero; - - _orderTotalCalculationService.GetShoppingCartSubTotal(cart, - false, out orderSubTotalDiscountAmount2, out orderSubTotalAppliedDiscount2, out subTotalWithoutDiscountBase2, out subTotalWithDiscountBase2); + // Sub total (excl tax). + _orderTotalCalculationService.GetShoppingCartSubTotal(cart, false, + out var orderSubTotalDiscountAmount2, out var orderSubTotalAppliedDiscount2, out var subTotalWithoutDiscountBase2, out var subTotalWithDiscountBase2); orderSubTotalExclTax = subTotalWithoutDiscountBase2; orderSubTotalDiscountExclTax = orderSubTotalDiscountAmount2; @@ -632,8 +840,8 @@ public virtual PlaceOrderResult PlaceOrder( } - // shipping info - bool shoppingCartRequiresShipping = false; + // Shipping info. + var shoppingCartRequiresShipping = false; if (!processPaymentRequest.IsRecurringPayment) { shoppingCartRequiresShipping = cart.RequiresShipping(); @@ -662,19 +870,18 @@ public virtual PlaceOrderResult PlaceOrder( } } - // shipping total + // Shipping total. decimal? orderShippingTotalInclTax, orderShippingTotalExclTax = null; decimal orderShippingTaxRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) { - Discount shippingTotalDiscount = null; - orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out orderShippingTaxRate, out shippingTotalDiscount); + orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out orderShippingTaxRate, out var shippingTotalDiscount); orderShippingTotalExclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, false); - if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue) - throw new SmartException(T("Order.CannotCalculateShippingTotal")); if (shippingTotalDiscount != null && !appliedDiscounts.Any(x => x.Id == shippingTotalDiscount.Id)) + { appliedDiscounts.Add(shippingTotalDiscount); + } } else { @@ -683,11 +890,11 @@ public virtual PlaceOrderResult PlaceOrder( orderShippingTaxRate = initialOrder.OrderShippingTaxRate; } - //payment total + // Payment total. decimal paymentAdditionalFeeInclTax, paymentAdditionalFeeExclTax, paymentAdditionalFeeTaxRate; if (!processPaymentRequest.IsRecurringPayment) { - decimal paymentAdditionalFee = _paymentService.GetAdditionalHandlingFee(cart, processPaymentRequest.PaymentMethodSystemName); + var paymentAdditionalFee = _paymentService.GetAdditionalHandlingFee(cart, processPaymentRequest.PaymentMethodSystemName); paymentAdditionalFeeInclTax = _taxService.GetPaymentMethodAdditionalFee(paymentAdditionalFee, true, customer, out paymentAdditionalFeeTaxRate); paymentAdditionalFeeExclTax = _taxService.GetPaymentMethodAdditionalFee(paymentAdditionalFee, false, customer); } @@ -698,16 +905,15 @@ public virtual PlaceOrderResult PlaceOrder( paymentAdditionalFeeTaxRate = initialOrder.PaymentMethodAdditionalFeeTaxRate; } - //tax total - decimal orderTaxTotal = decimal.Zero; + // Tax total. + var orderTaxTotal = decimal.Zero; string vatNumber = "", taxRates = ""; if (!processPaymentRequest.IsRecurringPayment) { - //tax amount - SortedDictionary taxRatesDictionary = null; - orderTaxTotal = _orderTotalCalculationService.GetTaxTotal(cart, out taxRatesDictionary); + // Tax amount. + orderTaxTotal = _orderTotalCalculationService.GetTaxTotal(cart, out var taxRatesDictionary); - //VAT number + // VAT number. var customerVatStatus = (VatNumberStatus)customer.GetAttribute(SystemCustomerAttributeNames.VatNumberStatusId); if (_taxSettings.EuVatEnabled && customerVatStatus == VatNumberStatus.Valid) { @@ -719,22 +925,19 @@ public virtual PlaceOrderResult PlaceOrder( else { orderTaxTotal = initialOrder.OrderTax; - // VAT number vatNumber = initialOrder.VatNumber; } + processPaymentRequest.OrderTax = orderTaxTotal; - // Order total (and applied discounts, gift cards, reward points) + // Order total (and applied discounts, gift cards, reward points). ShoppingCartTotal cartTotal = null; if (!processPaymentRequest.IsRecurringPayment) { cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); - if (!cartTotal.TotalAmount.HasValue) - throw new SmartException(T("Order.CannotCalculateOrderTotal")); - - // Discount history + // Discount history. if (cartTotal.AppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == cartTotal.AppliedDiscount.Id)) { appliedDiscounts.Add(cartTotal.AppliedDiscount); @@ -750,9 +953,9 @@ public virtual PlaceOrderResult PlaceOrder( #endregion - #region Addresses & pre payment workflow + #region Addresses & pre-payment workflow - // give payment processor the opportunity to fullfill billing address + // Give payment processor the opportunity to fullfill billing address. var preProcessPaymentResult = _paymentService.PreProcessPayment(processPaymentRequest); if (!preProcessPaymentResult.Success) @@ -762,95 +965,44 @@ public virtual PlaceOrderResult PlaceOrder( return result; } - Address billingAddress = null; - if (!processPaymentRequest.IsRecurringPayment) - { - if (customer.BillingAddress == null) - throw new SmartException(T("Order.BillingAddressMissing")); - - if (!customer.BillingAddress.Email.IsEmail()) - throw new SmartException(T("Common.Error.InvalidEmail")); - - billingAddress = (Address)customer.BillingAddress.Clone(); - } - else - { - if (initialOrder.BillingAddress == null) - throw new SmartException(T("Order.BillingAddressMissing")); - - billingAddress = (Address)initialOrder.BillingAddress.Clone(); - } - - if (billingAddress.Country != null && !billingAddress.Country.AllowsBilling) - throw new SmartException(T("Order.CountryNotAllowedForBilling", billingAddress.Country.Name)); + var billingAddress = !processPaymentRequest.IsRecurringPayment + ? (Address)customer.BillingAddress.Clone() + : (Address)initialOrder.BillingAddress.Clone(); Address shippingAddress = null; if (shoppingCartRequiresShipping) { - if (!processPaymentRequest.IsRecurringPayment) - { - if (customer.ShippingAddress == null) - throw new SmartException(T("Order.ShippingAddressMissing")); - - if (!customer.ShippingAddress.Email.IsEmail()) - throw new SmartException(T("Common.Error.InvalidEmail")); - - shippingAddress = (Address)customer.ShippingAddress.Clone(); - } - else - { - if (initialOrder.ShippingAddress == null) - throw new SmartException(T("Order.ShippingAddressMissing")); - - shippingAddress = (Address)initialOrder.ShippingAddress.Clone(); - } - - if (shippingAddress.Country != null && !shippingAddress.Country.AllowsShipping) - throw new SmartException(T("Order.CountryNotAllowedForShipping", shippingAddress.Country.Name)); + shippingAddress = !processPaymentRequest.IsRecurringPayment + ? (Address)customer.ShippingAddress.Clone() + : (Address)initialOrder.ShippingAddress.Clone(); } #endregion #region Payment workflow - // skip payment workflow if order total equals zero - var skipPaymentWorkflow = false; - if (cartTotal.TotalAmount.Value == decimal.Zero) - { - skipPaymentWorkflow = true; - } + // Skip payment workflow if order total equals zero. + var skipPaymentWorkflow = cartTotal.TotalAmount.Value == decimal.Zero; - // payment workflow + // Payment workflow. Provider paymentMethod = null; if (!skipPaymentWorkflow) { paymentMethod = _paymentService.LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); - if (paymentMethod == null) - throw new SmartException(T("Payment.CouldNotLoadMethod")); - - // ensure that payment method is active - if (!paymentMethod.IsPaymentMethodActive(_paymentSettings)) - throw new SmartException(T("Payment.MethodNotAvailable")); } else { processPaymentRequest.PaymentMethodSystemName = ""; } - // recurring or standard shopping cart? + // Recurring or standard shopping cart? var isRecurringShoppingCart = false; if (!processPaymentRequest.IsRecurringPayment) { isRecurringShoppingCart = cart.IsRecurring(); if (isRecurringShoppingCart) { - var recurringCycleLength = 0; - var recurringTotalCycles = 0; - RecurringProductCyclePeriod recurringCyclePeriod; - string recurringCyclesError = cart.GetRecurringCycleInfo(_localizationService, out recurringCycleLength, out recurringCyclePeriod, out recurringTotalCycles); - - if (!string.IsNullOrEmpty(recurringCyclesError)) - throw new SmartException(recurringCyclesError); + var unused = cart.GetRecurringCycleInfo(_localizationService, out var recurringCycleLength, out var recurringCyclePeriod, out var recurringTotalCycles); processPaymentRequest.RecurringCycleLength = recurringCycleLength; processPaymentRequest.RecurringCyclePeriod = recurringCyclePeriod; @@ -862,7 +1014,7 @@ public virtual PlaceOrderResult PlaceOrder( isRecurringShoppingCart = true; } - // process payment + // Process payment. ProcessPaymentResult processPaymentResult = null; if (!skipPaymentWorkflow && !processPaymentRequest.IsMultiOrder) { @@ -870,7 +1022,7 @@ public virtual PlaceOrderResult PlaceOrder( { if (isRecurringShoppingCart) { - // recurring cart + // Recurring cart. var recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); switch (recurringPaymentType) { @@ -886,7 +1038,7 @@ public virtual PlaceOrderResult PlaceOrder( } else { - // standard cart + // Standard cart. processPaymentResult = _paymentService.ProcessPayment(processPaymentRequest); } } @@ -894,11 +1046,11 @@ public virtual PlaceOrderResult PlaceOrder( { if (isRecurringShoppingCart) { - // Old credit card info + // Old credit card info. processPaymentRequest.CreditCardType = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardType) : ""; processPaymentRequest.CreditCardName = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardName) : ""; processPaymentRequest.CreditCardNumber = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardNumber) : ""; - // MaskedCreditCardNumber + // MaskedCreditCardNumber. processPaymentRequest.CreditCardCvv2 = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardCvv2) : ""; try @@ -917,7 +1069,7 @@ public virtual PlaceOrderResult PlaceOrder( processPaymentResult = _paymentService.ProcessRecurringPayment(processPaymentRequest); break; case RecurringPaymentType.Automatic: - //payment is processed on payment gateway site + // Payment is processed on payment gateway site. processPaymentResult = new ProcessPaymentResult(); break; default: @@ -932,20 +1084,19 @@ public virtual PlaceOrderResult PlaceOrder( } else { - // payment is not required - if (processPaymentResult == null) - { - processPaymentResult = new ProcessPaymentResult(); - } - processPaymentResult.NewPaymentStatus = PaymentStatus.Paid; + // Payment is not required. + processPaymentResult = new ProcessPaymentResult + { + NewPaymentStatus = PaymentStatus.Paid + }; } #endregion if (processPaymentResult.Success) { - //save order in data storage - //uncomment this line to support transactions + // Save order. + // Uncomment this line to support transactions. //using (var scope = new System.Transactions.TransactionScope()) { #region Save order details @@ -1112,7 +1263,7 @@ public virtual PlaceOrderResult PlaceOrder( order.OrderItems.Add(orderItem); _orderService.UpdateOrder(order); - // Gift cards + // Gift cards. if (sc.Item.Product.IsGiftCard) { _productAttributeParser.GetGiftCardAttribute( @@ -1147,7 +1298,7 @@ public virtual PlaceOrderResult PlaceOrder( _productService.AdjustInventory(sc, true); } - // Clear shopping cart + // Clear shopping cart. if (!processPaymentRequest.IsMultiOrder) { cart.ToList().ForEach(sci => _shoppingCartService.DeleteShoppingCartItem(sci.Item, false)); @@ -1155,11 +1306,11 @@ public virtual PlaceOrderResult PlaceOrder( } else { - // Recurring payment + // Recurring payment. var initialOrderItems = initialOrder.OrderItems; foreach (var orderItem in initialOrderItems) { - // Save item + // Save item. var newOrderItem = new OrderItem { OrderItemGuid = Guid.NewGuid(), @@ -1187,7 +1338,7 @@ public virtual PlaceOrderResult PlaceOrder( order.OrderItems.Add(newOrderItem); _orderService.UpdateOrder(order); - // Gift cards + // Gift cards. if (orderItem.Product.IsGiftCard) { _productAttributeParser.GetGiftCardAttribute( @@ -1223,7 +1374,7 @@ public virtual PlaceOrderResult PlaceOrder( } } - // Discount usage history + // Discount usage history. if (!processPaymentRequest.IsRecurringPayment) { foreach (var discount in appliedDiscounts) @@ -1238,7 +1389,7 @@ public virtual PlaceOrderResult PlaceOrder( } } - // Gift card usage history + // Gift card usage history. if (!processPaymentRequest.IsRecurringPayment && cartTotal.AppliedGiftCards != null) { foreach (var agc in cartTotal.AppliedGiftCards) @@ -1256,7 +1407,7 @@ public virtual PlaceOrderResult PlaceOrder( } } - // Reward points history + // Reward points history. if (cartTotal.RedeemedRewardPointsAmount > decimal.Zero) { customer.AddRewardPointsHistoryEntry(-cartTotal.RedeemedRewardPoints, @@ -1267,10 +1418,10 @@ public virtual PlaceOrderResult PlaceOrder( _customerService.UpdateCustomer(customer); } - // Recurring orders + // Recurring orders. if (!processPaymentRequest.IsRecurringPayment && isRecurringShoppingCart) { - // Create recurring payment (the first payment) + // Create recurring payment (the first payment). var rp = new RecurringPayment { CycleLength = processPaymentRequest.RecurringCycleLength, @@ -1287,26 +1438,22 @@ public virtual PlaceOrderResult PlaceOrder( switch (recurringPaymentType) { case RecurringPaymentType.NotSupported: - { - //not supported - } break; case RecurringPaymentType.Manual: { - //first payment - var rph = new RecurringPaymentHistory() + // First payment. + rp.RecurringPaymentHistory.Add(new RecurringPaymentHistory { RecurringPayment = rp, - CreatedOnUtc = utcNow, - OrderId = order.Id, - }; - rp.RecurringPaymentHistory.Add(rph); + CreatedOnUtc = utcNow, + OrderId = order.Id + }); _orderService.UpdateRecurringPayment(rp); } break; case RecurringPaymentType.Automatic: { - //will be created later (process is automated) + // Will be created later (process is automated). } break; default: @@ -1318,10 +1465,10 @@ public virtual PlaceOrderResult PlaceOrder( #region Notifications, notes and attributes - // notes, messages + // Notes, messages. _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPlaced")); - // send email notifications + // Send email notifications. var msg = _messageFactory.SendOrderPlacedStoreOwnerNotification(order, _localizationSettings.DefaultAdminLanguageId); if (msg?.Email?.Id != null) { @@ -1334,25 +1481,25 @@ public virtual PlaceOrderResult PlaceOrder( _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerEmailQueued", msg.Email.Id)); } - // check order status + // 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); } - // check for generic attributes to be inserted automatically + // 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); } - //uncomment this line to support transactions + // Uncomment this line to support transactions. //scope.Complete(); - // raise event + // Publish events. _eventPublisher.PublishOrderPlaced(order); if (!processPaymentRequest.IsRecurringPayment) @@ -1360,7 +1507,6 @@ public virtual PlaceOrderResult PlaceOrder( _customerActivityService.InsertActivity("PublicStore.PlaceOrder", T("ActivityLog.PublicStore.PlaceOrder", order.GetOrderNumber())); } - //raise event if (order.PaymentStatus == PaymentStatus.Paid) { _eventPublisher.PublishOrderPaid(order); @@ -1378,10 +1524,7 @@ public virtual PlaceOrderResult PlaceOrder( if (nsResult.HasValue) { - if (nsResult.Value) - _orderService.AddOrderNote(order, T("Admin.OrderNotice.NewsLetterSubscriptionAdded")); - else - _orderService.AddOrderNote(order, T("Admin.OrderNotice.NewsLetterSubscriptionRemoved")); + _orderService.AddOrderNote(order, T(nsResult.Value ? "Admin.OrderNotice.NewsLetterSubscriptionAdded" : "Admin.OrderNotice.NewsLetterSubscriptionRemoved")); } } @@ -1404,7 +1547,7 @@ public virtual PlaceOrderResult PlaceOrder( result.AddError(ex.Message); } - if (result.Errors.Count > 0) + if (result.Errors.Any()) { Logger.Error(string.Join(" ", result.Errors)); } @@ -1418,30 +1561,28 @@ public virtual PlaceOrderResult PlaceOrder( /// The order public virtual void DeleteOrder(Order order) { - if (order == null) - throw new ArgumentNullException("order"); - - //reward points - ReduceRewardPoints(order); + Guard.NotNull(order, nameof(order)); - //cancel recurring payments - var recurringPayments = _orderService.SearchRecurringPayments(0, 0, order.Id, null); - foreach (var rp in recurringPayments) + if (order.OrderStatus != OrderStatus.Cancelled) { - //use errors? - var errors = CancelRecurringPayment(rp); + ReduceRewardPoints(order); + + // Cancel recurring payments. + var recurringPayments = _orderService.SearchRecurringPayments(0, 0, order.Id, null); + foreach (var rp in recurringPayments) + { + CancelRecurringPayment(rp); + } + + // Adjust inventory. + foreach (var orderItem in order.OrderItems) + { + _productService.AdjustInventory(orderItem, false, orderItem.Quantity); + } } - //Adjust inventory - foreach (var orderItem in order.OrderItems) - { - _productService.AdjustInventory(orderItem, false, orderItem.Quantity); - } + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderDeleted")); - //add a note - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderDeleted")); - - //now delete an order _orderService.DeleteOrder(order); } @@ -1714,26 +1855,26 @@ public virtual bool CanCancelOrder(Order order) /// True to notify customer public virtual void CancelOrder(Order order, bool notifyCustomer) { - if (order == null) - throw new ArgumentNullException("order"); + Guard.NotNull(order, nameof(order)); if (!CanCancelOrder(order)) + { throw new SmartException(T("Order.CannotCancel")); + } - //Cancel order + // Cancel order. SetOrderStatus(order, OrderStatus.Cancelled, notifyCustomer); _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCancelled")); - //cancel recurring payments + // Cancel recurring payments. var recurringPayments = _orderService.SearchRecurringPayments(0, 0, order.Id, null); foreach (var rp in recurringPayments) { - //use errors? - var errors = CancelRecurringPayment(rp); + CancelRecurringPayment(rp); } - //Adjust inventory + // Adjust inventory. foreach (var orderItem in order.OrderItems) { _productService.AdjustInventory(orderItem, false, orderItem.Quantity); diff --git a/src/Libraries/SmartStore.Services/Orders/OrderService.cs b/src/Libraries/SmartStore.Services/Orders/OrderService.cs index 586f3656e1..0d9d8f965d 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderService.cs @@ -10,10 +10,11 @@ using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Events; +using SmartStore.Data.Caching; namespace SmartStore.Services.Orders { - public partial class OrderService : IOrderService + public partial class OrderService : IOrderService { private readonly IRepository _orderRepository; private readonly IRepository _orderItemRepository; @@ -48,7 +49,7 @@ public virtual Order GetOrderById(int orderId) if (orderId == 0) return null; - return _orderRepository.GetById(orderId); + return _orderRepository.GetByIdCached(orderId, "db.order.id-" + orderId); } public virtual IList GetOrdersByIds(int[] orderIds) @@ -134,7 +135,8 @@ public virtual IQueryable GetOrders( int[] shippingStatusIds, string billingEmail, string orderNumber, - string billingName = null) + string billingName = null, + string[] paymentMethods = null) { var query = _orderRepository.Table; @@ -173,6 +175,11 @@ public virtual IQueryable GetOrders( 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); return query; @@ -180,10 +187,11 @@ public virtual IQueryable GetOrders( 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 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); + billingEmail, orderNumber, billingName, paymentMethods); query = query.OrderByDescending(x => x.CreatedOnUtc); @@ -212,11 +220,6 @@ public virtual IPagedList GetAllOrders(int affiliateId, int pageIndex, in return orders; } - public virtual IList LoadAllOrders() - { - return SearchOrders(0, 0, null, null, null, null, null, null, null, null, 0, int.MaxValue); - } - public virtual IList GetOrdersByAffiliateId(int affiliateId) { var query = from o in _orderRepository.Table diff --git a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs index d2fbbd50e7..7bcf2910a0 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs @@ -1250,9 +1250,9 @@ public virtual ShoppingCartTotal GetShoppingCartTotal( resultTemp = resultTemp.RoundIfEnabledFor(currency); - // Return null if we have errors 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; diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs index 47e05d3015..d0880c760d 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs @@ -942,10 +942,16 @@ public virtual IList GetShoppingCartWarnings(IList a.TextPrompt))) - warnings.Add(ca2.GetLocalized(a => a.TextPrompt)); + string textPrompt = ca2.GetLocalized(a => a.TextPrompt); + + if (textPrompt.HasValue()) + { + warnings.Add(textPrompt); + } else + { warnings.Add(T("ShoppingCart.SelectAttribute", ca2.GetLocalized(a => a.Name))); + } } } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryAliasMapper.cs b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryAliasMapper.cs index be969584e7..6cfae38fd0 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryAliasMapper.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryAliasMapper.cs @@ -16,12 +16,12 @@ 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_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 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; diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs index 88bb3da613..3c30544a0d 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs @@ -595,7 +595,7 @@ protected virtual void OnConverted(CatalogSearchQuery query, RouteData routeData protected virtual bool GetValueFor(CatalogSearchQuery query, string key, FacetGroupKind kind, out T value) { - return GetValueFor(_catalogSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); + return TryGetValueFor(_catalogSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); } } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs index 05f89aa954..d4c9c754d5 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs @@ -252,7 +252,7 @@ protected virtual void OnConverted(ForumSearchQuery query, RouteData routeData, protected virtual bool GetValueFor(ForumSearchQuery query, string key, FacetGroupKind kind, out T value) { - return GetValueFor(_forumSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); + return TryGetValueFor(_forumSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); } protected virtual void AddDates(FacetDescriptor descriptor, DateTime? selectedFrom, DateTime? selectedTo) diff --git a/src/Libraries/SmartStore.Services/Search/SearchQueryFactoryBase.cs b/src/Libraries/SmartStore.Services/Search/SearchQueryFactoryBase.cs index 998503180d..0fa710c25d 100644 --- a/src/Libraries/SmartStore.Services/Search/SearchQueryFactoryBase.cs +++ b/src/Libraries/SmartStore.Services/Search/SearchQueryFactoryBase.cs @@ -62,21 +62,19 @@ protected virtual Multimap Aliases protected virtual T GetValueFor(string key) { - T value; - return GetValueFor(key, out value) ? value : default(T); + return TryGetValueFor(key, out T value) ? value : default; } - protected virtual bool GetValueFor(string key, out T value) + protected virtual bool TryGetValueFor(string key, out T value) { var strValue = _httpContext.Request?.Unvalidated.Form?[key] ?? _httpContext.Request?.Unvalidated.QueryString?[key]; if (strValue.HasValue()) { - value = strValue.Convert(); - return true; + return CommonHelper.TryConvert(strValue, out value); } - value = default(T); + value = default; return false; } diff --git a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs index 499e977c7f..32ba800597 100644 --- a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs +++ b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs @@ -24,6 +24,7 @@ public partial class StandardPermissionProvider : IPermissionProvider public static readonly PermissionRecord ManageBlog = new PermissionRecord { Name = "Admin area. Manage Blog", SystemName = "ManageBlog", Category = "Content Management" }; public static readonly PermissionRecord ManageWidgets = new PermissionRecord { Name = "Admin area. Manage Widgets", SystemName = "ManageWidgets", Category = "Content Management" }; public static readonly PermissionRecord ManageTopics = new PermissionRecord { Name = "Admin area. Manage Topics", SystemName = "ManageTopics", Category = "Content Management" }; + public static readonly PermissionRecord ManageMenus = new PermissionRecord { Name = "Admin area. Manage Menus", SystemName = "ManageMenus", Category = "Content Management" }; public static readonly PermissionRecord ManageForums = new PermissionRecord { Name = "Admin area. Manage Forums", SystemName = "ManageForums", Category = "Content Management" }; public static readonly PermissionRecord ManageMessageTemplates = new PermissionRecord { Name = "Admin area. Manage Message Templates", SystemName = "ManageMessageTemplates", Category = "Content Management" }; public static readonly PermissionRecord ManageCountries = new PermissionRecord { Name = "Admin area. Manage Countries", SystemName = "ManageCountries", Category = "Configuration" }; @@ -79,6 +80,7 @@ public virtual IEnumerable GetPermissions() ManageBlog, ManageWidgets, ManageTopics, + ManageMenus, ManageForums, ManageMessageTemplates, ManageCountries, @@ -138,6 +140,7 @@ public virtual IEnumerable GetDefaultPermissions() ManageBlog, ManageWidgets, ManageTopics, + ManageMenus, ManageForums, ManageMessageTemplates, ManageCountries, diff --git a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs index f1c7b37d22..5e833a086e 100644 --- a/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/IXmlSitemapGenerator.cs @@ -1,6 +1,7 @@ using SmartStore.Services.Tasks; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; namespace SmartStore.Services.Seo { @@ -18,7 +19,7 @@ public partial interface IXmlSitemapGenerator /// sitemap XML document at this index, but only when the sitemap is actually indexed (otherwise null is returned) /// /// Sitemap partition - XmlSitemapPartition GetSitemapPart(int index = 0); + 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 @@ -30,7 +31,7 @@ public partial interface IXmlSitemapGenerator /// During rebuilding, requests are being served from the existing cache. /// Once rebuild is completed, the cache is updated. /// - void Rebuild(XmlSitemapBuildContext ctx); + Task RebuildAsync(XmlSitemapBuildContext ctx); /// /// Determines whether a rebuild is already running. diff --git a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs index d427ecc2d8..3a2c115b85 100644 --- a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs +++ b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs @@ -348,6 +348,7 @@ public static string GetSeName(string name, SeoSettings seoSettings) name, seoSettings == null ? false : seoSettings.ConvertNonWesternChars, seoSettings == null ? false : seoSettings.AllowUnicodeCharsInUrls, + true, seoSettings?.SeoNameCharConversion); } diff --git a/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs b/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs index bfd1186699..5e07f2a9fb 100644 --- a/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs +++ b/src/Libraries/SmartStore.Services/Seo/Tasks/RebuildXmlSitemapTask.cs @@ -10,7 +10,7 @@ namespace SmartStore.Services.Seo { - public class RebuildXmlSitemapTask : ITask + public class RebuildXmlSitemapTask : AsyncTask { private readonly IStoreService _storeService; private readonly ILanguageService _languageService; @@ -29,7 +29,7 @@ public RebuildXmlSitemapTask( _seoSettings = seoSettings; } - public void Execute(TaskExecutionContext ctx) + public override async Task ExecuteAsync(TaskExecutionContext ctx) { var stores = _storeService.GetAllStores(); @@ -42,7 +42,7 @@ public void Execute(TaskExecutionContext ctx) ProgressCallback = OnProgress }; - _generator.Rebuild(buildContext); + await _generator.RebuildAsync(buildContext); } void OnProgress(int value, int max, string msg) diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index af827690c3..df8bad3de9 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -311,7 +311,7 @@ orderby x.Id descending return result; } - }); + }, independent: true); var key = GenerateKey(entityId, entityName, languageId); if (!allActiveSlugs.TryGetValue(key, out slug)) @@ -539,7 +539,7 @@ orderby ur.Id descending return dict; } - }); + }, independent: true); } /// diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index e0f4162c2a..17a8e5356e 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -102,12 +102,12 @@ public XmlSitemapGenerator( public ILogger Logger { get; set; } - public virtual XmlSitemapPartition GetSitemapPart(int index = 0) + public virtual async Task GetSitemapPartAsync(int index = 0) { - return GetSitemapPart(index, false); + return await GetSitemapPartAsync(index, false); } - private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) + private async Task GetSitemapPartAsync(int index, bool isRetry) { Guard.NotNegative(index, nameof(index)); @@ -174,11 +174,11 @@ private XmlSitemapPartition GetSitemapPart(int index, bool isRetry) CancellationToken = CancellationToken.None }; - Rebuild(buildContext); + await RebuildAsync(buildContext); } // DRY: call self to get sitemap partition object - return GetSitemapPart(index, true); + return await GetSitemapPartAsync(index, true); } private bool SitemapFileExists(int storeId, int languageId, int index, out string path, out string name) @@ -216,7 +216,7 @@ private string GetLockFilePath(int storeId, int languageId) return _tenantFolder.Combine(_baseDir, fileName); } - public virtual void Rebuild(XmlSitemapBuildContext ctx) + public virtual async Task RebuildAsync(XmlSitemapBuildContext ctx) { Guard.NotNull(ctx, nameof(ctx)); @@ -283,7 +283,7 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) var nodes = new List(); var queries = CreateQueries(ctx); - var total = queries.GetTotalRecordCount(); + var total = await queries.GetTotalRecordCountAsync(); var totalSegments = (int)Math.Ceiling(total / (double)MaximumSiteMapNodeCount); var hasIndex = totalSegments > 1; @@ -339,7 +339,7 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) { // Commit every 5th segment (10.000 nodes) temporarily to disk to minimize RAM usage var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[language.Id]); - SaveTemp(documents, data, segment - documents.Count + (hasIndex ? 1 : 0)); + await SaveTempAsync(documents, data, segment - documents.Count + (hasIndex ? 1 : 0)); documents.Clear(); sitemaps.RemoveAll(language.Id); @@ -348,8 +348,8 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) slugs.Clear(); - GC.Collect(); - GC.WaitForPendingFinalizers(); + //GC.Collect(); + //GC.WaitForPendingFinalizers(); } // Process custom nodes @@ -363,7 +363,7 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) if (sitemaps.ContainsKey(data.Language.Id) && sitemaps[data.Language.Id].Count > 0) { var documents = GetSiteMapDocuments((IReadOnlyCollection)sitemaps[data.Language.Id]); - SaveTemp(documents, data, (segment + 1) - documents.Count + (hasIndex ? 1 : 0)); + await SaveTempAsync(documents, data, (segment + 1) - documents.Count + (hasIndex ? 1 : 0)); } else if (segment == 0) { @@ -371,7 +371,7 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) // the system will try to rebuild again. var homeNode = new XmlSitemapNode { LastMod = DateTime.UtcNow, Loc = data.BaseUrl }; var documents = GetSiteMapDocuments(new List { homeNode }); - SaveTemp(documents, data, 0); + await SaveTempAsync(documents, data, 0); } } @@ -388,7 +388,7 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) if (hasIndex && indexNodes.Any()) { var indexDocument = CreateSitemapIndexDocument(indexNodes[data.Language.Id]); - SaveTemp(new List { indexDocument }, data, 0); + await SaveTempAsync(new List { indexDocument }, data, 0); } // Save finally (actually renames temp folder) @@ -409,8 +409,8 @@ public virtual void Rebuild(XmlSitemapBuildContext ctx) } } - GC.Collect(); - GC.WaitForPendingFinalizers(); + //GC.Collect(); + //GC.WaitForPendingFinalizers(); } } } @@ -437,7 +437,7 @@ private string BuildNodeUrl(string baseUrl, NamedEntity entity, UrlRecordCollect return baseUrl + slugs.GetSlug(language.Id, entity.Id, true); } - private void SaveTemp(List documents, LanguageData data, int start) + private async Task SaveTempAsync(List documents, LanguageData data, int start) { for (int i = 0; i < documents.Count; i++) { @@ -445,7 +445,7 @@ private void SaveTemp(List documents, LanguageData data, int start) var fileName = SiteMapFileNamePattern.FormatInvariant(i + start); var filePath = _tenantFolder.Combine(data.TempDir, fileName); - _tenantFolder.CreateTextFile(filePath, documents[i]); + await _tenantFolder.CreateTextFileAsync(filePath, documents[i]); } } @@ -476,8 +476,6 @@ private void SaveFinal(LanguageData data) private IEnumerable EnumerateEntities(QueryHolder queries) { - var entities = Enumerable.Empty(); - if (queries.Categories != null) { var categories = queries.Categories.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); @@ -707,7 +705,7 @@ private QueryHolder CreateQueries(XmlSitemapBuildContext ctx) if (seoSettings.XmlSitemapIncludesCategories) { - holder.Categories = _categoryService.GetAllCategories(showHidden: false, storeId: storeId).SourceQuery; + holder.Categories = _categoryService.BuildCategoriesQuery(showHidden: false, storeId: storeId); } if (seoSettings.XmlSitemapIncludesManufacturers) @@ -726,7 +724,7 @@ private QueryHolder CreateQueries(XmlSitemapBuildContext ctx) if (seoSettings.XmlSitemapIncludesProducts) { var searchQuery = new CatalogSearchQuery() - .VisibleOnly() + .VisibleOnly(_services.WorkContext.CurrentCustomer) .VisibleIndividuallyOnly(true) .HasStoreId(storeId); @@ -810,13 +808,13 @@ class QueryHolder public IQueryable Topics { get; set; } public IQueryable Products { get; set; } - public int GetTotalRecordCount() + public async Task GetTotalRecordCountAsync() { int num = 0; - if (Categories != null) num += Categories.Count(); - if (Manufacturers != null) num += Manufacturers.Count(); - if (Topics != null) num += Topics.Count(); - if (Products != null) num += Products.Count(); + if (Categories != null) num += await Categories.CountAsync(); + if (Manufacturers != null) num += await Manufacturers.CountAsync(); + if (Topics != null) num += await Topics.CountAsync(); + if (Products != null) num += await Products.CountAsync(); //if (Products != null) num += 200000; return num; diff --git a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj index 2d0fe53eb5..723ecf1212 100644 --- a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj +++ b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj @@ -1,5 +1,6 @@  - + + Debug AnyCPU @@ -97,8 +98,11 @@ ..\..\packages\LumenWorksCsvReader.4.0.0\lib\net461\LumenWorks.Framework.IO.dll - - ..\..\packages\MaxMind.GeoIP.2.1.17\lib\net35\MaxMind.GeoIP.dll + + ..\..\packages\MaxMind.Db.2.4.0\lib\net45\MaxMind.Db.dll + + + ..\..\packages\MaxMind.GeoIP2.3.0.0\lib\net45\MaxMind.GeoIP2.dll @@ -196,9 +200,22 @@ + + + + + + + + + + + + + @@ -229,6 +246,7 @@ + @@ -265,6 +283,7 @@ + @@ -275,6 +294,9 @@ + + + @@ -404,11 +426,8 @@ - - - diff --git a/src/Libraries/SmartStore.Services/Stores/StoreService.cs b/src/Libraries/SmartStore.Services/Stores/StoreService.cs index 68a7280116..f8cde15ab0 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreService.cs @@ -18,17 +18,6 @@ public StoreService(IRepository storeRepository) _storeRepository = storeRepository; } - 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 IList GetAllStores() { var query = _storeRepository.Table @@ -63,6 +52,17 @@ public virtual void UpdateStore(Store 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) diff --git a/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs b/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs index 3ff6f7c69a..be442aa61a 100644 --- a/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs +++ b/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using SmartStore.Core; using SmartStore.Core.Domain.Tasks; @@ -41,7 +42,7 @@ public partial interface IScheduleTaskService /// Gets all pending tasks /// /// Tasks - IList GetPendingTasks(); + Task> GetPendingTasksAsync(); /// /// Inserts a task diff --git a/src/Libraries/SmartStore.Services/Tasks/ITask.cs b/src/Libraries/SmartStore.Services/Tasks/ITask.cs index 4cc75d9a75..01e576c330 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ITask.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ITask.cs @@ -1,17 +1,42 @@  +using System; +using System.Threading.Tasks; + namespace SmartStore.Services.Tasks { /// - /// Interface that should be implemented by each task + /// Interface that should be implemented by background tasks /// public partial interface ITask { /// - /// Execute task + /// Executes a task synchronously /// /// /// The execution context /// 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); + } + + public abstract class AsyncTask : IAsyncTask + { + public void Execute(TaskExecutionContext ctx) => + throw new NotSupportedException(); + + 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 ef9d7da4f0..5f0f126359 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs @@ -1,12 +1,13 @@ using System.Collections.Generic; +using System.Threading.Tasks; using SmartStore.Core.Domain.Tasks; namespace SmartStore.Services.Tasks { public interface ITaskExecutor { - void Execute( - ScheduleTask task, + Task ExecuteAsync( + ScheduleTask entity, IDictionary taskParameters = null, bool throwOnError = false); } diff --git a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs index 8a92d0abe2..665fca2d30 100644 --- a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs +++ b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs @@ -32,15 +32,7 @@ public void OnAuthentication(AuthenticationContext filterContext) var eventPublisher = EngineContext.Current.Resolve(); var logger = EngineContext.Current.Resolve().CreateLogger(); - - // The very first request must set app state to 'fully initialized' - if (!EngineContext.Current.IsFullyInitialized) - { - EngineContext.Current.IsFullyInitialized = true; - eventPublisher.Publish(new AppStartedEvent { HttpContext = filterContext.HttpContext }); - } - - ITaskScheduler taskScheduler = EngineContext.Current.Resolve(); + var taskScheduler = EngineContext.Current.Resolve(); try { diff --git a/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs b/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs index 7d5b7373a7..0b4e3ef725 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs @@ -3,6 +3,8 @@ using System.Data.Entity.Core; using System.Data.SqlClient; using System.Linq; +using System.Threading.Tasks; +using System.Data.Entity; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Common; @@ -98,7 +100,7 @@ public virtual IList GetAllTasks(bool includeDisabled = false) RetryOnDeadlockException); } - public virtual IList GetPendingTasks() + public async virtual Task> GetPendingTasksAsync() { var now = DateTime.UtcNow; var machineName = _env.MachineName; @@ -116,8 +118,8 @@ from t in _taskRepository.Table .FirstOrDefault() }); - var tasks = Retry.Run( - () => query.ToList(), + var tasks = await Retry.RunAsync( + () => query.ToListAsync(), 3, TimeSpan.FromMilliseconds(100), RetryOnDeadlockException); diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs index 466e1db24f..fa52d51967 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs @@ -9,7 +9,6 @@ namespace SmartStore.Services.Tasks { public delegate void ProgressCallback(int value, int maximum, string message); - /// /// Provides the context for the Execute method of the interface. /// diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs index 361141e2c8..93e71a4f7a 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; using Autofac; using SmartStore.Core; using SmartStore.Core.Async; @@ -43,8 +44,8 @@ public TaskExecutor( public ILogger Logger { get; set; } public Localizer T { get; set; } - public void Execute( - ScheduleTask task, + public async Task ExecuteAsync( + ScheduleTask entity, IDictionary taskParameters = null, bool throwOnError = false) { @@ -53,13 +54,13 @@ public void Execute( return; } - if (task.LastHistoryEntry == null) + if (entity.LastHistoryEntry == null) { // The task was started manually. - task.LastHistoryEntry = _scheduledTaskService.GetLastHistoryEntryByTaskId(task.Id); + entity.LastHistoryEntry = _scheduledTaskService.GetLastHistoryEntryByTaskId(entity.Id); } - if (task?.LastHistoryEntry?.IsRunning == true) + if (entity?.LastHistoryEntry?.IsRunning == true) { return; } @@ -67,14 +68,14 @@ public void Execute( bool faulted = false; bool canceled = false; string lastError = null; - ITask instance = null; + ITask task = null; string stateName = null; Type taskType = null; Exception exception = null; var historyEntry = new ScheduleTaskHistory { - ScheduleTaskId = task.Id, + ScheduleTaskId = entity.Id, IsRunning = true, MachineName = _env.MachineName.EmptyNull(), StartedOnUtc = DateTime.UtcNow @@ -82,10 +83,10 @@ public void Execute( try { - taskType = Type.GetType(task.Type); + taskType = Type.GetType(entity.Type); if (taskType == null) { - Logger.DebugFormat("Invalid scheduled task type: {0}", task.Type.NaIfEmpty()); + Logger.DebugFormat("Invalid scheduled task type: {0}", entity.Type.NaIfEmpty()); } if (taskType == null) @@ -94,8 +95,8 @@ public void Execute( if (!PluginManager.IsActivePluginAssembly(taskType.Assembly)) return; - task.ScheduleTaskHistory.Add(historyEntry); - _scheduledTaskService.UpdateTask(task); + entity.ScheduleTaskHistory.Add(historyEntry); + _scheduledTaskService.UpdateTask(entity); } catch { @@ -106,8 +107,8 @@ public void Execute( { // Task history entry has been successfully added, now we execute the task. // Create task instance. - instance = _taskResolver(taskType); - stateName = task.Id.ToString(); + 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); @@ -120,8 +121,16 @@ public void Execute( Parameters = taskParameters ?? new Dictionary() }; - Logger.DebugFormat("Executing scheduled task: {0}", task.Type); - instance.Execute(ctx); + Logger.DebugFormat("Executing scheduled task: {0}", entity.Type); + + if (task is IAsyncTask asyncTask) + { + await asyncTask.ExecuteAsync(ctx); + } + else + { + task.Execute(ctx); + } } catch (Exception ex) { @@ -132,11 +141,11 @@ public void Execute( if (canceled) { - Logger.Warn(ex, T("Admin.System.ScheduleTasks.Cancellation", task.Name)); + Logger.Warn(ex, T("Admin.System.ScheduleTasks.Cancellation", entity.Name)); } else { - Logger.Error(ex, string.Concat(T("Admin.System.ScheduleTasks.RunningError", task.Name), ": ", ex.Message)); + Logger.Error(ex, string.Concat(T("Admin.System.ScheduleTasks.RunningError", entity.Name), ": ", ex.Message)); } } finally @@ -152,9 +161,9 @@ public void Execute( if (faulted) { - if ((!canceled && task.StopOnError) || instance == null) + if ((!canceled && entity.StopOnError) || task == null) { - task.Enabled = false; + entity.Enabled = false; updateTask = true; } } @@ -165,7 +174,7 @@ public void Execute( try { - Logger.DebugFormat("Executed scheduled task: {0}. Elapsed: {1} ms.", task.Type, (now - historyEntry.StartedOnUtc).TotalMilliseconds); + Logger.DebugFormat("Executed scheduled task: {0}. Elapsed: {1} ms.", entity.Type, (now - historyEntry.StartedOnUtc).TotalMilliseconds); // Remove from AsyncState. if (stateName.HasValue()) @@ -178,9 +187,9 @@ public void Execute( Logger.Error(ex); } - if (task.Enabled) + if (entity.Enabled) { - task.NextRunUtc = _scheduledTaskService.GetNextSchedule(task); + entity.NextRunUtc = _scheduledTaskService.GetNextSchedule(entity); updateTask = true; } @@ -188,7 +197,7 @@ public void Execute( if (updateTask) { - _scheduledTaskService.UpdateTask(task); + _scheduledTaskService.UpdateTask(entity); } Throttle.Check("Delete old schedule task history entries", TimeSpan.FromDays(1), () => _scheduledTaskService.DeleteHistoryEntries() > 0); diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs index 131d0239c0..f8fca60dc9 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs @@ -112,7 +112,7 @@ private string CreateAuthToken() private string GenerateAuthTokenCacheKey(string authToken) { - return "Scheduler.AuthToken." + authToken; + return "Scheduler:AuthToken:" + authToken; } public bool VerifyAuthToken(string authToken) diff --git a/src/Libraries/SmartStore.Services/Tax/TaxService.cs b/src/Libraries/SmartStore.Services/Tax/TaxService.cs index bcf14821e3..6d5cb78344 100644 --- a/src/Libraries/SmartStore.Services/Tax/TaxService.cs +++ b/src/Libraries/SmartStore.Services/Tax/TaxService.cs @@ -140,15 +140,9 @@ protected virtual bool IsEuConsumer(Customer customer) return true; } - var country = address == null ? null : address.Country; - - if (country == null) - { - // No Country or BillingAddress set: try to resolve country from IP address - _geoCountryLookup.IsEuIpAddress(customer.LastIpAddress, out country); - } - - if (country == null || !country.SubjectToVat) + // No Country or BillingAddress set: try to resolve country from IP address + var subjectToVat = _geoCountryLookup.LookupCountry(customer.LastIpAddress)?.IsInEu == true; + if (!subjectToVat) { return false; } diff --git a/src/Libraries/SmartStore.Services/Topics/TopicService.cs b/src/Libraries/SmartStore.Services/Topics/TopicService.cs index eb8dfee3a0..07152bf79b 100644 --- a/src/Libraries/SmartStore.Services/Topics/TopicService.cs +++ b/src/Libraries/SmartStore.Services/Topics/TopicService.cs @@ -49,7 +49,7 @@ public virtual Topic GetTopicById(int topicId) if (topicId == 0) return null; - return _topicRepository.GetById(topicId); + return _topicRepository.GetByIdCached(topicId, "db.topic.id-" + topicId); } public virtual Topic GetTopicBySystemName(string systemName, int storeId = 0, bool checkPermission = true) @@ -115,7 +115,7 @@ from a in ca.DefaultIfEmpty() if (joinApplied) { - // Only distinct categories (group by ID) + // Only distinct topics (group by ID) query = from t in query group t by t.Id into tGroup orderby tGroup.Key diff --git a/src/Libraries/SmartStore.Services/app.config b/src/Libraries/SmartStore.Services/app.config index 6aa328e21b..08f9834796 100644 --- a/src/Libraries/SmartStore.Services/app.config +++ b/src/Libraries/SmartStore.Services/app.config @@ -39,6 +39,10 @@ + + + + diff --git a/src/Libraries/SmartStore.Services/packages.config b/src/Libraries/SmartStore.Services/packages.config index 497fe401d6..be5db3727d 100644 --- a/src/Libraries/SmartStore.Services/packages.config +++ b/src/Libraries/SmartStore.Services/packages.config @@ -8,10 +8,12 @@ - + + + diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs index 30fcf453b6..35fbe06a06 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs @@ -1,25 +1,34 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Web; using System.Web.Mvc; using SmartStore.AmazonPay.Services; +using SmartStore.Core.Html; +using SmartStore.Core.Logging; using SmartStore.Services.Common; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; namespace SmartStore.AmazonPay.Controllers { - public class AmazonPayCheckoutController : AmazonPayControllerBase + public class AmazonPayCheckoutController : AmazonPayControllerBase { private readonly HttpContextBase _httpContext; private readonly IAmazonPayService _apiService; private readonly IGenericAttributeService _genericAttributeService; + private readonly IOrderProcessingService _orderProcessingService; - public AmazonPayCheckoutController( + public AmazonPayCheckoutController( HttpContextBase httpContext, IAmazonPayService apiService, - IGenericAttributeService genericAttributeService) + IGenericAttributeService genericAttributeService, + IOrderProcessingService orderProcessingService) { _httpContext = httpContext; _apiService = apiService; _genericAttributeService = genericAttributeService; + _orderProcessingService = orderProcessingService; } public ActionResult OrderReferenceCreated(string orderReferenceId/*, string accessToken*/) @@ -49,9 +58,9 @@ public ActionResult OrderReferenceCreated(string orderReferenceId/*, string acce error = error.Grow(T("Plugins.Payments.AmazonPay.MissingAddressConsentToken"), " "); } } - catch (Exception exception) + catch (Exception ex) { - error = exception.Message; + error = ex.Message; } return new JsonResult { Data = new { success = success, error = error } }; @@ -107,5 +116,97 @@ public ActionResult CheckoutCompleted() return new EmptyResult(); } - } + + #region Confirmation flow + + // Ajax. + public ActionResult ConfirmOrder(string formData) + { + string redirectUrl = null; + var messages = new List(); + var success = false; + + try + { + var store = Services.StoreContext.CurrentStore; + var customer = Services.WorkContext.CurrentCustomer; + var processPaymentRequest = (_httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest) ?? new ProcessPaymentRequest(); + + processPaymentRequest.StoreId = store.Id; + processPaymentRequest.CustomerId = customer.Id; + processPaymentRequest.PaymentMethodSystemName = AmazonPayPlugin.SystemName; + + // We must check here if an order can be placed to avoid creating unauthorized Amazon payment objects. + var warnings = _orderProcessingService.GetOrderPlacementWarnings(processPaymentRequest); + + if (!warnings.Any()) + { + if (_orderProcessingService.IsMinimumOrderPlacementIntervalValid(customer, store)) + { + if (_apiService.ConfirmOrderReference()) + { + success = true; + + var state = _httpContext.GetAmazonPayState(Services.Localization); + state.FormData = formData.EmptyNull(); + } + else + { + _httpContext.Session["AmazonPayFailedPaymentReason"] = "PaymentMethodNotAllowed"; + + redirectUrl = Url.Action("PaymentMethod", "Checkout", new { area = "" }); + } + } + else + { + messages.Add(T("Checkout.MinOrderPlacementInterval")); + } + } + else + { + messages.AddRange(warnings.Select(x => HtmlUtils.ConvertPlainTextToHtml(x))); + } + } + catch (Exception ex) + { + messages.Add(ex.Message); + Logger.Error(ex); + } + + return Json(new { success, redirectUrl, messages }); + } + + public ActionResult ConfirmationResult(string authenticationStatus) + { + var state = _httpContext.GetAmazonPayState(Services.Localization); + state.SubmitForm = false; + + if (authenticationStatus.IsCaseInsensitiveEqual("Success")) + { + state.SubmitForm = true; + return RedirectToAction("Confirm", "Checkout", new { area = "" }); + } + else if (authenticationStatus.IsCaseInsensitiveEqual("Failure")) + { + // The buyer has exhausted their retry attempts and payment instrument selection on the Amazon Pay page. + // Review: redirect back to shopping cart (like AmazonRejected). + _httpContext.Session["AmazonPayFailedPaymentReason"] = "AuthenticationStatusFailure"; + + return RedirectToRoute("ShoppingCart"); + } + else + { + // authenticationStatus == "Abandoned": The buyer took action to close/cancel the MFA challenge. + // Review: redirect to checkout payment page (like InvalidPaymentMethod). + state.IsConfirmed = false; + state.FormData = null; + + _httpContext.Session["AmazonPayFailedPaymentReason"] = "AuthenticationStatusAbandoned"; + + return RedirectToAction("PaymentMethod", "Checkout", new { area = "" }); + } + } + + #endregion + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs index bce38ebbd0..669cc4c009 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Web; using System.Web.Mvc; using SmartStore.AmazonPay.Models; using SmartStore.AmazonPay.Services; using SmartStore.ComponentModel; using SmartStore.Core.Domain.Customers; using SmartStore.Services.Authentication.External; +using SmartStore.Services.Customers; using SmartStore.Services.Payments; using SmartStore.Services.Tasks; using SmartStore.Web.Framework; @@ -17,17 +21,20 @@ namespace SmartStore.AmazonPay.Controllers { public class AmazonPayController : PaymentControllerBase { - private readonly IAmazonPayService _apiService; + private readonly HttpContextBase _httpContext; + private readonly IAmazonPayService _apiService; private readonly Lazy _scheduleTaskService; private readonly Lazy _openAuthenticationService; private readonly Lazy _externalAuthenticationSettings; public AmazonPayController( - IAmazonPayService apiService, + HttpContextBase httpContext, + IAmazonPayService apiService, Lazy scheduleTaskService, Lazy openAuthenticationService, Lazy externalAuthenticationSettings) { + _httpContext = httpContext; _apiService = apiService; _scheduleTaskService = scheduleTaskService; _openAuthenticationService = openAuthenticationService; @@ -148,9 +155,57 @@ public ActionResult IPNHandler() return Content("OK"); } - // Authentication + // For payment simulation in sandbox mode. Admin and sandbox only. + public ActionResult SetSellerNote(string paymentState) + { + var customer = Services.WorkContext.CurrentCustomer; + if (!customer.IsAdmin()) + { + return HttpNotFound(); + } + + var store = Services.StoreContext.CurrentStore; + var settings = Services.Settings.LoadSetting(store.Id); + + if (!settings.UseSandbox) + { + return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Endpoint is for testing purposes and only available in sandbox mode."); + } + + var state = _httpContext.GetAmazonPayState(Services.Localization); + var sellerNote = string.Empty; // Reset payment state. + + // See https://pay.amazon.com/de/developer/documentation/lpwa/201956480 + if (paymentState.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) + { + sellerNote = "{\"SandboxSimulation\":{\"Constraint\":\"PaymentMethodNotAllowed\"}}"; + } + else if (paymentState.IsCaseInsensitiveEqual("Success")) + { + sellerNote = "{\"SandboxSimulation\":{\"PaymentAuthenticationStatus\":{\"State\":\"Success\"}}}"; + } + else if (paymentState.IsCaseInsensitiveEqual("Failure")) + { + sellerNote = "{\"SandboxSimulation\":{\"PaymentAuthenticationStatus\":{\"State\":\"Failure\"}}}"; + } + else if (paymentState.IsCaseInsensitiveEqual("Abandoned")) + { + sellerNote = "{\"SandboxSimulation\":{\"PaymentAuthenticationStatus\":{\"State\":\"Abandoned\"}}}"; + } + + var result = _apiService.SetOrderReferenceDetails(null, settings, state, sellerNote, out var errorMesage); + + var sb = new StringBuilder(); + sb.Append(result ? "ok. " : "failure. "); + sb.AppendLine(sellerNote); + sb.AppendLine(errorMesage.EmptyNull()); + + return Content(sb.ToString()); + } - [ChildActionOnly] + #region Authentication + + [ChildActionOnly] public ActionResult AuthenticationPublicInfo() { var model = _apiService.CreateViewModel(AmazonPayRequestType.AuthenticationPublicInfo, TempData); @@ -197,5 +252,7 @@ public ActionResult AuthenticationButtonHandler() return new RedirectResult(Url.LogOn(returnUrl)); } } - } + + #endregion + } } diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs index e90edbd309..2bebbbaf81 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs @@ -3,9 +3,9 @@ namespace SmartStore.AmazonPay.Controllers { - public class AmazonPayShoppingCartController : AmazonPayControllerBase + public class AmazonPayShoppingCartController : AmazonPayControllerBase { - private readonly IAmazonPayService _apiService; + private readonly IAmazonPayService _apiService; public AmazonPayShoppingCartController(IAmazonPayService apiService) { @@ -27,6 +27,7 @@ public ActionResult ShoppingCart() return GetActionResult(model); } + return new EmptyResult(); } @@ -53,5 +54,5 @@ public ActionResult MiniShoppingCart(bool renderAmazonPayView) } return new EmptyResult(); } - } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Description.txt b/src/Plugins/SmartStore.AmazonPay/Description.txt index 900e79594d..7449e587bb 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.1.5.1 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 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 a9e120bd2f..3d6e180d4f 100644 --- a/src/Plugins/SmartStore.AmazonPay/Events/EventConsumer.cs +++ b/src/Plugins/SmartStore.AmazonPay/Events/EventConsumer.cs @@ -13,7 +13,7 @@ namespace SmartStore.AmazonPay.Events { - public class EventConsumer : IConsumer, IConsumer + public class EventConsumer : IConsumer { private readonly IPluginFinder _pluginFinder; private readonly ICommonServices _services; diff --git a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml index 3ec171cd79..baa01c2299 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml @@ -45,13 +45,19 @@ Textvorschläge:
    Leider wurde die Zahlung zu Ihrer Bestellung in unserem Onlineshop von Amazon Pay zurückgewiesen. Bitte wählen Sie eine andere Zahlart. - + Leider wurde die Zahlung zu Ihrer Bestellung in unserem Onlineshop von Amazon Pay zurückgewiesen. Bitte kontaktieren Sie uns.

    ]]>
    - + + Beim Zahlungsvorgang ist ein Problem aufgetreten. Ihre Bestellung wurde nicht aufgegeben und Ihr Konto nicht belastet. + + + Mit dieser Zahlungsart ist ein Problem aufgetreten. Um Ihre Bestellung abzuschließen, wählen Sie bitte eine andere aus. + + Amazon Pay meldet @@ -111,7 +117,7 @@ Textvorschläge:
      Mitteilungstyp;Mitteilungs ID;Autorisierungs ID;Buchungs ID;Rückerstatattungs ID;Referenz ID;Status;Statusaktualisierung;Gebühr;Autorisierungsbetrag;Buchungsbetrag;Erstattungsbetrag;Sofort buchen;Erstellt am;Verfällt am - + Sandbox benutzen diff --git a/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml b/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml index bc510fac8e..caaa7ebf43 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml @@ -51,7 +51,13 @@ Text suggestions:
        Unfortunately Amazon Pay declined the payment for your order in our online shop. Please contact us.

        ]]> - + + There was a problem with your payment. Your order hasn't been placed, and you haven't been charged. + + + Something's wrong with your payment method. To place your order, try another. + + Amazon Pay reports @@ -111,7 +117,7 @@ Text suggestions:
          Message type;Message ID;Authorization ID;Capture ID;Refund ID;Reference ID;State;State update;Fee;Authorized amount;Captured amount;Refunded amount;Capture now;Creation;Expiration - + Use Sandbox diff --git a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs index d56a233de9..77c391b448 100644 --- a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs +++ b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs @@ -15,10 +15,7 @@ public AmazonPayViewModel() BillingAddress = new AddressModel(); } - public string SystemName - { - get { return AmazonPayPlugin.SystemName; } - } + public string SystemName => AmazonPayPlugin.SystemName; public string SellerId { get; set; } public string ClientId { get; set; } @@ -50,5 +47,10 @@ public string SystemName public string ShippingMethod { get; set; } public AddressModel BillingAddress { get; set; } - } + + // Confirmation flow. + public bool IsConfirmed { get; set; } + public string FormData { get; set; } + public bool SubmitForm { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Plugin.cs b/src/Plugins/SmartStore.AmazonPay/Plugin.cs index 9006d725cf..9524984a8d 100644 --- a/src/Plugins/SmartStore.AmazonPay/Plugin.cs +++ b/src/Plugins/SmartStore.AmazonPay/Plugin.cs @@ -70,12 +70,6 @@ public override void Uninstall() base.Uninstall(); } - public override PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest) - { - var result = _apiService.PreProcessPayment(processPaymentRequest); - return result; - } - public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) { var result = _apiService.ProcessPayment(processPaymentRequest); diff --git a/src/Plugins/SmartStore.AmazonPay/Scripts/jquery.deserialize.js b/src/Plugins/SmartStore.AmazonPay/Scripts/jquery.deserialize.js new file mode 100644 index 0000000000..13c00e2a0d --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Scripts/jquery.deserialize.js @@ -0,0 +1,276 @@ +/** + * Copyright (C) 2017 Kyle Florence + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * @website https://github.com/kflorence/jquery-deserialize/ + * @version 2.0.0-rc1 + * + * Dual licensed under the MIT and GPLv2 licenses. + */ +(function( factory ) { + if ( typeof module === "object" && module.exports ) { + // Node/CommonJS + module.exports = factory( require( "jquery" ) ); + + } else { + // Browser globals + factory( window.jQuery ); + } +}(function( $ ) { + + /** + * Updates a key/valueArray with the given property and value. Values will always be stored as arrays. + * + * @param prop The property to add the value to. + * @param value The value to add. + * @param obj The object to update. + * @returns {object} Updated object. + */ + function updateKeyValueArray( prop, value, obj ) { + var current = obj[ prop ]; + + if ( current === undefined ) { + obj[ prop ] = [ value ]; + + } else { + current.push( value ); + } + + return obj; + } + + /** + * Get all of the fields contained within the given elements by name. + * + * @param $elements jQuery object of elements. + * @param filter Custom filter to apply to the list of fields. + * @returns {object} All of the fields contained within the given elements, keyed by name. + */ + function getFieldsByName( $elements, filter ) { + var elementsByName = {}; + + // Extract fields from elements + var fields = $elements + .map(function convertFormToElements() { + return this.elements ? $.makeArray( this.elements ) : this; + }) + .filter( filter || ":input:not(:disabled)" ) + .get(); + + $.each( fields, function( index, field ) { + updateKeyValueArray( field.name, field, elementsByName ); + }); + + return elementsByName; + } + + /** + * Figure out the type of an element. Input type will be used first, falling back to nodeName. + * + * @param element DOM element to check type of. + * @returns {string} The element's type. + */ + function getElementType( element ) { + return ( element.type || element.nodeName ).toLowerCase(); + } + + /** + * Normalize the provided data into a key/valueArray store. + * + * @param data The data provided by the user to the plugin. + * @returns {object} The data normalized into a key/valueArray store. + */ + function normalizeData( data ) { + var normalized = {}; + var rPlus = /\+/g; + + // Convert data from .serializeObject() notation + if ( $.isPlainObject( data ) ) { + $.extend( normalized, data ); + + // Convert non-array values into an array + $.each( normalized, function( name, value ) { + if ( !$.isArray( value ) ) { + normalized[ name ] = [ value ]; + } + }); + + // Convert data from .serializeArray() notation + } else if ( $.isArray( data ) ) { + $.each( data, function( index, field ) { + updateKeyValueArray( field.name, field.value, normalized ); + }); + + // Convert data from .serialize() notation + } else if ( typeof data === "string" ) { + $.each( data.split( "&" ), function( index, field ) { + var current = field.split( "=" ); + var name = decodeURIComponent( current[ 0 ].replace( rPlus, "%20" ) ); + var value = decodeURIComponent( current[ 1 ].replace( rPlus, "%20" ) ); + updateKeyValueArray( name, value, normalized ); + }); + } + + return normalized; + } + + /** + * Map of property name -> element types. + * + * @type {object} + */ + var updateTypes = { + checked: [ + "radio", + "checkbox" + ], + selected: [ + "option", + "select-one", + "select-multiple" + ], + value: [ + "button", + "color", + "date", + "datetime", + "datetime-local", + "email", + "hidden", + "month", + "number", + "password", + "range", + "reset", + "search", + "submit", + "tel", + "text", + "textarea", + "time", + "url", + "week" + ] + }; + + /** + * Get the property to update on an element being updated. + * + * @param element The DOM element to get the property for. + * @returns The name of the property to update if element is supported, otherwise `undefined`. + */ + function getPropertyToUpdate( element ) { + var type = getElementType( element ); + var elementProperty = undefined; + + $.each( updateTypes, function( property, types ) { + if ( $.inArray( type, types ) > -1 ) { + elementProperty = property; + return false; + } + }); + + return elementProperty; + } + + /** + * Update the element based on the provided data. + * + * @param element The DOM element to update. + * @param elementIndex The index of this element in the list of elements with the same name. + * @param value The serialized element value. + * @param valueIndex The index of the value in the list of values for elements with the same name. + * @param callback A function to call if the value of an element was updated. + */ + function update( element, elementIndex, value, valueIndex, callback ) { + var property = getPropertyToUpdate( element ); + + // Handle value inputs + // If there are multiple value inputs with the same name, they will be populated by matching indexes. + if ( property == "value" && elementIndex == valueIndex ) { + element.value = value; + callback.call( element, value ); + + // Handle select menus, checkboxes and radio buttons + } else if ( property == "checked" || property == "selected" ) { + var fields = []; + + // Extract option fields from select menus + if ( element.options ) { + $.each( element.options, function( index, option ) { + fields.push( option ); + }); + + } else { + fields.push( element ); + } + + // #37: Remove selection from multiple select menus before deserialization + if ( element.multiple && valueIndex == 0 ) { + element.selectedIndex = -1; + } + + $.each( fields, function( index, field ) { + if ( field.value == value ) { + field[ property ] = true; + callback.call( field, value ); + } + }); + } + } + + /** + * Default plugin options. + * + * @type {object} + */ + var defaultOptions = { + change: $.noop, + complete: $.noop + }; + + /** + * The $.deserialize function. + * + * @param data The data to deserialize. + * @param options Additional options. + * @returns {jQuery} The jQuery object that was provided to the plugin. + */ + $.fn.deserialize = function( data, options ) { + + // Backwards compatible with old arguments: data, callback + if ( $.isFunction( options ) ) { + options = { complete: options }; + } + + options = $.extend( defaultOptions, options || {} ); + data = normalizeData( data ); + + var elementsByName = getFieldsByName( this, options.filter ); + + $.each( data, function( name, values ) { + $.each( elementsByName[ name ], function( elementIndex, element ) { + $.each( values, function( valueIndex, value ) { + update( element, elementIndex, value, valueIndex, options.change ); + }); + }); + }); + + options.complete.call( this ); + + return this; + }; +})); diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs index 4b7ef18f9d..a24c0646f7 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using AmazonPay; @@ -22,18 +21,17 @@ 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; using SmartStore.Services; using SmartStore.Services.Authentication.External; -using SmartStore.Services.Catalog; using SmartStore.Services.Common; using SmartStore.Services.Customers; using SmartStore.Services.Directory; using SmartStore.Services.Helpers; using SmartStore.Services.Localization; -using SmartStore.Services.Messages; using SmartStore.Services.Orders; using SmartStore.Services.Payments; using SmartStore.Web; @@ -41,30 +39,27 @@ namespace SmartStore.AmazonPay.Services { - public partial class AmazonPayService : IAmazonPayService + 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 IOrderTotalCalculationService _orderTotalCalculationService; private readonly ICurrencyService _currencyService; private readonly ICustomerService _customerService; private readonly ICountryService _countryService; private readonly IStateProvinceService _stateProvinceService; - private readonly IAddressService _addressService; private readonly IOrderService _orderService; private readonly IOrderProcessingService _orderProcessingService; + private readonly IOrderTotalCalculationService _orderTotalCalculationService; - private readonly IPriceFormatter _priceFormatter; private readonly IDateTimeHelper _dateTimeHelper; private readonly IPluginFinder _pluginFinder; private readonly Lazy _authorizer; private readonly AddressSettings _addressSettings; private readonly OrderSettings _orderSettings; private readonly CompanyInformationSettings _companyInformationSettings; - private readonly Lazy _externalAuthenticationSettings; public AmazonPayService( HttpContextBase httpContext, @@ -72,45 +67,39 @@ public AmazonPayService( ICommonServices services, IPaymentService paymentService, IGenericAttributeService genericAttributeService, - IOrderTotalCalculationService orderTotalCalculationService, ICurrencyService currencyService, ICustomerService customerService, ICountryService countryService, IStateProvinceService stateProvinceService, - IAddressService addressService, IOrderService orderService, IOrderProcessingService orderProcessingService, - IPriceFormatter priceFormatter, + IOrderTotalCalculationService orderTotalCalculationService, IDateTimeHelper dateTimeHelper, IPluginFinder pluginFinder, Lazy authorizer, AddressSettings addressSettings, OrderSettings orderSettings, - CompanyInformationSettings companyInformationSettings, - Lazy externalAuthenticationSettings) + CompanyInformationSettings companyInformationSettings) { _httpContext = httpContext; _orderRepository = orderRepository; _services = services; _paymentService = paymentService; _genericAttributeService = genericAttributeService; - _orderTotalCalculationService = orderTotalCalculationService; _currencyService = currencyService; _customerService = customerService; _countryService = countryService; _stateProvinceService = stateProvinceService; - _addressService = addressService; _orderService = orderService; _orderProcessingService = orderProcessingService; + _orderTotalCalculationService = orderTotalCalculationService; - _priceFormatter = priceFormatter; _dateTimeHelper = dateTimeHelper; _pluginFinder = pluginFinder; _authorizer = authorizer; _addressSettings = addressSettings; _orderSettings = orderSettings; _companyInformationSettings = companyInformationSettings; - _externalAuthenticationSettings = externalAuthenticationSettings; T = NullLocalizer.Instance; Logger = NullLogger.Instance; @@ -144,8 +133,7 @@ public void SetupConfiguration(ConfigurationModel model) model.KeyShareUrl = GetPluginUrl("ShareKey", store.SslEnabled); model.LanguageLocale = language.UniqueSeoCode.ToAmazonLanguageCode('_'); model.MerchantStoreDescription = store.Name.Truncate(2048); - - model.MerchantPrivacyNoticeUrl = urlHelper.RouteUrl("Topic", new { SeName = urlHelper.TopicSeName("privacyinfo") }, store.SslEnabled ? "https" : "http"); + model.MerchantPrivacyNoticeUrl = WebHelper.GetAbsoluteUrl(urlHelper.Topic("privacyinfo"), _httpContext.Request, protocol: store.SslEnabled ? "https" : "http"); model.MerchantSandboxIpnUrl = model.IpnUrl; model.MerchantProductionIpnUrl = model.IpnUrl; @@ -353,7 +341,11 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic var state = _httpContext.GetAmazonPayState(_services.Localization); model.OrderReferenceId = state.OrderReferenceId; model.AddressConsentToken = state.AccessToken; - //model.IsOrderConfirmed = state.IsOrderConfirmed; + model.IsConfirmed = state.IsConfirmed; + model.FormData = state.FormData; + model.SubmitForm = state.SubmitForm; + + state.SubmitForm = false; if (type == AmazonPayRequestType.ShippingMethod || type == AmazonPayRequestType.PaymentMethod) { @@ -365,8 +357,6 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic } } - var currency = store.PrimaryStoreCurrency; - model.SellerId = settings.SellerId; model.ClientId = settings.ClientId; model.IsShippable = cart.RequiresShipping(); @@ -391,12 +381,18 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic model.ButtonColor = settings.PayButtonColor; model.ButtonSize = settings.PayButtonSize; - var failedPaymentReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; - if (failedPaymentReason.IsCaseInsensitiveEqual("AmazonRejected")) - { - model.Logout = true; - _services.Notifier.Error(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthorizationSoftDeclineMessage"))); - } + var failureReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; + if (failureReason.IsCaseInsensitiveEqual("AmazonRejected")) + { + model.Logout = true; + _services.Notifier.Error(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthorizationSoftDeclineMessage"))); + } + else if (failureReason.IsCaseInsensitiveEqual("AuthenticationStatusFailure")) + { + model.Logout = true; + _services.Notifier.Error(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthenticationStatusFailureMessage"))); + } + _httpContext.Session.SafeRemove("AmazonPayFailedPaymentReason"); } else if (type == AmazonPayRequestType.AuthenticationPublicInfo) @@ -489,24 +485,29 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic } else { - LogError(getOrderResponse); + LogError(getOrderRequest, getOrderResponse); } } } else if (type == AmazonPayRequestType.PaymentMethod) { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, AmazonPayPlugin.SystemName, store.Id); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, AmazonPayPlugin.SystemName, store.Id); - var failedPaymentReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; - if (failedPaymentReason.IsCaseInsensitiveEqual("InvalidPaymentMethod")) + var paymentFailure = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; + + if (paymentFailure.IsCaseInsensitiveEqual("InvalidPaymentMethod")) { _services.Notifier.Warning(new LocalizedString(T("Payment.PayingFailed"))); } - else if (failedPaymentReason.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) + else if (paymentFailure.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) { _services.Notifier.Warning(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthorizationSoftDeclineMessage"))); } - else + else if (paymentFailure.IsCaseInsensitiveEqual("AuthenticationStatusAbandoned")) + { + _services.Notifier.Warning(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthenticationStatusAbandonedMessage"))); + } + else { decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart); if (shoppingCartTotalBase.HasValue) @@ -523,7 +524,7 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); if (!setOrderResponse.GetSuccess()) { - LogError(setOrderResponse); + LogError(setOrderRequest, setOrderResponse); } } } @@ -532,7 +533,7 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; if (paymentRequest == null) { - _httpContext.Session["OrderPaymentInfo"] = new ProcessPaymentRequest(); + _httpContext.Session["OrderPaymentInfo"] = new ProcessPaymentRequest { OrderGuid = Guid.NewGuid() }; } } else if (type == AmazonPayRequestType.OrderReviewData) @@ -540,8 +541,10 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic if (model.IsShippable) { var shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, store.Id); - if (shippingOption != null) - model.ShippingMethod = shippingOption.Name; + if (shippingOption != null) + { + model.ShippingMethod = shippingOption.Name; + } } if (customer.BillingAddress != null) @@ -550,10 +553,10 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic } } } - catch (Exception exception) + catch (Exception ex) { - Logger.Error(exception); - _services.Notifier.Error(new LocalizedString(exception.Message)); + Logger.Error(ex); + _services.Notifier.Error(new LocalizedString(ex.Message)); } return model; @@ -630,7 +633,7 @@ private void ProcessCaptureResult(Client client, AmazonPaySettings settings, Ord } else { - LogError(authDetailsResponse); + LogError(authDetailsRequest, authDetailsResponse); } } @@ -792,7 +795,7 @@ public void CloseOrderReference(AmazonPaySettings settings, Order order) } else { - LogError(closeResponse, true); + LogError(closeRequest, closeResponse, true); } } } @@ -837,9 +840,9 @@ public void AddCustomerOrderNoteLoop(AmazonPayActionState state) Thread.Sleep(sleepMillSec); } } - catch (Exception exception) + catch (Exception ex) { - Logger.Error(exception); + Logger.Error(ex); } } @@ -920,101 +923,124 @@ public void GetBillingAddress() } else { - LogError(getOrderResponse); + LogError(getOrderRequest, getOrderResponse); } } - public PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest request) - { - // Fulfill the Amazon checkout. - var result = new PreProcessPaymentResult(); - - try - { - var store = _services.StoreService.GetStoreById(request.StoreId); - - if (!IsPaymentMethodActive(store.Id, true)) - { - result.AddError(T("Plugins.Payments.AmazonPay.PaymentMethodNotActive", store.Name)); - return result; - } - - var orderGuid = request.OrderGuid.ToString(); - var customer = _customerService.GetCustomerById(request.CustomerId); - var settings = _services.Settings.LoadSetting(store.Id); - var state = _httpContext.GetAmazonPayState(_services.Localization); - var client = CreateClient(settings); - - var failedPaymentReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; - if (!failedPaymentReason.IsCaseInsensitiveEqual("InvalidPaymentMethod")) - { - var setOrderRequest = new SetOrderReferenceDetailsRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(state.OrderReferenceId) - .WithPlatformId(PlatformId) - .WithAmount(request.OrderTotal) - .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) - .WithSellerOrderId(orderGuid) - .WithStoreName(store.Name); - - // See https://pay.amazon.com/de/developer/documentation/lpwa/201956480 - //{"SandboxSimulation":{"Constraint":"PaymentMethodNotAllowed"}} - //if (settings.UseSandbox) - //{ - // var orderReferenceNote = _services.Settings.GetSettingByKey("SmartStore.AmazonPay.SellerOrderReferenceNote"); - // if (orderReferenceNote.HasValue()) - // { - // setOrderRequest = setOrderRequest.WithSellerNote(orderReferenceNote); - // } - //} - - var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); - if (setOrderResponse.GetSuccess()) - { - 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) - { - result.Errors.Add($"{descriptions[idx]} ({id})"); - } - - if (id.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) - { - // Must be redirected to checkout payment page. - _httpContext.Session["AmazonPayFailedPaymentReason"] = id; - - var urlHelper = new UrlHelper(_httpContext.Request.RequestContext); - _httpContext.Response.Redirect(urlHelper.Action("PaymentMethod", "Checkout", new { area = "" })); - } - } - } - } - else - { - var message = LogError(setOrderResponse); - result.AddError(message); - } - - if (!result.Success) - { - return result; - } - } - } - catch (Exception exception) - { - Logger.Error(exception); - result.AddError(exception.Message); - } - - return result; - } + 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) + { + 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) { @@ -1037,30 +1063,24 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) var captureNow = settings.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture; var state = _httpContext.GetAmazonPayState(_services.Localization); var client = CreateClient(settings); - AuthorizeResponse authResponse = null; + AuthorizeRequest authRequest = null; + AuthorizeResponse authResponse = null; informCustomerAboutErrors = settings.InformCustomerAboutErrors; informCustomerAddErrors = settings.InformCustomerAddErrors; - // Confirm order. This already generates the payment object at Amazon. - var confirmRequest = new ConfirmOrderReferenceRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(state.OrderReferenceId); - - client.ConfirmOrderReference(confirmRequest); - // Authorize. if (settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Omnichronous) { // First try synchronously. - authResponse = AuthorizePayment(settings, state, store, request, client, true); + 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); + authResponse = AuthorizePayment(settings, state, store, request, client, false, out authRequest); } else { @@ -1070,7 +1090,7 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) else { isSynchronous = settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Synchronous; - authResponse = AuthorizePayment(settings, state, store, request, client, isSynchronous); + authResponse = AuthorizePayment(settings, state, store, request, client, isSynchronous, out authRequest); } // Process authorization response. @@ -1128,6 +1148,10 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) // 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 = "" })); } @@ -1135,13 +1159,13 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) } else { - error = LogError(authResponse); + error = LogError(authRequest, authResponse); } } - catch (Exception exception) + catch (Exception ex) { - Logger.Error(exception); - error = exception.Message; + Logger.Error(ex); + error = ex.Message; } if (error.HasValue()) @@ -1168,12 +1192,12 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) state.Errors.AddRange(orderNoteErrors); } - AsyncRunner.Run((container, ct, o) => + AsyncRunner.Run((c, ct, o) => { var obj = o as AmazonPayActionState; - container.Resolve().AddCustomerOrderNoteLoop(obj); + c.Resolve().AddCustomerOrderNoteLoop(obj); }, - state, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + state); } return result; @@ -1225,7 +1249,7 @@ public void PostProcessPayment(PostProcessPaymentRequest request) } else { - LogError(closeResponse, true); + LogError(closeRequest, closeResponse, true); } } @@ -1272,7 +1296,7 @@ public CapturePaymentResult Capture(CapturePaymentRequest request) } else { - var message = LogError(captureResponse); + var message = LogError(captureRequest, captureResponse); result.AddError(message); } } @@ -1325,7 +1349,7 @@ public RefundPaymentResult Refund(RefundPaymentRequest request) } else { - var message = LogError(refundResponse); + var message = LogError(refundRequest, refundResponse); result.AddError(message); } } @@ -1380,7 +1404,7 @@ public VoidPaymentResult Void(VoidPaymentRequest request) } else { - var message = LogError(cancelResponse); + var message = LogError(cancelRequest, cancelResponse); result.AddError(message); } } @@ -1551,7 +1575,7 @@ orderby x.Id descending } else { - LogError(authDetailsResponse); + LogError(authDetailsRequest, authDetailsResponse); } } @@ -1579,7 +1603,7 @@ orderby x.Id descending } else { - LogError(captureDetailsResponse); + LogError(captureDetailsRequest, captureDetailsResponse); } } } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs index 11e22c003b..baf21a850a 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs @@ -441,11 +441,18 @@ private string GetRandomId(string prefix) return str.Truncate(32); } - private string LogError(IResponse response, bool isWarning = false) + private string LogError(DelegateRequest request, IResponse response, bool isWarning = false) { - var message = $"{response.GetErrorMessage().NaIfEmpty()} ({response.GetErrorCode().NaIfEmpty()})"; + var message = string.Empty; - Logger.Log(isWarning ? LogLevel.Warning : LogLevel.Error, new Exception(response.GetJson()), message, null); + if (response != null) + { + var requestMethod = request != null ? request.GetAction() : null; + + message = $"{requestMethod.NaIfEmpty()} --> {response.GetErrorCode().NaIfEmpty()}. {response.GetErrorMessage().NaIfEmpty()}"; + + Logger.Log(isWarning ? LogLevel.Warning : LogLevel.Error, new Exception(response.GetJson()), message, null); + } return message; } @@ -563,9 +570,10 @@ private AuthorizeResponse AuthorizePayment( Store store, ProcessPaymentRequest request, Client client, - bool synchronously) + bool synchronously, + out AuthorizeRequest authRequest) { - var authRequest = new AuthorizeRequest() + authRequest = new AuthorizeRequest() .WithMerchantId(settings.SellerId) .WithAmazonOrderReferenceId(state.OrderReferenceId) .WithAuthorizationReferenceId(GetRandomId("Authorize")) @@ -578,19 +586,16 @@ private AuthorizeResponse AuthorizePayment( authRequest = authRequest.WithTransactionTimeout(0); } - // See https://pay.amazon.com/de/developer/documentation/lpwa/201956480 - //{"SandboxSimulation": {"State":"Declined", "ReasonCode":"InvalidPaymentMethod", "PaymentMethodUpdateTimeInMins":5}} - //{"SandboxSimulation": {"State":"Declined", "ReasonCode":"AmazonRejected"}} - //if (settings.UseSandbox) - //{ - // var authNote = _services.Settings.GetSettingByKey("SmartStore.AmazonPay.SellerAuthorizationNote"); - // if (authNote.HasValue()) - // { - // authRequest = authRequest.WithSellerAuthorizationNote(authNote); - // } - //} - - var authResponse = client.Authorize(authRequest); + // See https://pay.amazon.com/de/developer/documentation/lpwa/201956480 + //{"SandboxSimulation": {"State":"Declined", "ReasonCode":"InvalidPaymentMethod", "PaymentMethodUpdateTimeInMins":5}} + //{"SandboxSimulation": {"State":"Declined", "ReasonCode":"AmazonRejected"}} + //if (settings.UseSandbox) + //{ + // var authNote = _services.Settings.GetSettingByKey("SmartStore.AmazonPay.SellerAuthorizationNote"); + // authRequest = authRequest.WithSellerAuthorizationNote(authNote); + //} + + var authResponse = client.Authorize(authRequest); return authResponse; } } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs index 7d54a8456b..8836575653 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs @@ -10,7 +10,13 @@ public class AmazonPayCheckoutState { public string OrderReferenceId { get; set; } public string AccessToken { get; set; } - } + + // Confirmation flow. + public bool DetailsSet { get; set; } + public bool IsConfirmed { get; set; } + public string FormData { get; set; } + public bool SubmitForm { get; set; } + } public class AmazonPayActionState diff --git a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs index 6f3f727d48..f4adc780e2 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs @@ -1,5 +1,6 @@ using System.Web; using System.Web.Mvc; +using AmazonPay; using SmartStore.AmazonPay.Models; using SmartStore.Core.Domain.Orders; using SmartStore.Services.Authentication.External; @@ -7,7 +8,7 @@ namespace SmartStore.AmazonPay.Services { - public partial interface IAmazonPayService : IExternalProviderAuthorizer + public partial interface IAmazonPayService : IExternalProviderAuthorizer { void SetupConfiguration(ConfigurationModel model); @@ -19,7 +20,14 @@ public partial interface IAmazonPayService : IExternalProviderAuthorizer void GetBillingAddress(); - PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest request); + bool SetOrderReferenceDetails( + Client client, + AmazonPaySettings settings, + AmazonPayCheckoutState state, + string sellerNote, + out string errorMessage); + + bool ConfirmOrderReference(); ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request); diff --git a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs index f77931cc01..1eb4378dfa 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs @@ -116,7 +116,7 @@ internal static AmazonPayCheckoutState GetAmazonPayState(this HttpContextBase ht return state; } - internal static Order GetOrderByAmazonId(this IRepository orderRepository, string amazonId) + internal static Order GetOrderByAmazonId(this IRepository orderRepository, string amazonId) { // S02-9777218-8608106 OrderReferenceId // S02-9777218-8608106-A088344 Auth ID diff --git a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj index 6700cd8636..b2db65457e 100644 --- a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj +++ b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj @@ -1,6 +1,6 @@  - - + + @@ -78,8 +78,8 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\AmazonPay.3.3.1\lib\net20\AmazonPay.dll + + ..\..\packages\AmazonPay.3.5.0\lib\net20\AmazonPay.dll True @@ -182,6 +182,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayShoppingCart/ScriptingReadWidgets.cshtml b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayShoppingCart/ScriptingReadWidgets.cshtml index 944e80302a..86fe47e92c 100644 --- a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayShoppingCart/ScriptingReadWidgets.cshtml +++ b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayShoppingCart/ScriptingReadWidgets.cshtml @@ -1,9 +1,11 @@ -@model SmartStore.AmazonPay.Models.AmazonPayViewModel +@using SmartStore.AmazonPay +@model SmartStore.AmazonPay.Models.AmazonPayViewModel @{ Html.AddCssFileParts(true, Url.Content("~/Plugins/SmartStore.AmazonPay/Content/SmartStore.AmazonPay.css")); + Html.AddScriptParts(true, "~/Plugins/SmartStore.AmazonPay/Scripts/jquery.deserialize.js"); } - diff --git a/src/Plugins/SmartStore.AmazonPay/packages.config b/src/Plugins/SmartStore.AmazonPay/packages.config index 0486d9903c..e78470d805 100644 --- a/src/Plugins/SmartStore.AmazonPay/packages.config +++ b/src/Plugins/SmartStore.AmazonPay/packages.config @@ -1,6 +1,6 @@  - + diff --git a/src/Plugins/SmartStore.Clickatell/Description.txt b/src/Plugins/SmartStore.Clickatell/Description.txt index fa7b60c219..003d6a572e 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.1.5 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.Clickatell.dll ResourceRootKey: Plugins.Sms.Clickatell \ No newline at end of file diff --git a/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs b/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs index 9af73ff01e..8ff1936f22 100644 --- a/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs +++ b/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs @@ -7,7 +7,7 @@ namespace SmartStore.Clickatell { - public class OrderPlacedEventConsumer : IConsumer + public class OrderPlacedEventConsumer : IConsumer { private readonly ICommonServices _services; private readonly ClickatellSettings _clickatellSettings; diff --git a/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj b/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj index abcbcb82e0..decee9a5f7 100644 --- a/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj +++ b/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs b/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs new file mode 100644 index 0000000000..a089af9c47 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using FluentValidation; +using FluentValidation.Attributes; +using Newtonsoft.Json; +using SmartStore.Services.Cms.Blocks; +using SmartStore.Web.Framework; + +namespace SmartStore.DevTools.Blocks +{ + /// + /// The block handler is the controller which is responsible for loading, instantiating, storing and rendering block types. + /// The 'BlockHandlerBase' abstract class already implements all relevant parts. + /// 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 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. + + 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 + block.MyProperties.Add(new SelectListItem { Text = "Item1", Value = "1" }); + } + else if (viewMode == StoryViewMode.GridEdit) + { + // Manipulate properties especially for the grid edit mode e.g. turn of any animation which could distract the user from editing the grid. + //block.Autoplay = false; + } + + return block; + } + + 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. + + 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); + // } + //} + + //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))] + public class SampleBlock : IBlock + { + [SmartResourceDisplayName("Plugins.SmartStore.DevTools.Block.Property")] + public string Property { get; set; } + + // By default a block instance will be converted to JSON and stored in the 'Model' field of the 'PageStoryBlock' table. + // If your block type contains some special properties - e.g. volatile data for the edit mode - and you don't want them to be persisted, add the [JsonIgnore] attribute to your property. + [JsonIgnore] + public IList MyProperties { get; set; } + } + + /// + /// This is the validator which is used to validate the user input while editing your block + /// + public partial class SampleBlockValidator : AbstractValidator + { + public SampleBlockValidator() + { + RuleFor(x => x.Property).NotEmpty(); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs index 98c3b165e4..28827a7307 100644 --- a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs +++ b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs @@ -1,6 +1,8 @@ using System.Web.Mvc; +using SmartStore.DevTools.Blocks; using SmartStore.DevTools.Models; using SmartStore.Services; +using SmartStore.Services.Cms.Blocks; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; @@ -56,6 +58,13 @@ 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); + } + [AdminAuthorize, AdminThemed] public ActionResult BackendExtension() { diff --git a/src/Plugins/SmartStore.DevTools/Description.txt b/src/Plugins/SmartStore.DevTools/Description.txt index 15391bbfbc..54399e9fcf 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) SystemName: SmartStore.DevTools Group: Developer -Version: 3.1.5 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.DevTools.dll ResourceRootKey: Plugins.Developer.DevTools \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs b/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs index b48c84c76a..cb5f929564 100644 --- a/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs +++ b/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs @@ -3,14 +3,10 @@ using SmartStore.Services; using SmartStore.Web.Framework.Events; using SmartStore.Web.Framework.Modelling; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; namespace SmartStore.DevTools.Events { - public class CustomTab : IConsumer, IConsumer + public class CustomTab : IConsumer { private readonly ICommonServices _services; diff --git a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj index 3f2db6a143..025ef8e5bf 100644 --- a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj +++ b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj @@ -1,5 +1,6 @@  - + + @@ -92,6 +93,14 @@ ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll False + + ..\..\packages\FluentValidation.7.4.0\lib\net45\FluentValidation.dll + False + + + ..\..\packages\FluentValidation.Mvc5.7.4.0\lib\net45\FluentValidation.Mvc.dll + False + ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll False @@ -105,6 +114,10 @@ ..\..\packages\MiniProfiler.EF6.3.0.11\lib\net40\MiniProfiler.EntityFramework6.dll True + + ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll + False + @@ -150,6 +163,7 @@ Properties\AssemblyVersionInfo.cs + @@ -238,6 +252,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/src/Plugins/SmartStore.DevTools/Views/DevTools/MiniProfiler.cshtml b/src/Plugins/SmartStore.DevTools/Views/DevTools/MiniProfiler.cshtml index d1cffb6efe..ba456f87eb 100644 --- a/src/Plugins/SmartStore.DevTools/Views/DevTools/MiniProfiler.cshtml +++ b/src/Plugins/SmartStore.DevTools/Views/DevTools/MiniProfiler.cshtml @@ -1,3 +1,8 @@ @model dynamic -@StackExchange.Profiling.MiniProfiler.RenderIncludes(useExistingjQuery: true, showControls: true) +@if (base.ViewContext.IsBareBonePage()) +{ + return; +} + +@StackExchange.Profiling.MiniProfiler.RenderIncludes(useExistingjQuery: true, showControls: true) \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Edit.cshtml b/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Edit.cshtml new file mode 100644 index 0000000000..14bf178d59 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Edit.cshtml @@ -0,0 +1,15 @@ +@model SampleBlock + +@using SmartStore.DevTools.Blocks + +
          +
          +
          + @Html.SmartLabelFor(model => model.Property) +
          +
          + @Html.EditorFor(model => model.Property) + @Html.ValidationMessageFor(model => model.Property) +
          +
          +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Public.cshtml b/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Public.cshtml new file mode 100644 index 0000000000..0ff1a80f66 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Public.cshtml @@ -0,0 +1,14 @@ +@model SampleBlock + +@using SmartStore.DevTools.Blocks + +@{ + if (!Model.Property.HasValue()) + { + return; + } +} + +
          + @Model.Property +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/packages.config b/src/Plugins/SmartStore.DevTools/packages.config index 355e10d46a..d040144b1c 100644 --- a/src/Plugins/SmartStore.DevTools/packages.config +++ b/src/Plugins/SmartStore.DevTools/packages.config @@ -3,10 +3,13 @@ + + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.DiscountRules/Description.txt b/src/Plugins/SmartStore.DiscountRules/Description.txt index a399d1f92b..b7b46e9263 100644 --- a/src/Plugins/SmartStore.DiscountRules/Description.txt +++ b/src/Plugins/SmartStore.DiscountRules/Description.txt @@ -2,8 +2,8 @@ Description: Contains common discount requirement rule providers like "Billing country is", "Customer role is", "Had spent amount" etc. Group: Marketing SystemName: SmartStore.DiscountRules -Version: 3.1.5 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 0 FileName: SmartStore.DiscountRules.dll ResourceRootKey: Plugins.SmartStore.DiscountRules diff --git a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj index a91c5b48ac..97356664ed 100644 --- a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj +++ b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj @@ -1,6 +1,6 @@  - - + + diff --git a/src/Plugins/SmartStore.FacebookAuth/Description.txt b/src/Plugins/SmartStore.FacebookAuth/Description.txt index 2c84b47230..447d4bf86d 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.1.5 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 5 FileName: SmartStore.FacebookAuth.dll ResourceRootKey: Plugins.ExternalAuth.Facebook \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj index 3c3eac8964..832cc36b51 100644 --- a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj +++ b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/PublicInfo.cshtml b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/PublicInfo.cshtml index 2d894ad278..8dbc3b16e9 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/PublicInfo.cshtml +++ b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/PublicInfo.cshtml @@ -5,7 +5,7 @@ } + href="@Url.RouteUrl("SmartStore.FacebookAuth", new { ReturnUrl = HttpContext.Current.Request.QueryString["returnUrl"], action = "Login" })"> \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt index c12d33b49a..ac4936f475 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt +++ b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Google Analytics SystemName: SmartStore.GoogleAnalytics Group: Analytics -Version: 3.1.5 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.GoogleAnalytics.dll ResourceRootKey: Plugins.Widgets.GoogleAnalytics diff --git a/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj b/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj index 1927f9a6bf..b48e5d3f5d 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj +++ b/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt index 1a363bf017..3b87cd8d04 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.1.5 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.GoogleMerchantCenter.dll diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs index f129d4c009..211c45083d 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs @@ -8,9 +8,7 @@ namespace SmartStore.GoogleMerchantCenter { - public class Events : - IConsumer, - IConsumer + public class Events : IConsumer { private readonly IGoogleFeedService _googleService; diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs index 1f57fcf317..c2e73c4fc4 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -480,19 +480,19 @@ protected override void Export(ExportExecuteContext context) } var customLabel0 = GetAttributeValue(attributeValues, "custom_label_0", languageId, gmc?.CustomLabel0, null); - WriteString(writer, "custom_label_0", gmc.CustomLabel0.NullEmpty()); + WriteString(writer, "custom_label_0", gmc?.CustomLabel0.NullEmpty()); var customLabel1 = GetAttributeValue(attributeValues, "custom_label_1", languageId, gmc?.CustomLabel1, null); - WriteString(writer, "custom_label_1", gmc.CustomLabel1.NullEmpty()); + WriteString(writer, "custom_label_1", gmc?.CustomLabel1.NullEmpty()); var customLabel2 = GetAttributeValue(attributeValues, "custom_label_2", languageId, gmc?.CustomLabel2, null); - WriteString(writer, "custom_label_2", gmc.CustomLabel2.NullEmpty()); + WriteString(writer, "custom_label_2", gmc?.CustomLabel2.NullEmpty()); var customLabel3 = GetAttributeValue(attributeValues, "custom_label_3", languageId, gmc?.CustomLabel3, null); - WriteString(writer, "custom_label_3", gmc.CustomLabel3.NullEmpty()); + WriteString(writer, "custom_label_3", gmc?.CustomLabel3.NullEmpty()); var customLabel4 = GetAttributeValue(attributeValues, "custom_label_4", languageId, gmc?.CustomLabel4, null); - WriteString(writer, "custom_label_4", gmc.CustomLabel4.NullEmpty()); + WriteString(writer, "custom_label_4", gmc?.CustomLabel4.NullEmpty()); ++context.RecordsSucceeded; } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs index 764edb7f2b..c2a8ff01a3 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs @@ -302,7 +302,7 @@ public List GetTaxonomyList(string searchTerm) try { var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(GoogleMerchantCenterFeedPlugin.SystemName); - var fileDir = Path.Combine(descriptor.OriginalAssemblyFile.Directory.FullName, "Files"); + 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(); diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj index cf87aa8f56..0a93c5cd18 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml index 7f047f12de..d85fa485ea 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml @@ -66,13 +66,13 @@ })
          - + @Html.SmartLabel(string.Empty, string.Empty)
          - + @Html.SmartLabel(string.Empty, string.Empty) + +
          +
          +
          + @Html.Raw(T("Plugins.Payments.PayPalInstalments.AdminInstruction")) + +
          +
          +
          + + PayPal + +
          +
          + +@{ Html.RenderAction("StoreScopeConfiguration", "Setting", new { area = "Admin" }); } + +@Html.ValidationSummary(false) + +@using (Html.BeginForm()) +{ + @Html.HiddenFor(x => x.TransactMode) + + @Html.Partial("ApiConfiguration") + + + + + + + + + + + + + + + + + + + + + + + @* + + + + + + + + + + + + + *@ + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + @T("Plugins.SmartStore.PayPal.Promotion") +
          +
          +
          + @Html.SmartLabelFor(x => x.ProductPagePromotion) + + @Html.SettingEditorFor(x => x.ProductPagePromotion, + @Html.DropDownListFor(x => x.ProductPagePromotion, Model.ProductPagePromotions, T("Common.Unspecified"))) + @Html.ValidationMessageFor(x => x.ProductPagePromotion) +
          + @Html.SmartLabelFor(x => x.CartPagePromotion) + + @Html.SettingEditorFor(x => x.CartPagePromotion, + @Html.DropDownListFor(x => x.CartPagePromotion, Model.CartPagePromotions, T("Common.Unspecified"))) + @Html.ValidationMessageFor(x => x.CartPagePromotion) +
          + @Html.SmartLabelFor(x => x.PaymentListPromotion) + + @Html.SettingEditorFor(x => x.PaymentListPromotion, + @Html.DropDownListFor(x => x.PaymentListPromotion, Model.PaymentListPromotions, T("Common.Unspecified"))) + @Html.ValidationMessageFor(x => x.PaymentListPromotion) +
          + @Html.SmartLabelFor(x => x.Lender) + + @Html.SettingEditorFor(x => x.Lender) + @Html.ValidationMessageFor(x => x.Lender) +
          + @Html.SmartLabelFor(x => x.Promote) + + @Html.SettingEditorFor(x => x.Promote, Html.CheckBoxFor(x => x.Promote, new { data_toggler_for = "#pnlPromote" })) + @Html.ValidationMessageFor(x => x.Promote) +
          + @Html.SmartLabelFor(x => x.PromotionWidgetZones) + + @Html.SettingEditorFor(x => x.PromotionWidgetZones, @Html.EditorFor(x => x.PromotionWidgetZones)) + @Html.ValidationMessageFor(x => x.PromotionWidgetZones) +
          + @Html.SmartLabelFor(x => x.PromotionDisplayOrder) + + @Html.SettingEditorFor(x => x.PromotionDisplayOrder) + @Html.ValidationMessageFor(x => x.PromotionDisplayOrder) +
          +
          +
          + @T("Common.Misc") +
          +
          +
          + @Html.SmartLabelFor(x => x.FinancingMin) + + @Html.SettingEditorFor(x => x.FinancingMin, null, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(x => x.FinancingMin) +
          + @Html.SmartLabelFor(x => x.FinancingMax) + + @Html.SettingEditorFor(x => x.FinancingMax, null, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(x => x.FinancingMax) +
          + @Html.SmartLabelFor(x => x.AdditionalFee) + + @Html.SettingEditorFor(x => x.AdditionalFee, null, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(x => x.AdditionalFee) +
          + @Html.SmartLabelFor(x => x.AdditionalFeePercentage) + + @Html.SettingEditorFor(x => x.AdditionalFeePercentage) + @Html.ValidationMessageFor(x => x.AdditionalFeePercentage) +
          +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.Print.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.Print.cshtml new file mode 100644 index 0000000000..1dd5df86f0 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.Print.cshtml @@ -0,0 +1,15 @@ +@{ + var financingCosts = ViewBag.FinancingCosts as string; + var totalInclFinancingCosts = ViewBag.TotalInclFinancingCosts as string; +} + + + + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.FinancingCosts"):@financingCosts
          @T("Plugins.Payments.PayPalInstalments.TotalInclFinancingCosts"):@totalInclFinancingCosts
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.cshtml new file mode 100644 index 0000000000..804e7706bf --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.cshtml @@ -0,0 +1,15 @@ +@{ + var financingCosts = ViewBag.FinancingCosts as string; + var totalInclFinancingCosts = ViewBag.TotalInclFinancingCosts as string; +} + + + + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.FinancingCosts"):@financingCosts
          @T("Plugins.Payments.PayPalInstalments.TotalInclFinancingCosts"):@totalInclFinancingCosts
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.liquid b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.liquid new file mode 100644 index 0000000000..894b198598 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.liquid @@ -0,0 +1,19 @@ + + + + + +
          +   + + + + + + + + + + +
          {{ 'Plugins.Payments.PayPalInstalments.FinancingCosts' | T }}:{{ PayPalInstalments.FormattedFinancingCosts }}
          {{ 'Plugins.Payments.PayPalInstalments.TotalInclFinancingCosts' | T }}:{{ PayPalInstalments.FormattedTotalInclFinancingCosts }}
          +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderSummaryTotals.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderSummaryTotals.cshtml new file mode 100644 index 0000000000..804e7706bf --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderSummaryTotals.cshtml @@ -0,0 +1,15 @@ +@{ + var financingCosts = ViewBag.FinancingCosts as string; + var totalInclFinancingCosts = ViewBag.TotalInclFinancingCosts as string; +} + + + + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.FinancingCosts"):@financingCosts
          @T("Plugins.Payments.PayPalInstalments.TotalInclFinancingCosts"):@totalInclFinancingCosts
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PaymentInfo.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PaymentInfo.cshtml new file mode 100644 index 0000000000..26e095da5a --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PaymentInfo.cshtml @@ -0,0 +1,18 @@ +@using SmartStore.PayPal.Services; +@model FinancingOptions +@if (Model.Promotion.HasValue) +{ +
          + @{ Html.RenderPartial("Promotion.Content"); } +
          +} +else +{ +
          + @T( +
          + @Html.Raw(T("Plugins.Payments.PayPalInstalments.PaymentInfo")) +
          +
          +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.Content.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.Content.cshtml new file mode 100644 index 0000000000..e350ab0a68 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.Content.cshtml @@ -0,0 +1,101 @@ +@using SmartStore.PayPal; +@using SmartStore.PayPal.Services; +@model FinancingOptions +@{ + var defaultOption = Model.GetDefaultOption(); +} +
          +@T( +
          +@if (Model.Promotion == PayPalPromotion.FinancingExample && defaultOption != null && defaultOption.AnnualPercentageRate > decimal.Zero) +{ +
          + @T("Plugins.Payments.PayPalInstalments.FinancingPerXMonth", defaultOption.MonthlyPayment.ToString(), defaultOption.Term) +
          +
          + @T("Plugins.Payments.PayPalInstalments.RepresentativeExample"): +
          +
          + + + @*German: Darlehehnsbetrag.*@ + + + + + @*German: Nominalzins. Is the same as Sollzins.*@ + + + + + @*German: Effektivzins.*@ + + + + + @*MonthlyPayment * Term != TotalCost --> last monthly payment is different\lower.*@ + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.NetLoanAmount"):@(Model.NetLoanAmount.ToString())
          @T("Plugins.Payments.PayPalInstalments.FixedNominalRate"):@(Math.Round(defaultOption.NominalRate, 2).ToString("0.00")) %
          @T("Plugins.Payments.PayPalInstalments.AnnualPercentageRate"):@(Math.Round(defaultOption.AnnualPercentageRate, 2).ToString("0.00")) %
          @T("Plugins.Payments.PayPalInstalments.TotalPayableAmount"):@(defaultOption.TotalCost.ToString())
          @T("Plugins.Payments.PayPalInstalments.XMonthlyInstalmentsAmount", defaultOption.Term):@(defaultOption.MonthlyPayment.ToString())
          +
          +
          @T("Plugins.Payments.PayPalInstalments.Lender"): @Html.Raw(Model.Lender)
          +} +else +{ +
          + @if (defaultOption != null) + { + @T("Plugins.Payments.PayPalInstalments.FinancingPerMonth", defaultOption.MonthlyPayment.ToString()) + } + else + { + @T("Plugins.Payments.PayPalInstalments.FinancingWanted") + } +
          +} + + +@{ + using (Html.BeginZoneContent("end", key: "pp-instalments-promotion-modal")) + { + Html.SmartStore().Window() + .Name("paypal-promotion-modal") + .Size(WindowSize.FlexSmall) + .RenderAtPageEnd(true) + .FooterContent(@) + .Render(); + } +} + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.cshtml new file mode 100644 index 0000000000..ab7d6645c1 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.cshtml @@ -0,0 +1,7 @@ +@using SmartStore.PayPal.Services; +@model FinancingOptions +
          +
          + @{ Html.RenderPartial("Promotion.Content"); } +
          +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PromotionPopup.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PromotionPopup.cshtml new file mode 100644 index 0000000000..56690c6801 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PromotionPopup.cshtml @@ -0,0 +1,65 @@ +@using SmartStore.PayPal.Services; +@model FinancingOptions +@{ + var defaultOption = Model.GetDefaultOption(false); +} +
          +
          + @T( +
          +
          @T("Plugins.Payments.PayPalInstalments.PromotionDetailsTitle")
          +
          @Html.Raw(T("Plugins.Payments.PayPalInstalments.PromotionDetailsIntro"))
          +
          @T("Plugins.Payments.PayPalInstalments.NetLoanAmount"): @Model.NetLoanAmount.ToString()
          +
          + @foreach (var option in Model.Qualified) + { +
          +
          + @(T("Plugins.Payments.PayPalInstalments.PlanX", option.Index)) @(defaultOption != null && defaultOption.Index == option.Index ? " *" : "") +
          +
          + + + + + + @*PayPal review: do not display last instalment.*@ + @*@if (option.MonthlyPayment.Amount != option.LastInstalment.Amount) + { + + + + + }*@ + + + + + + + + + + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.XMonthlyInstalmentsAmount", option.Term)@(option.MonthlyPayment.ToString())
          @T("Plugins.Payments.PayPalInstalments.LastInstalment")@(option.LastInstalment.ToString())
          @T("Plugins.Payments.PayPalInstalments.FixedNominalRate"):@(Math.Round(option.NominalRate, 2).ToString("0.00")) %
          @T("Plugins.Payments.PayPalInstalments.AnnualPercentageRate"):@(Math.Round(option.AnnualPercentageRate, 2).ToString("0.00")) %
          @T("Plugins.Payments.PayPalInstalments.InterestAmount"):@(option.TotalInterest.ToString())
          @T("Plugins.Payments.PayPalInstalments.TotalPayableAmount"):@(option.TotalCost.ToString())
          +
          +
          + } +
          +
          + @if (defaultOption != null) + { +
          * @T("Plugins.Payments.PayPalInstalments.RepresentativeExample").
          + } +
          @T("Plugins.Payments.PayPalInstalments.Lender"): @Html.Raw(Model.Lender)
          +
          +
          +
          +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml index efce2e5bbc..a4f7f4476c 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml @@ -1,13 +1,8 @@ -@using SmartStore.PayPal; -@using SmartStore.PayPal.Models; +@using SmartStore.PayPal.Models; @using SmartStore.Web.Framework; -@using SmartStore.Web.Framework.UI; @model PayPalPlusConfigurationModel @{ Layout = ""; - Html.AddCssFileParts(true, "~/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css"); - - var hasCredentials = (Model.ClientId.HasValue() && Model.Secret.HasValue()); }
          @@ -39,114 +34,14 @@ { @Html.HiddenFor(model => model.TransactMode) - - - - - - - - - - - - - - - - - - - - - - - - - - - +
          -
          -
          - @Model.ConfigGroups.SafeGet(0) -
          -
          -
          - @Html.SmartLabelFor(model => model.UseSandbox) - - @Html.EditorFor(model => model.UseSandbox) - @Html.ValidationMessageFor(model => model.UseSandbox) -
          - @Html.SmartLabelFor(model => model.ClientId) - - @Html.SettingEditorFor(model => model.ClientId) - @Html.ValidationMessageFor(model => model.ClientId) -
          - @Html.SmartLabelFor(model => model.Secret) - - @Html.SettingEditorFor(model => model.Secret) - @Html.ValidationMessageFor(model => model.Secret) -
          -
          -
          - @Model.ConfigGroups.SafeGet(1) -
          -
          -
          - @Html.SmartLabelFor(model => model.ExperienceProfileId) - - @Html.SettingEditorFor(model => model.ExperienceProfileId) - - - - @T(Model.ExperienceProfileId.HasValue() ? "Common.Refresh" : "Common.AddNew") - + @Html.Partial("ApiConfiguration") - @if (Model.ExperienceProfileId.HasValue()) - { - - - @T("Admin.Common.Delete") - - } - - @Html.ValidationMessageFor(model => model.ExperienceProfileId) -
          - @Html.SmartLabelFor(model => model.WebhookId) - -
          - @* IPNs and webhook messages have no store context, so multistore configuration not possible here *@ - @Html.EditorFor(model => model.WebhookId) - -
          - @if (Model.WebhookId.HasValue()) - { - - - @T("Admin.Common.Delete") - - } - else - { - - - @T("Common.AddNew") - - } -
          -
          - @Html.ValidationMessageFor(model => model.WebhookId) -
          diff --git a/src/Plugins/SmartStore.PayPal/Views/Shared/Partials/ApiConfiguration.cshtml b/src/Plugins/SmartStore.PayPal/Views/Shared/Partials/ApiConfiguration.cshtml new file mode 100644 index 0000000000..b0d068e120 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/Shared/Partials/ApiConfiguration.cshtml @@ -0,0 +1,115 @@ +@using SmartStore.PayPal; +@using SmartStore.PayPal.Models; +@using SmartStore.Web.Framework; +@using SmartStore.Web.Framework.UI; +@model ApiConfigurationModel +@{ + Layout = ""; + + var hasCredentials = Model.ClientId.HasValue() && Model.Secret.HasValue(); +} + +
          - @Model.ConfigGroups.SafeGet(2) + @T("Common.Misc")
          + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + @T("Plugins.SmartStore.PayPal.AccessData") +
          +
          +
          + @Html.SmartLabelFor(model => model.UseSandbox) + + @Html.EditorFor(model => model.UseSandbox) + @Html.ValidationMessageFor(model => model.UseSandbox) +
          + @Html.SmartLabelFor(model => model.ClientId) + + @Html.SettingEditorFor(model => model.ClientId) + @Html.ValidationMessageFor(model => model.ClientId) +
          + @Html.SmartLabelFor(model => model.Secret) + + @Html.SettingEditorFor(model => model.Secret) + @Html.ValidationMessageFor(model => model.Secret) +
          +
          +
          + @T("Admin.Common.DataExchange") +
          +
          +
          + @Html.SmartLabelFor(model => model.ExperienceProfileId) + + @Html.SettingEditorFor(model => model.ExperienceProfileId) + + + + @T(Model.ExperienceProfileId.HasValue() ? "Common.Refresh" : "Common.AddNew") + + + @if (Model.ExperienceProfileId.HasValue()) + { + + + @T("Admin.Common.Delete") + + } + + @Html.ValidationMessageFor(model => model.ExperienceProfileId) +
          + @Html.SmartLabelFor(model => model.WebhookId) + +
          + @* IPNs and webhook messages have no store context, so multistore configuration not possible here *@ + @Html.EditorFor(model => model.WebhookId) + +
          + @if (Model.WebhookId.HasValue()) + { + + + @T("Admin.Common.Delete") + + } + else + { + + + @T("Common.AddNew") + + } +
          +
          + @Html.ValidationMessageFor(model => model.WebhookId) +
          diff --git a/src/Plugins/SmartStore.PayPal/changelog.md b/src/Plugins/SmartStore.PayPal/changelog.md index 5c6598b0a5..ba84600675 100644 --- a/src/Plugins/SmartStore.PayPal/changelog.md +++ b/src/Plugins/SmartStore.PayPal/changelog.md @@ -1,5 +1,9 @@ #Release Notes +##Paypal 3.2.2.0 +###New Features +* New instalments provider. + ##Paypal 3.1.5.3 ###Improvements * PayPal PLUS: now up to 10 more third party payment methods are allowed by PayPal. diff --git a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs index e55f1cdd0a..05761ba86c 100644 --- a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs +++ b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs @@ -71,6 +71,7 @@ public ActionResult Configure() model.LimitMethodsToCreated = _shippingByTotalSettings.LimitMethodsToCreated; model.SmallQuantityThreshold = _shippingByTotalSettings.SmallQuantityThreshold; model.SmallQuantitySurcharge = _shippingByTotalSettings.SmallQuantitySurcharge; + model.CalculateTotalIncludingTax = _shippingByTotalSettings.CalculateTotalIncludingTax; model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; model.GridPageSize = _adminAreaSettings.GridPageSize; @@ -162,6 +163,7 @@ public ActionResult Configure(ByTotalListModel model) _shippingByTotalSettings.LimitMethodsToCreated = model.LimitMethodsToCreated; _shippingByTotalSettings.SmallQuantityThreshold = model.SmallQuantityThreshold; _shippingByTotalSettings.SmallQuantitySurcharge = model.SmallQuantitySurcharge; + _shippingByTotalSettings.CalculateTotalIncludingTax = model.CalculateTotalIncludingTax; _services.Settings.SaveSetting(_shippingByTotalSettings); diff --git a/src/Plugins/SmartStore.Shipping/Description.txt b/src/Plugins/SmartStore.Shipping/Description.txt index 07c7595d0a..654d28c65a 100644 --- a/src/Plugins/SmartStore.Shipping/Description.txt +++ b/src/Plugins/SmartStore.Shipping/Description.txt @@ -2,8 +2,8 @@ Description: Provides shipping methods for fixed rate shipping and computation based on weight. SystemName: SmartStore.Shipping Group: Shipping -Version: 3.1.5 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.Shipping.dll ResourceRootKey: Plugins.SmartStore.Shipping \ No newline at end of file diff --git a/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml b/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml index c5d47af01d..de15e1946b 100644 --- a/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml @@ -84,6 +84,12 @@ Dem Kunden wird während des Checkouts kostenloser Versand als Fallback angeboten, wenn keine der festgelegten Bedingungen zutreffen. Aktivieren Sie diese Option, wenn das nicht gewünscht ist. + + Berechne Bestellwert inkl. Steuer + + + Legt fest, ob der Bestellwert zur Berechnung der Versandkosten inkl. oder exkl. Steuern berechnet werden soll. + Basisgebühr diff --git a/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml b/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml index 16cfa0bb5f..41e299ed1c 100644 --- a/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml @@ -85,6 +85,12 @@ If you check this option, then your customers will be limited to shipping options configured here. Otherwise, they'll be able to choose any existing shipping options even if they're not configured here (zero shipping fee in this case). + + Calculate order total incl. tax + + + Specifies whether to calculate the order total including or excluding taxes. + Base fee diff --git a/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs b/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs index 848836b1af..4816ca9b1b 100644 --- a/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs +++ b/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs @@ -63,6 +63,9 @@ public ByTotalListModel() [SmartResourceDisplayName("Plugins.Shipping.ByTotal.Fields.SmallQuantitySurcharge")] public decimal SmallQuantitySurcharge { get; set; } + [SmartResourceDisplayName("Plugins.Shipping.ByTotal.Fields.CalculateTotalIncludingTax")] + public bool CalculateTotalIncludingTax { get; set; } + public string PrimaryStoreCurrencyCode { get; set; } public int GridPageSize { get; set; } diff --git a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs index 05bd66b8cc..f06e57d635 100644 --- a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs +++ b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs @@ -172,7 +172,6 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get int countryId = 0; int stateProvinceId = 0; string zip = null; - var taxRate = decimal.Zero; decimal subTotal = decimal.Zero; int storeId = _storeContext.CurrentStore.Id; @@ -190,13 +189,19 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get continue; } - var itemSubTotal = _priceCalculationService.GetSubTotal(shoppingCartItem, true); - var itemSubTotalInclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, itemSubTotal, true, getShippingOptionRequest.Customer, out taxRate); - subTotal += itemSubTotalInclTax; + var itemSubTotalBase = _priceCalculationService.GetSubTotal(shoppingCartItem, true); + var itemSubTotal = _taxService.GetProductPrice( + shoppingCartItem.Item.Product, + itemSubTotalBase, + _shippingByTotalSettings.CalculateTotalIncludingTax, + getShippingOptionRequest.Customer, + out var _); + + subTotal += itemSubTotal; } - decimal sqThreshold = _shippingByTotalSettings.SmallQuantityThreshold; - decimal sqSurcharge = _shippingByTotalSettings.SmallQuantitySurcharge; + var sqThreshold = _shippingByTotalSettings.SmallQuantityThreshold; + var sqSurcharge = _shippingByTotalSettings.SmallQuantitySurcharge; var shippingMethods = _shippingService.GetAllShippingMethods(getShippingOptionRequest, storeId); foreach (var shippingMethod in shippingMethods) @@ -206,7 +211,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get { if (rate > 0 && sqThreshold > 0 && subTotal <= sqThreshold) { - // Add small quantity surcharge (Mindermengenzuschlag) + // Add small quantity surcharge (Mindermengenzuschlag). rate += sqSurcharge; } diff --git a/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs b/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs index 8d2e5b8ceb..c3a94d7e03 100644 --- a/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs +++ b/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs @@ -163,45 +163,17 @@ public virtual ShippingByTotalRecord FindShippingByTotalRecord(int shippingMetho .Where(sbt => sbt.ShippingMethodId == shippingMethodId && subtotal >= sbt.From && (sbt.To == null || subtotal <= sbt.To.Value)) .ToList(); - //filter by store - var matchedByStore = new List(); - foreach (var sbw in existingRates) - { - if (sbw.StoreId == 0 || storeId == sbw.StoreId) - { - matchedByStore.Add(sbw); - } - } + //filter by store + var matchedByStore = existingRates.Where(x => x.StoreId == storeId || x.StoreId == 0).ToList(); //filter by country - var matchedByCountry = new List(); - foreach (var sbt in matchedByStore) - { - if (sbt.CountryId.GetValueOrDefault() == 0 || countryId == sbt.CountryId.GetValueOrDefault()) - { - matchedByCountry.Add(sbt); - } - } + var matchedByCountry = matchedByStore.Where(x => x.CountryId.GetValueOrDefault() == countryId || x.CountryId.GetValueOrDefault() == 0).ToList(); //filter by state/province - var matchedByStateProvince = new List(); - foreach (var sbt in matchedByCountry) - { - if (sbt.StateProvinceId.GetValueOrDefault() == 0 || stateProvinceId == sbt.StateProvinceId.GetValueOrDefault()) - { - matchedByStateProvince.Add(sbt); - } - } + var matchedByStateProvince = matchedByCountry.Where(x => x.StateProvinceId.GetValueOrDefault() == stateProvinceId || x.StateProvinceId.GetValueOrDefault() == 0).ToList(); //filter by zip - var matchedByZip = new List(); - foreach (var sbt in matchedByStateProvince) - { - if ((zip.IsEmpty() && sbt.Zip.IsEmpty()) || (ZipMatches(zip, sbt.Zip))) - { - matchedByZip.Add(sbt); - } - } + var matchedByZip = matchedByStateProvince.Where(x => (zip.IsEmpty() && x.Zip.IsEmpty()) || ZipMatches(zip, x.Zip)).ToList(); return matchedByZip.LastOrDefault(); } diff --git a/src/Plugins/SmartStore.Shipping/Settings/ShippingByTotalSettings.cs b/src/Plugins/SmartStore.Shipping/Settings/ShippingByTotalSettings.cs index 3ba3e94ee9..ce7f882691 100644 --- a/src/Plugins/SmartStore.Shipping/Settings/ShippingByTotalSettings.cs +++ b/src/Plugins/SmartStore.Shipping/Settings/ShippingByTotalSettings.cs @@ -4,7 +4,13 @@ namespace SmartStore.Shipping { public class ShippingByTotalSettings : ISettings { + public ShippingByTotalSettings() + { + CalculateTotalIncludingTax = true; + } + public bool LimitMethodsToCreated { get; set; } + public bool CalculateTotalIncludingTax { get; set; } public decimal SmallQuantityThreshold { get; set; } public decimal SmallQuantitySurcharge { get; set; } diff --git a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj index 5492feba91..8d59d2df62 100644 --- a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj +++ b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml b/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml index d96bda0ff5..ee33ab492d 100644 --- a/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml +++ b/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml @@ -44,6 +44,15 @@ @Html.ValidationMessageFor(model => model.SmallQuantitySurcharge) + + + @Html.SmartLabelFor(model => model.CalculateTotalIncludingTax) + + + @Html.EditorFor(model => model.CalculateTotalIncludingTax) + @Html.ValidationMessageFor(model => model.CalculateTotalIncludingTax) + + @Html.SmartLabelFor(model => model.LimitMethodsToCreated) diff --git a/src/Plugins/SmartStore.ShippingByWeight/Description.txt b/src/Plugins/SmartStore.ShippingByWeight/Description.txt index 2f41cdf30b..f73070d64c 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Description.txt +++ b/src/Plugins/SmartStore.ShippingByWeight/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Shipping by weight SystemName: SmartStore.ShippingByWeight Group: Shipping -Version: 3.1.5 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.ShippingByWeight.dll ResourceRootKey: Plugins.Shipping.ByWeight \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs index 62545474f0..186f71ac26 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs @@ -115,49 +115,14 @@ public virtual ShippingByWeightRecord FindRecord(int shippingMethodId, int store .Where(x => x.ShippingMethodId == shippingMethodId && weight >= x.From && weight <= x.To) .ToList(); - //filter by store - var matchedByStore = new List(); - foreach (var sbw in existingRecords) - { - if (storeId == sbw.StoreId) - matchedByStore.Add(sbw); - } + //filter by store + var matchedByStore = existingRecords.Where(x => x.StoreId == storeId || x.StoreId == 0).ToList(); - if (matchedByStore.Count == 0) - { - foreach (var sbw in existingRecords) - { - if (sbw.StoreId == 0) - matchedByStore.Add(sbw); - } - } - - //filter by country - var matchedByCountry = new List(); - foreach (var sbw in matchedByStore) - { - if (countryId == sbw.CountryId) - matchedByCountry.Add(sbw); - } - - if (matchedByCountry.Count == 0) - { - foreach (var sbw in matchedByStore) - { - if (sbw.CountryId == 0) - matchedByCountry.Add(sbw); - } - } + //filter by country + var matchedByCountry = matchedByStore.Where(x => x.CountryId == countryId || x.CountryId == 0).ToList(); //filter by zip - var matchedByZip = new List(); - foreach (var sbt in matchedByCountry) - { - if ((zip.IsEmpty() && sbt.Zip.IsEmpty()) || (ZipMatches(zip, sbt.Zip))) - { - matchedByZip.Add(sbt); - } - } + var matchedByZip = matchedByCountry.Where(x => (zip.IsEmpty() && x.Zip.IsEmpty()) || ZipMatches(zip, x.Zip)).ToList(); return matchedByZip.LastOrDefault(); } diff --git a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj index 0392e89152..30c483ab7c 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj +++ b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.Tax/Description.txt b/src/Plugins/SmartStore.Tax/Description.txt index c244e318d1..66eb94aab2 100644 --- a/src/Plugins/SmartStore.Tax/Description.txt +++ b/src/Plugins/SmartStore.Tax/Description.txt @@ -2,8 +2,8 @@ Description: Contains default tax providers like FixedRate, ByRegion etc. Group: Tax SystemName: SmartStore.Tax -Version: 3.1.5 -MinAppVersion: 3.1.5 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 0 FileName: SmartStore.Tax.dll ResourceRootKey: Plugins.SmartStore.Tax diff --git a/src/Plugins/SmartStore.Tax/Providers/ByRegionTaxProvider.cs b/src/Plugins/SmartStore.Tax/Providers/ByRegionTaxProvider.cs index 5752d63f6a..bd1765149e 100644 --- a/src/Plugins/SmartStore.Tax/Providers/ByRegionTaxProvider.cs +++ b/src/Plugins/SmartStore.Tax/Providers/ByRegionTaxProvider.cs @@ -1,37 +1,32 @@ -using System.Data.Entity.Migrations; +using System.Linq; using System.Web.Routing; +using SmartStore.Core.Domain.Tax; using SmartStore.Core.Plugins; -using SmartStore.Tax.Data; -using SmartStore.Tax.Data.Migrations; -using SmartStore.Tax.Services; -using SmartStore.Services.Localization; +using SmartStore.Services.Configuration; using SmartStore.Services.Tax; +using SmartStore.Tax.Services; namespace SmartStore.Tax { - [SystemName("Tax.CountryStateZip")] + [SystemName("Tax.CountryStateZip")] [FriendlyName("Tax By Region")] [DisplayOrder(10)] public class ByRegionTaxProvider : ITaxProvider, IConfigurable { private readonly ITaxRateService _taxRateService; - private readonly TaxRateObjectContext _objectContext; - private readonly ILocalizationService _localizationService; + private readonly ISettingService _settingService; + private readonly TaxSettings _taxSettings; - public ByRegionTaxProvider(ITaxRateService taxRateService, - TaxRateObjectContext objectContext, - ILocalizationService localizationService) + public ByRegionTaxProvider( + ITaxRateService taxRateService, + ISettingService settingService, + TaxSettings taxSettings) { - this._taxRateService = taxRateService; - this._objectContext = objectContext; - _localizationService = localizationService; + _taxRateService = taxRateService; + _settingService = settingService; + _taxSettings = taxSettings; } - /// - /// Gets tax rate - /// - /// Tax calculation request - /// Tax public CalculateTaxResult GetTaxRate(CalculateTaxRequest calculateTaxRequest) { var result = new CalculateTaxResult(); @@ -42,12 +37,26 @@ public CalculateTaxResult GetTaxRate(CalculateTaxRequest calculateTaxRequest) return result; } - var taxRates = _taxRateService.GetAllTaxRates(calculateTaxRequest.TaxCategoryId, - calculateTaxRequest.Address.Country != null ? calculateTaxRequest.Address.Country.Id: 0, - calculateTaxRequest.Address.StateProvince != null ? calculateTaxRequest.Address.StateProvince.Id : 0, + if (_taxSettings.EuVatEnabled) + { + if (!(calculateTaxRequest.Address.Country?.SubjectToVat ?? false)) + { + // Fallback to fixed rate (merchant country VAT rate). + result.TaxRate = _settingService.GetSettingByKey($"Tax.TaxProvider.FixedRate.TaxCategoryId{calculateTaxRequest.TaxCategoryId}"); + return result; + } + } + + var taxRates = _taxRateService.GetAllTaxRates( + calculateTaxRequest.TaxCategoryId, + calculateTaxRequest.Address.Country?.Id ?? 0, + calculateTaxRequest.Address.StateProvince?.Id ?? 0, calculateTaxRequest.Address.ZipPostalCode); - if (taxRates.Count > 0) + + if (taxRates.Any()) + { result.TaxRate = taxRates[0].Percentage; + } return result; } @@ -56,8 +65,7 @@ public void GetConfigurationRoute(out string actionName, out string controllerNa { actionName = "Configure"; controllerName = "TaxByRegion"; - routeValues = new RouteValueDictionary() { { "area", "SmartStore.Tax" } }; + routeValues = new RouteValueDictionary { { "area", "SmartStore.Tax" } }; } - } } diff --git a/src/Plugins/SmartStore.Tax/Providers/FixedRateTaxProvider.cs b/src/Plugins/SmartStore.Tax/Providers/FixedRateTaxProvider.cs index 55d3ab87b7..4fabac4b04 100644 --- a/src/Plugins/SmartStore.Tax/Providers/FixedRateTaxProvider.cs +++ b/src/Plugins/SmartStore.Tax/Providers/FixedRateTaxProvider.cs @@ -1,37 +1,35 @@ using System.Web.Routing; using SmartStore.Core.Plugins; using SmartStore.Services.Configuration; -using SmartStore.Services.Localization; using SmartStore.Services.Tax; namespace SmartStore.Tax { - [SystemName("Tax.FixedRate")] + [SystemName("Tax.FixedRate")] [FriendlyName("Fixed Tax Rate")] [DisplayOrder(5)] public class FixedRateTaxProvider : ITaxProvider, IConfigurable { private readonly ISettingService _settingService; - private readonly ILocalizationService _localizationService; - public FixedRateTaxProvider(ISettingService settingService, ILocalizationService localizationService) + public FixedRateTaxProvider(ISettingService settingService) { - this._settingService = settingService; - _localizationService = localizationService; + _settingService = settingService; } public CalculateTaxResult GetTaxRate(CalculateTaxRequest calculateTaxRequest) { - var result = new CalculateTaxResult() + var result = new CalculateTaxResult { TaxRate = GetTaxRate(calculateTaxRequest.TaxCategoryId) }; + return result; } protected decimal GetTaxRate(int taxCategoryId) { - decimal rate = this._settingService.GetSettingByKey(string.Format("Tax.TaxProvider.FixedRate.TaxCategoryId{0}", taxCategoryId)); + var rate = _settingService.GetSettingByKey($"Tax.TaxProvider.FixedRate.TaxCategoryId{taxCategoryId}"); return rate; } @@ -39,8 +37,7 @@ public void GetConfigurationRoute(out string actionName, out string controllerNa { actionName = "Configure"; controllerName = "TaxFixedRate"; - routeValues = new RouteValueDictionary() { { "area", "SmartStore.Tax" } }; + routeValues = new RouteValueDictionary { { "area", "SmartStore.Tax" } }; } - } } diff --git a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj index 7b93e203b3..21dbe5eef1 100644 --- a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj +++ b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/AdminControllerBase.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/AdminControllerBase.cs index 0511597b70..f04be13344 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/AdminControllerBase.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/AdminControllerBase.cs @@ -1,35 +1,37 @@ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Web.Mvc; -using System.Web.Routing; using SmartStore.Core; -using SmartStore.Core.Domain.Security; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Infrastructure; -using SmartStore.Services; -using SmartStore.Services.Localization; -using SmartStore.Services.Security; -using SmartStore.Services.Stores; using SmartStore.Web.Framework.Filters; -using SmartStore.Web.Framework.Localization; -using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Theming; namespace SmartStore.Web.Framework.Controllers { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public sealed class NonAdminAttribute : Attribute + public sealed class NonAdminAttribute : FilterAttribute, IActionFilter { + public Lazy WorkContext { get; set; } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + if (!filterContext.IsChildAction) + { + WorkContext.Value.IsAdmin = false; + filterContext.HttpContext.Items["IsNonAdmin"] = true; + } + } } [AdminValidateIpAddress(Order = 100)] - [RequireHttpsByConfig(SslRequirement.Yes, Order = 110)] + [RewriteUrl(SslRequirement.Yes, Order = 110)] [CustomerLastActivity(Order = 100)] [StoreIpAddress(Order = 100)] [AdminThemed(Order = -1)] - public abstract class AdminControllerBase : SmartController + public abstract class AdminControllerBase : ManageController { protected override void OnActionExecuting(ActionExecutingContext filterContext) { @@ -37,81 +39,7 @@ protected override void OnActionExecuting(ActionExecutingContext filterContext) return; var isNonAdmin = filterContext.ActionDescriptor.HasAttribute(true); - Services.Resolve().IsAdmin = !isNonAdmin; - } - - /// - /// Add locales for localizable entities - /// - /// Localizable model - /// Language service - /// Locales - protected virtual void AddLocales(ILanguageService languageService, IList locales) where TLocalizedModelLocal : ILocalizedModelLocal - { - AddLocales(languageService, locales, null); - } - - /// - /// Add locales for localizable entities - /// - /// Localizable model - /// Language service - /// Locales - /// Configure action - protected virtual void AddLocales(ILanguageService languageService, IList locales, Action configure) where TLocalizedModelLocal : ILocalizedModelLocal - { - foreach (var language in languageService.GetAllLanguages(true)) - { - var locale = Activator.CreateInstance(); - locale.LanguageId = language.Id; - if (configure != null) - { - configure.Invoke(locale, locale.LanguageId); - } - locales.Add(locale); - } - } - - /// - /// Save the store mappings for an entity - /// - /// Entity type - /// The entity - /// Model representation of store selection - protected virtual void SaveStoreMappings(T entity, IStoreSelector model) where T : BaseEntity, IStoreMappingSupported - { - entity.LimitedToStores = model.LimitedToStores; - Services.Resolve().SaveStoreMappings(entity, model.SelectedStoreIds); - } - - /// - /// Save the ACL mappings for an entity - /// - /// Entity type - /// The entity - /// Model representation of ACL selection - protected virtual void SaveAclMappings(T entity, IAclSelector model) where T : BaseEntity, IAclSupported - { - entity.SubjectToAcl = model.SubjectToAcl; - Services.Resolve().SaveAclMappings(entity, model.SelectedCustomerRoleIds); - } - - /// - /// Access denied view - /// - /// Access denied view - [SuppressMessage("ReSharper", "Mvc.AreaNotResolved")] - protected ActionResult AccessDeniedView() - { - return RedirectToAction("AccessDenied", "Security", new { pageUrl = this.Request.RawUrl, area = "Admin" }); - } - - /// - /// Renders default access denied view as a partial - /// - protected ActionResult AccessDeniedPartialView() - { - return PartialView("~/Administration/Views/Security/AccessDenied.cshtml"); + Services.WorkContext.IsAdmin = !isNonAdmin; } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs index 94e9ff679c..1d17a3d790 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs @@ -1,22 +1,63 @@ using System; +using System.Globalization; using System.IO; using System.Text; using System.Web.Mvc; +using System.Web.Mvc.Html; +using System.Web.Routing; using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Services.Common; using SmartStore.Services.Stores; using SmartStore.Utilities; +using SmartStore.Web.Framework.Modelling.Results; + #pragma warning disable 1573 namespace SmartStore.Web.Framework.Controllers { public static class ContollerExtensions { - /// - /// Render partial view to string - /// - /// Result + #region InvokeAction + + public static MvcHtmlString InvokeAction(this ControllerBase controller, string actionName, string controllerName = null, RouteValueDictionary routeValues = null) + { + Guard.NotNull(controller, nameof(controller)); + Guard.NotEmpty(actionName, nameof(actionName)); + + using (StringWriter writer = new StringWriter(CultureInfo.CurrentCulture)) + { + InvokeAction(controller, writer, actionName, controllerName, routeValues); + return MvcHtmlString.Create(writer.ToString()); + } + } + + public static void InvokeAction(this ControllerBase controller, TextWriter writer, string actionName, string controllerName = null, RouteValueDictionary routeValues = null) + { + Guard.NotNull(controller, nameof(controller)); + Guard.NotNull(writer, nameof(writer)); + Guard.NotEmpty(actionName, nameof(actionName)); + + var viewContext = new ViewContext( + controller.ControllerContext, + new WebFormView(controller.ControllerContext, "tmp"), + controller.ViewData, + controller.TempData, + writer + ); + + var htmlHelper = new HtmlHelper(viewContext, new ViewPage()); + htmlHelper.RenderAction(actionName, controllerName, routeValues); + } + + #endregion + + #region Render view + + /// + /// Render partial view to string + /// + /// Result public static string RenderPartialViewToString(this ControllerBase controller) { return RenderPartialViewToString(controller, null, null, null); @@ -85,7 +126,7 @@ public static string RenderPartialViewToString(this ControllerBase controller, s { ViewEngineResult viewResult = System.Web.Mvc.ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName.EmptyNull()); - ThrowIfViewNotFound(viewResult, viewName); + ThrowIfViewNotFound(viewResult, viewName); var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw); viewResult.View.Render(viewContext, sw); @@ -178,6 +219,87 @@ private static void ThrowIfViewNotFound(ViewEngineResult viewResult, string view } } + #endregion + + #region RootActionView + + /// + /// Creates a object using the root actions's for view resolution. + /// + public static ViewResult RootActionView(this Controller controller, string viewName) + { + return RootActionView(controller, viewName, (string)null, null); + } + + /// + /// Creates a object using the root actions's for view resolution. + /// + public static ViewResult RootActionView(this Controller controller, string viewName, string masterName) + { + return RootActionView(controller, viewName, masterName, null); + } + + /// + /// Creates a object using the root actions's for view resolution. + /// + public static ViewResult RootActionView(this Controller controller, string viewName, object model) + { + return RootActionView(controller, viewName, (string)null, model); + } + + /// + /// Creates a object using the root actions's for view resolution. + /// + public static ViewResult RootActionView(this Controller controller, string viewName, string masterName, object model) + { + Guard.NotEmpty(viewName, nameof(viewName)); + + if (model != null) + { + controller.ViewData.Model = model; + } + + return new RootActionViewResult + { + ViewName = viewName, + MasterName = masterName, + ViewData = controller.ViewData, + TempData = controller.TempData, + ViewEngineCollection = controller.ViewEngineCollection + }; + } + + /// + /// Creates a object using the root actions's for view resolution. + /// + public static PartialViewResult RootActionPartialView(this Controller controller, string viewName) + { + return RootActionPartialView(controller, viewName, null); + } + + /// + /// Creates a object using the root actions's for view resolution. + /// + public static PartialViewResult RootActionPartialView(this Controller controller, string viewName, object model) + { + Guard.NotEmpty(viewName, nameof(viewName)); + + if (model != null) + { + controller.ViewData.Model = model; + } + + return new RootActionPartialViewResult + { + ViewName = viewName, + ViewData = controller.ViewData, + TempData = controller.TempData, + ViewEngineCollection = controller.ViewEngineCollection + }; + } + + #endregion + /// /// Get active store scope (for multi-store configuration mode) /// diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/ManageController.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/ManageController.cs new file mode 100644 index 0000000000..d49d4d6a5c --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/ManageController.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Web.Mvc; +using SmartStore.Core; +using SmartStore.Core.Domain.Security; +using SmartStore.Core.Domain.Stores; +using SmartStore.Services; +using SmartStore.Services.Localization; +using SmartStore.Services.Security; +using SmartStore.Services.Stores; +using SmartStore.Web.Framework.Localization; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.Web.Framework.Controllers +{ + public abstract class ManageController : SmartController + { + /// + /// Add locales for localizable entities + /// + /// Localizable model + /// Language service + /// Locales + protected virtual void AddLocales(ILanguageService languageService, IList locales) where TLocalizedModelLocal : ILocalizedModelLocal + { + AddLocales(languageService, locales, null); + } + + /// + /// Add locales for localizable entities + /// + /// Localizable model + /// Language service + /// Locales + /// Configure action + protected virtual void AddLocales(ILanguageService languageService, IList locales, Action configure) where TLocalizedModelLocal : ILocalizedModelLocal + { + foreach (var language in languageService.GetAllLanguages(true)) + { + var locale = Activator.CreateInstance(); + locale.LanguageId = language.Id; + if (configure != null) + { + configure.Invoke(locale, locale.LanguageId); + } + locales.Add(locale); + } + } + + /// + /// Save the store mappings for an entity + /// + /// Entity type + /// The entity + /// Model representation of store selection + protected virtual void SaveStoreMappings(T entity, IStoreSelector model) where T : BaseEntity, IStoreMappingSupported + { + entity.LimitedToStores = model.LimitedToStores; + Services.Resolve().SaveStoreMappings(entity, model.SelectedStoreIds); + } + + /// + /// Save the ACL mappings for an entity + /// + /// Entity type + /// The entity + /// Model representation of ACL selection + protected virtual void SaveAclMappings(T entity, IAclSelector model) where T : BaseEntity, IAclSupported + { + entity.SubjectToAcl = model.SubjectToAcl; + Services.Resolve().SaveAclMappings(entity, model.SelectedCustomerRoleIds); + } + + /// + /// Access denied view + /// + /// Access denied view + [SuppressMessage("ReSharper", "Mvc.AreaNotResolved")] + protected ActionResult AccessDeniedView() + { + return RedirectToAction("AccessDenied", "Security", new { pageUrl = this.Request.RawUrl, area = "Admin" }); + } + + /// + /// Renders default access denied view as a partial + /// + protected ActionResult AccessDeniedPartialView() + { + return PartialView("~/Administration/Views/Security/AccessDenied.cshtml"); + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/PluginControllerBase.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/PluginControllerBase.cs index da8b191c32..4a3acc5710 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/PluginControllerBase.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/PluginControllerBase.cs @@ -1,27 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +using System; using System.Web.Mvc; namespace SmartStore.Web.Framework.Controllers { - public abstract partial class PluginControllerBase : SmartController + public abstract partial class PluginControllerBase : ManageController { - /// - /// Access denied view - /// - /// Access denied view - [SuppressMessage("ReSharper", "Mvc.AreaNotResolved")] - protected ActionResult AccessDeniedView() - { - return RedirectToAction("AccessDenied", "Security", new { pageUrl = this.Request.RawUrl, area = "Admin" }); - } - - /// - /// Renders default access denied view as a partial - /// - protected ActionResult AccessDeniedPartialView() - { - return PartialView("~/Administration/Views/Security/AccessDenied.cshtml"); - } - } } diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/PublicControllerBase.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/PublicControllerBase.cs index bdd6a5a50c..fee48defb5 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/PublicControllerBase.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/PublicControllerBase.cs @@ -6,8 +6,7 @@ namespace SmartStore.Web.Framework.Controllers { - [CanonicalHostName(Order = 100)] - [RequireHttpsByConfigAttribute(SslRequirement.Retain, Order = 110)] + [RewriteUrl(SslRequirement.Retain)] [StoreClosed(Order = -1)] [PublicStoreAllowNavigation(Order = -1)] [LanguageSeoCode(Order = -1)] @@ -17,5 +16,5 @@ namespace SmartStore.Web.Framework.Controllers [CheckAffiliate(Order = 100)] public abstract partial class PublicControllerBase : SmartController { - } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs index 4d4b354a66..80526ac5c3 100644 --- a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs +++ b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Web; using System.Web.Hosting; -using System.Web.Mvc; using Autofac; using Autofac.Builder; using Autofac.Core; @@ -95,13 +95,13 @@ namespace SmartStore.Web.Framework { - public class DependencyRegistrar : IDependencyRegistrar + public class DependencyRegistrar : IDependencyRegistrar { public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { // plugins - var pluginFinder = new PluginFinder(); - builder.RegisterInstance(pluginFinder).As().SingleInstance(); + var pluginFinder = PluginFinder.Current; + builder.RegisterInstance(pluginFinder); builder.RegisterType(); // modules @@ -110,7 +110,6 @@ public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, b builder.RegisterModule(new CachingModule()); builder.RegisterModule(new SearchModule()); builder.RegisterModule(new LocalizationModule()); - builder.RegisterModule(new EventModule(typeFinder, pluginFinder)); builder.RegisterModule(new MessagingModule()); builder.RegisterModule(new WebModule(typeFinder)); builder.RegisterModule(new WebApiModule(typeFinder)); @@ -120,6 +119,7 @@ public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, b builder.RegisterModule(new ProvidersModule(typeFinder, pluginFinder)); builder.RegisterModule(new TasksModule(typeFinder)); builder.RegisterModule(new DataExchangeModule(typeFinder)); + builder.RegisterModule(new EventModule(typeFinder, pluginFinder)); } public int Order @@ -202,7 +202,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); @@ -251,14 +251,16 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); + + builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); @@ -280,7 +282,7 @@ protected override void AttachToComponentRegistration(IComponentRegistry compone if (servicesProperty == null) return; - + registration.Metadata.Add("Property.ICommonServices", new FastProperty(servicesProperty)); registration.Activated += (sender, e) => @@ -544,13 +546,14 @@ protected override void Load(ContainerBuilder builder) { // Output cache builder.RegisterType().As().InstancePerRequest(); - builder.Register(c => NullOutputCacheInvalidationObserver.Instance).SingleInstance(); + builder.Register(c => NullOutputCacheInvalidationObserver.Instance).SingleInstance(); builder.RegisterType().As().InstancePerRequest(); // Request cache builder.RegisterType().As().InstancePerRequest(); // Model/Business cache (application scoped) + builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().Named("null").SingleInstance(); @@ -609,37 +612,29 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - builder.RegisterGeneric(typeof(DefaultConsumerFactory<>)).As(typeof(IConsumerFactory<>)).InstancePerDependency(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); - // Register event consumers - var consumerTypes = _typeFinder.FindClassesOfType(typeof(IConsumer<>)); - foreach (var consumerType in consumerTypes) + var consumerTypes = _typeFinder.FindClassesOfType(typeof(IConsumer)); + foreach (var type in consumerTypes) { - Type[] implementedInterfaces = consumerType.FindInterfaces(IsConsumerInterface, typeof(IConsumer<>)); - - var registration = builder.RegisterType(consumerType).As(implementedInterfaces); + var registration = builder + .RegisterType(type) + .As() + .Keyed(type) + .InstancePerRequest(); - var pluginDescriptor = _pluginFinder.GetPluginDescriptorByAssembly(consumerType.Assembly); - var isActive = PluginManager.IsActivePluginAssembly(consumerType.Assembly); - var shouldExecuteAsync = consumerType.GetAttribute(false) != null; + var pluginDescriptor = _pluginFinder.GetPluginDescriptorByAssembly(type.Assembly); + var isActive = PluginManager.IsActivePluginAssembly(type.Assembly); registration.WithMetadata(m => { m.For(em => em.IsActive, isActive); - m.For(em => em.ExecuteAsync, shouldExecuteAsync); + m.For(em => em.ContainerType, type); m.For(em => em.PluginDescriptor, pluginDescriptor); }); - - if (!shouldExecuteAsync) - registration.InstancePerRequest(); - } } - - private static bool IsConsumerInterface(Type type, object criteria) - { - var isMatch = type.IsGenericType && ((Type)criteria).IsAssignableFrom(type.GetGenericTypeDefinition()); - return isMatch; - } } public class MessagingModule : Module @@ -712,8 +707,6 @@ static HttpContextBase HttpContextBaseFactory(IComponentContext ctx) return new HttpContextWrapper(HttpContext.Current); } - // TODO: determine store url - // register FakeHttpContext when HttpContext is not available return new FakeHttpContext("~/"); } @@ -721,19 +714,11 @@ static HttpContextBase HttpContextBaseFactory(IComponentContext ctx) static bool IsRequestValid() { if (HttpContext.Current == null) - return false; - - try - { - // The "Request" property throws at application startup on IIS integrated pipeline mode - var req = HttpContext.Current.Request; - } - catch (Exception) { return false; } - return true; + return HttpContext.Current.SafeGetHttpRequest() != null; } } @@ -807,10 +792,10 @@ public class UiModule : Module { private readonly ITypeFinder _typeFinder; - public UiModule(ITypeFinder typeFinder) + public UiModule(ITypeFinder typeFinder) { _typeFinder = typeFinder; - } + } protected override void Load(ContainerBuilder builder) { @@ -826,20 +811,45 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As>(); builder.RegisterType().As>(); - builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().SingleInstance(); - // Sitemaps - builder.RegisterType().As().InstancePerRequest(); + // Menus. + builder.RegisterType().As().InstancePerRequest(); + + var menuResolverTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true); + foreach (var type in menuResolverTypes) + { + builder.RegisterType(type).As().PropertiesAutowired(PropertyWiringOptions.None).InstancePerRequest(); + } - var siteMapTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true); - foreach (var type in siteMapTypes) - { - builder.RegisterType(type).As().PropertiesAutowired(PropertyWiringOptions.None).InstancePerRequest(); - } - } + builder.RegisterType().Named("database").InstancePerDependency(); + + var menuTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true); + foreach (var type in menuTypes) + { + builder.RegisterType(type).As().PropertiesAutowired(PropertyWiringOptions.None).InstancePerRequest(); + } + + var menuItemProviderTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true); + foreach (var type in menuItemProviderTypes) + { + var attribute = type.GetAttribute(false); + var registration = builder.RegisterType(type).As().PropertiesAutowired(PropertyWiringOptions.None).InstancePerRequest(); + registration.WithMetadata(m => + { + m.For(em => em.ProviderName, attribute.ProviderName); + m.For(em => em.AppendsMultipleItems, attribute.AppendsMultipleItems); + }); + } + + if (DataSettings.DatabaseIsInstalled()) + { + builder.RegisterType().AsResultFilterFor(0); + } + } } public class IOModule : Module @@ -1185,8 +1195,8 @@ class SettingsSource : IRegistrationSource BindingFlags.Static | BindingFlags.NonPublic); public IEnumerable RegistrationsFor( - Service service, - Func> registrations) + Service service, + Func> registrations) { if (service is TypedService ts && typeof(ISettings).IsAssignableFrom(ts.ServiceType)) { @@ -1201,22 +1211,25 @@ public IEnumerable RegistrationsFor( .ForDelegate((c, p) => { int currentStoreId = 0; - try + if (EngineContext.Current.IsFullyInitialized) { - if (c.TryResolve(out IStoreContext storeContext)) + try { - currentStoreId = storeContext.CurrentStore.Id; - //uncomment the code below if you want load settings per store only when you have two stores installed. - //var currentStoreId = c.Resolve().GetAllStores().Count > 1 - // c.Resolve().CurrentStore.Id : 0; + if (c.TryResolve(out IStoreContext storeContext)) + { + currentStoreId = storeContext.CurrentStore.Id; + //uncomment the code below if you want load settings per store only when you have two stores installed. + //var currentStoreId = c.Resolve().GetAllStores().Count > 1 + // c.Resolve().CurrentStore.Id : 0; - ////although it's better to connect to your database and execute the following SQL: - //DELETE FROM [Setting] WHERE [StoreId] > 0 + ////although it's better to connect to your database and execute the following SQL: + //DELETE FROM [Setting] WHERE [StoreId] > 0 - //return c.Resolve().LoadSetting(currentStoreId); + //return c.Resolve().LoadSetting(currentStoreId); + } } + catch { } } - catch { } try { @@ -1247,8 +1260,7 @@ private static bool IsClosingTypeOf(Type type, Type openGenericType) public IEnumerable RegistrationsFor(Service service, Func> registrationAccessor) { - var swt = service as IServiceWithType; - if (swt == null || !IsClosingTypeOf(swt.ServiceType, typeof(Work<>))) + if (!(service is IServiceWithType swt) || !IsClosingTypeOf(swt.ServiceType, typeof(Work<>))) return Enumerable.Empty(); var valueType = swt.ServiceType.GetGenericArguments()[0]; diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs index 325144c315..b2198a285f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs @@ -35,8 +35,13 @@ public static class HtmlExtensions { public static MvcHtmlString Hint(this HtmlHelper helper, string value) { - // create a - var a = new TagBuilder("a"); + if (String.IsNullOrEmpty(value)) + { + return MvcHtmlString.Empty; + } + + // create a + var a = new TagBuilder("a"); a.MergeAttribute("href", "#"); a.MergeAttribute("onclick", "return false;"); //a.MergeAttribute("rel", "tooltip"); diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs index f3bdab1bfd..56dba19bee 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs @@ -215,15 +215,13 @@ internal static void SetUserThemeChoiceInCookie(this HttpContextBase context, st internal static HttpCookie GetPreviewModeCookie(this HttpContextBase context, bool createIfMissing) { - if (context == null) - return null; - - var cookie = context.Request.Cookies.Get("sm.PreviewModeOverrides"); + var httpRequest = context.SafeGetHttpRequest(); + var cookie = httpRequest?.Cookies?.Get("sm.PreviewModeOverrides"); - if (cookie == null && createIfMissing) + if (cookie == null && createIfMissing && httpRequest != null) { cookie = new HttpCookie("sm.PreviewModeOverrides") { HttpOnly = true }; - context.Request.Cookies.Set(cookie); + httpRequest.Cookies.Set(cookie); } if (cookie != null) diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/TelerikExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/TelerikExtensions.cs index 2c858db3ab..898e7a2923 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/TelerikExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/TelerikExtensions.cs @@ -71,6 +71,8 @@ public static GridBuilder PreserveGridState(this GridBuilder builder) w 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)); @@ -84,6 +86,8 @@ public static GridBuilder PreserveGridState(this GridBuilder builder) w 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)); @@ -97,6 +101,8 @@ public static GridBuilder PreserveGridState(this GridBuilder builder) w if (grid.Filtering.Enabled) { + grid.Filtering.Filters.Clear(); + foreach (var filter in command.FilterDescriptors) { var compositeFilter = filter as CompositeFilterDescriptor; @@ -117,20 +123,20 @@ public static GridBuilder PreserveGridState(this GridBuilder builder) w public static IEnumerable ForCommand(this IEnumerable current, GridCommand command) { - var queryable = current.AsQueryable() as IQueryable; + var query = current.AsQueryable() as IQueryable; if (command.FilterDescriptors.Any()) { - queryable = queryable.Where(command.FilterDescriptors.AsEnumerable()).AsQueryable() as IQueryable; + query = query.Where(command.FilterDescriptors.AsEnumerable()).AsQueryable() as IQueryable; } IList temporarySortDescriptors = new List(); - if (!command.SortDescriptors.Any() && queryable.Provider.IsEntityFrameworkProvider()) + if (!command.SortDescriptors.Any() && query.Provider.IsEntityFrameworkProvider()) { // The Entity Framework provider demands OrderBy before calling Skip. SortDescriptor sortDescriptor = new SortDescriptor { - Member = GetFirstSortableProperty(queryable.ElementType) + Member = GetFirstSortableProperty(query.ElementType) }; command.SortDescriptors.Add(sortDescriptor); temporarySortDescriptors.Add(sortDescriptor); @@ -153,10 +159,10 @@ public static IEnumerable ForCommand(this IEnumerable current, GridComm if (command.SortDescriptors.Any()) { - queryable = queryable.Sort(command.SortDescriptors); + query = query.Sort(command.SortDescriptors); } - return queryable as IQueryable; + return query as IQueryable; } public static IEnumerable PagedForCommand(this IEnumerable current, GridCommand command) diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs index 473c01a972..fdb1f1d332 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs @@ -1,15 +1,9 @@ -using System.Web.Mvc; -using SmartStore.Core; -using SmartStore.Core.Caching; +using System; +using System.Web.Mvc; using SmartStore.Core.Domain.Media; using SmartStore.Core.Infrastructure; +using SmartStore.Services.Cms; using SmartStore.Services.Media; -using SmartStore.Services.Topics; -using SmartStore.Services.Seo; -using SmartStore.Services.Security; -using System.Web.Routing; -using System; -using SmartStore.Services.Localization; namespace SmartStore.Web.Framework { @@ -18,7 +12,9 @@ public static class UrlHelperExtensions public static string LogOn(this UrlHelper urlHelper, string returnUrl) { if (!string.IsNullOrEmpty(returnUrl)) - return urlHelper.Action("Login", "Customer", new { ReturnUrl = returnUrl, area = "" }); + { + return urlHelper.Action("Login", "Customer", new { returnUrl, area = "" }); + } return urlHelper.Action("Login", "Customer", new { area = "" }); } @@ -26,7 +22,9 @@ public static string LogOn(this UrlHelper urlHelper, string returnUrl) public static string LogOff(this UrlHelper urlHelper, string returnUrl) { if (!string.IsNullOrEmpty(returnUrl)) - return urlHelper.Action("Logout", "Customer", new { ReturnUrl = returnUrl, area = "" }); + { + return urlHelper.Action("Logout", "Customer", new { returnUrl, area = "" }); + } return urlHelper.Action("Logout", "Customer", new { area = "" }); } @@ -54,70 +52,92 @@ public static string Picture(this UrlHelper urlHelper, Picture picture, int targ return pictureService.GetUrl(picture, targetSize, fallbackType, host); } - public static string TopicUrl(this UrlHelper urlHelper, string systemName, bool popup = false) + /// + /// 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) { - var linkData = GetTopicLinkData(systemName); + Guard.NotEmpty(systemName, nameof(systemName)); - if (linkData == null) - { - return string.Empty; - } - - var routeValues = new RouteValueDictionary { ["SeName"] = linkData.SeName }; + var expression = "topic:" + systemName; if (popup) - routeValues["popup"] = true; + { + expression += "?popup=true"; + } - return urlHelper.RouteUrl("Topic", routeValues); + return Entity(urlHelper, expression); } - public static string TopicSeName(this UrlHelper urlHelper, string systemName) + /// + /// 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) { - var data = GetTopicLinkData(systemName); - return data?.SeName.EmptyNull(); - } + Guard.NotEmpty(systemName, nameof(systemName)); - public static string TopicLinkText(this UrlHelper urlHelper, string systemName) - { - var data = GetTopicLinkData(systemName); - return data?.LinkText.EmptyNull(); + return EntityLabel(urlHelper, "topic:" + systemName); } - private static TopicLinkData GetTopicLinkData(string 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) { - var container = EngineContext.Current.ContainerManager; + Guard.NotEmpty(expression, nameof(expression)); - var workContext = container.Resolve(); - var storeId = container.Resolve().CurrentStore.Id; - var cache = container.Resolve(); + var linkResolver = EngineContext.Current.Resolve(); + var link = linkResolver.Resolve(expression); - var cacheKey = string.Format(FrameworkCacheConsumer.TOPIC_SENAME_BY_SYSTEMNAME, systemName.ToLower(), workContext.WorkingLanguage.Id, storeId, workContext.CurrentCustomer.GetRolesIdent()); - var data = cache.Get(cacheKey, () => + if (link.Status == LinkStatus.Ok) { - var topicService = container.Resolve(); - var topic = topicService.GetTopicBySystemName(systemName, storeId, true); + return link.Link.EmptyNull(); + } - if (topic == null || !topic.IsPublished) - return null; + return string.Empty; + } - var seName = topic.GetSeName(); - if (seName.IsEmpty()) - return null; + /// + /// 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)); - return new TopicLinkData - { - SeName = seName, - LinkText = topic.GetLocalized(x => x.ShortTitle).Value.NullEmpty() ?? topic.GetLocalized(x => x.Title).Value.NullEmpty() ?? seName - }; - }); + var linkResolver = EngineContext.Current.Resolve(); + var link = linkResolver.Resolve(expression); - return data; - } - } + if (link.Status == LinkStatus.Ok || link.Status == LinkStatus.Forbidden) + { + return link.Label; + } - [Serializable] - public class TopicLinkData - { - public string SeName { get; set; } - public string LinkText { get; set; } + return string.Empty; + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs index 24ba430c44..bdbde8447f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs @@ -26,7 +26,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) response.AppendHeader("Content-Encoding", "deflate"); } - //response.AppendHeader("Vary", "Content-Encoding"); + response.AppendHeader("Vary", "Content-Encoding"); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs b/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs index 081c754738..25a7056aa7 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs @@ -90,6 +90,8 @@ public virtual void OnActionExecuted(ActionExecutedContext filterContext) ViewData = new ViewDataDictionary(new HandleErrorInfo(filterContext.Exception, controllerName, actionName)), TempData = filterContext.Controller.TempData }; + + _workContext.Value.IsAdmin = false; } filterContext.ExceptionHandled = true; @@ -118,6 +120,9 @@ public virtual void OnActionExecuted(ActionExecutedContext filterContext) ViewData = new ViewDataDictionary(new HandleErrorInfo(new HttpException(404, "The resource does not exist."), descriptor.ActionName, descriptor.ControllerDescriptor.ControllerName)), TempData = filterContext.Controller.TempData }; + + _workContext.Value.IsAdmin = false; + requestContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; // prevent IIS 7.0 classic mode from handling the 404/500 itself diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs index 716cc5b6ef..8a65cd046a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs @@ -19,7 +19,7 @@ public virtual void OnActionExecuting(ActionExecutingContext filterContext) if (!DataSettings.DatabaseIsInstalled()) return; - if (filterContext == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) + if (filterContext?.HttpContext?.Request == null) return; // Don't apply filter to child methods. diff --git a/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs b/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs index f9b80b1f19..5dee2d6c96 100644 --- a/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs +++ b/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs @@ -7,11 +7,8 @@ using SmartStore.Core.Data.Hooks; using SmartStore.Core.Domain.Configuration; using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Seo; -using SmartStore.Core.Domain.Stores; using SmartStore.Core.Domain.Tax; using SmartStore.Core.Domain.Themes; -using SmartStore.Core.Domain.Topics; using SmartStore.Core.Events; using SmartStore.Core.Themes; using SmartStore.Utilities; @@ -19,7 +16,7 @@ namespace SmartStore.Web.Framework { - public partial class FrameworkCacheConsumer : DbSaveHook, IConsumer + public partial class FrameworkCacheConsumer : DbSaveHook, IConsumer { /// /// Key for ThemeVariables caching @@ -41,18 +38,6 @@ 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*"; - /// - /// Key for Topic SE names - /// - /// - /// {0} : systemname - /// {1} : language id - /// {2} : store id - /// {3} : role ids - /// - public const string TOPIC_SENAME_BY_SYSTEMNAME = "fw:topic:sename.bysystemname-{0}-{1}-{2}-{3}"; - public const string TOPIC_SENAME_PATTERN_KEY = "fw:topic:sename*"; - private readonly ICacheManager _cacheManager; private readonly IAssetCache _assetCache; @@ -82,26 +67,6 @@ public override void OnAfterSave(IHookedEntity entry) { _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); } - else if (entry.Entity is UrlRecord ur) - { - var entityName = ur.EntityName.ToLowerInvariant(); - if (entityName == "topic") - { - _cacheManager.RemoveByPattern(TOPIC_SENAME_PATTERN_KEY); - } - } - else if (entry.Entity is StoreMapping sm) - { - var entityName = sm.EntityName.ToLowerInvariant(); - if (entityName == "topic") - { - _cacheManager.RemoveByPattern(TOPIC_SENAME_PATTERN_KEY); - } - } - else if (entry.Entity is Topic) - { - _cacheManager.RemoveByPattern(TOPIC_SENAME_PATTERN_KEY); - } else if (entry.Entity is Setting && entry.InitialState == EntityState.Modified) { var setting = entry.Entity as Setting; diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs index 382574d0bd..42fba7cd2b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs @@ -172,14 +172,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()); - clone.RouteExistingFiles = this.RouteExistingFiles; - clone.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/Modelling/Results/CachedFileResult.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs index 0dda117ee9..0967a4d685 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; +using System.Globalization; using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; using System.Web; using System.Web.Hosting; using System.Web.Mvc; @@ -13,230 +9,226 @@ namespace SmartStore.Web.Framework.Modelling { - public class CachedFileResult : FileResult + public class CachedFileResult : ActionResult, IFileResponse { - // Default buffer size as defined in BufferedStream type - private const int BufferSize = 0x1000; - - private HttpContextBase _httpContext; - private string _etag; - - private readonly string _path; - private readonly Func _streamReader; - private readonly Func _bufferReader; + #region Ctor public CachedFileResult(string path, string contentType = null) - : base(contentType ?? MimeTypes.MapNameToMimeType(path)) + : this(GetFileInfo(path), contentType) { - _etag = CreateETag(path); - _path = path; } - public CachedFileResult(FileInfo file, string contentType = null, Func reader = null) - : this(CreateETag(file), contentType ?? MimeTypes.MapNameToMimeType(file.Name), reader ?? file.OpenRead) + 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, Func reader = null) - : this(CreateETag(file), contentType ?? MimeTypes.MapNameToMimeType(file.Name), reader ?? file.OpenRead) + 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, string contentType = null, Func reader = null) - : this(CreateETag(file), contentType ?? MimeTypes.MapNameToMimeType(file.Name), reader ?? file.Open) + 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 etag, string contentType, Func reader) - : base(contentType) + public CachedFileResult(string contentType, DateTime lastModifiedUtc, Func factory, long? fileLength = null) { - Guard.NotNull(reader, nameof(reader)); - SanitizeETag(etag); + Guard.NotNull(factory, nameof(factory)); + Guard.NotEmpty(contentType, nameof(contentType)); - _streamReader = reader; + Transmitter = new FileStreamTransmitter(factory); + ContentType = contentType; + FileLength = fileLength; + LastModifiedUtc = lastModifiedUtc; } - public CachedFileResult(string etag, string contentType, Func reader) - : base(contentType) + public CachedFileResult(string contentType, DateTime lastModifiedUtc, Func factory, long? fileLength = null) { - Guard.NotNull(reader, nameof(reader)); - SanitizeETag(etag); + Guard.NotNull(factory, nameof(factory)); + Guard.NotEmpty(contentType, nameof(contentType)); - _bufferReader = reader; + Transmitter = new FileBufferTransmitter(factory); + ContentType = contentType; + FileLength = fileLength; + LastModifiedUtc = lastModifiedUtc; } - private void SanitizeETag(string etag) + private static FileInfo GetFileInfo(string path) { - Guard.NotEmpty(etag, nameof(etag)); + Guard.NotEmpty(path, nameof(path)); - if (etag[0] != '"') + if (!PathHelper.IsAbsolutePhysicalPath(path)) { - etag = string.Concat("\"", etag, "\""); + path = CommonHelper.MapPath(path); } - _etag = etag; + return new FileInfo(path); } - public DateTime? LastModifiedUtc { get; set; } + #endregion - public DateTime Expiration { get; set; } = DateTime.UtcNow.AddDays(7); + #region Properties - public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(7); + public string ContentType { get; private set; } + public long? FileLength { get; set; } - public static string CreateETag(string path) - { - Guard.NotEmpty(path, nameof(path)); + public DateTime LastModifiedUtc { get; set; } - if (!PathHelper.IsAbsolutePhysicalPath(path)) - { - path = CommonHelper.MapPath(path); - } + public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(1); - return CreateETag(new FileInfo(path)); - } + /// + /// If not set, will be auto-generated based on property. + /// + public string ETag { get; set; } - public static string CreateETag(VirtualFile file) - { - Guard.NotNull(file, nameof(file)); + public FileTransmitter Transmitter { get; private set; } - var fi = new FileInfo(CommonHelper.MapPath(file.VirtualPath)); - return CreateETag(fi); - } + /// + /// A callback that will be invoked on successful result execution + /// + public Action OnExecuted { get; set; } - public static string CreateETag(FileInfo file) - { - Guard.NotNull(file, nameof(file)); + #endregion - if (!file.Exists) + 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) { - throw new FileNotFoundException("File to create ETag for must exist.", file.FullName); + // use 1 second resolution + lastModified = new DateTime(utcNow.Ticks - (utcNow.Ticks % TimeSpan.TicksPerSecond), DateTimeKind.Utc); } - return CreateETag(file.FullName, file.Length.GetHashCode(), file.CreationTimeUtc, file.LastWriteTimeUtc); - } + LastModifiedUtc = lastModified; - public static string CreateETag(IFile file) - { - Guard.NotNull(file, nameof(file)); - - if (!file.Exists) + // Generate ETag if empty + if (ETag.IsEmpty()) { - throw new FileNotFoundException("File to create ETag for must exist.", file.Path); + ETag = GenerateETag(httpContext, lastModified, utcNow); } - return CreateETag(file.Path, file.Size.GetHashCode(), file.LastUpdated); - } - - public static string CreateETag(params object[] tokens) - { - var sb = new StringBuilder(); - var tag = string.Empty; + // Determine applicable file responder + var responder = ResolveResponder(request); - foreach (var token in tokens) + // Execute response (send file) + if (responder.TrySendHeaders(httpContext)) { - if (token is DateTime dt) - { - tag += dt.ToUnixTime().ToString(); - } - else - { - tag += token.Convert(); - } + responder.SendFile(httpContext); } - return "\"" + tag.Hash(Encoding.UTF8) + "\""; + // Finish: invoke the optional callback + OnExecuted?.Invoke(); } - public override void ExecuteResult(ControllerContext context) + private FileResponder ResolveResponder(HttpRequestBase request) { - _httpContext = context.HttpContext; - base.ExecuteResult(context); - } - - protected override void WriteFile(HttpResponseBase response) - { - var ifNoneMatch = _httpContext.Request.Headers["If-None-Match"]; - if (ifNoneMatch.HasValue() && _etag == ifNoneMatch) + // is this a request for an unmodified file? + var ifNoneMatch = request.Headers["If-None-Match"]; + if (ifNoneMatch.HasValue() && ETag == ifNoneMatch) { - // File hasn't changed, so return HTTP 304 without retrieving the data - 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"); - - ApplyResponseHeaders(response, false); + return new UnmodifiedFileResponder(this); } - else - { - if (_path != null) - { - response.TransmitFile(_path); - } - else if (_streamReader != null) - { - var stream = _streamReader(); - if (stream == null) - { - throw new NullReferenceException("File stream cannot be NULL."); - } - // Grab chunks of data and write to the output stream - var outputStream = response.OutputStream; - using (stream) - { - var buffer = new byte[BufferSize]; - - while (true) - { - int bytesRead = stream.Read(buffer, 0, BufferSize); - if (bytesRead == 0) - { - // no more data - break; - } - - outputStream.Write(buffer, 0, bytesRead); - } - } - } - else if (_bufferReader != null) - { - var buffer = _bufferReader(); - if (buffer == null) - { - throw new NullReferenceException("File buffer cannot be NULL."); - } - - // Write buffer to output stream - response.OutputStream.Write(buffer, 0, buffer.Length); - } - - ApplyResponseHeaders(response, true); - - // Set ETag for served file (revalidated on subsequent requests) - response.Cache.SetETag(_etag); + // 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 void ApplyResponseHeaders(HttpResponseBase response, bool setLastModifiedDate) + private static string GenerateETag(HttpContextBase context, DateTime lastModified, DateTime now) { - var cache = response.Cache; - - cache.SetCacheability(System.Web.HttpCacheability.Public); - cache.VaryByHeaders["Accept-Encoding"] = true; - cache.SetExpires(Expiration); - cache.SetMaxAge(MaxAge); - cache.SetRevalidation(HttpCacheRevalidation.AllCaches); - - if (setLastModifiedDate && LastModifiedUtc.HasValue) - { - cache.SetLastModified(LastModifiedUtc.Value); - } + // 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 new file mode 100644 index 0000000000..3b30853fe1 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileResponder.cs @@ -0,0 +1,561 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Net; +using System.Web; + +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); + + return true; + } + + public abstract void SendFile(HttpContextBase context); + } + + + + 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 new file mode 100644 index 0000000000..82b2399294 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileTransmitter.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Web; + +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; + + using (stream) + { + int bufferSize = (int)Math.Min(DefaultBufferSize, length); + byte[] buffer = new byte[bufferSize]; + + int read; + long remaining = length; + + stream.Seek(offset, SeekOrigin.Begin); + while ((remaining > 0) && (read = stream.Read(buffer, 0, buffer.Length)) != 0) + { + if (response.IsClientConnected) + { + response.OutputStream.Write(buffer, 0, read); + //response.Flush(); + + remaining -= read; + } + 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 new file mode 100644 index 0000000000..f490fe8dac --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/IFileResponse.cs @@ -0,0 +1,14 @@ +using System; + +namespace SmartStore.Web.Framework.Modelling +{ + public interface IFileResponse + { + string ContentType { get; } + long? FileLength { get; } + DateTime LastModifiedUtc { get; } + TimeSpan MaxAge { get; } + string ETag { get; } + FileTransmitter Transmitter { get; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RootActionViewResult.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RootActionViewResult.cs new file mode 100644 index 0000000000..2e9f733b80 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/RootActionViewResult.cs @@ -0,0 +1,31 @@ +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)}."); + + 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)}."); + + return base.FindView(context.GetRootControllerContext()); + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs index c498275972..9f8d8026f0 100644 --- a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs @@ -24,33 +24,30 @@ public class PluginMediator public PluginMediator(ICommonServices services, IComponentContext ctx) { - this._services = services; - this._ctx = ctx; - - T = NullLocalizer.Instance; + _services = services; + _ctx = ctx; } - public Localizer T { get; set; } - - public string GetLocalizedFriendlyName(ProviderMetadata metadata, int languageId = 0, bool returnDefaultValue = true) + public string GetLocalizedFriendlyName(IProviderMetadata metadata, int languageId = 0, bool returnDefaultValue = true) { return GetLocalizedValue(metadata, "FriendlyName", x => x.FriendlyName, languageId, returnDefaultValue); } - public string GetLocalizedDescription(ProviderMetadata metadata, int languageId = 0, bool returnDefaultValue = true) + public string GetLocalizedDescription(IProviderMetadata metadata, int languageId = 0, bool returnDefaultValue = true) { return GetLocalizedValue(metadata, "Description", x => x.Description, languageId, returnDefaultValue); } - public string GetLocalizedValue(ProviderMetadata metadata, + public string GetLocalizedValue(TMetadata metadata, string propertyName, - Expression> fallback, + Expression> fallback, int languageId = 0, - bool returnDefaultValue = true) + bool returnDefaultValue = true) + where TMetadata : IProviderMetadata { Guard.NotNull(metadata, nameof(metadata)); - string systemName = metadata.SystemName; + var systemName = metadata.SystemName; var resourceName = metadata.ResourceKeyPattern.FormatInvariant(metadata.SystemName, propertyName); string result = _services.Localization.GetResource(resourceName, languageId, false, "", true); @@ -60,7 +57,7 @@ public string GetLocalizedValue(ProviderMetadata metadata, return result; } - public void SaveLocalizedValue(ProviderMetadata metadata, int languageId, string propertyName, string value) + public void SaveLocalizedValue(IProviderMetadata metadata, int languageId, string propertyName, string value) { Guard.NotNull(metadata, nameof(metadata)); Guard.IsPositive(languageId, nameof(languageId)); @@ -88,7 +85,7 @@ public void SaveLocalizedValue(ProviderMetadata metadata, int languageId, string if (value.HasValue()) { // insert - resource = new LocaleStringResource() + resource = new LocaleStringResource { LanguageId = languageId, ResourceName = resourceName, diff --git a/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs deleted file mode 100644 index 4fa6c14d76..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Web.Mvc; -using SmartStore.Core; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Security; - -namespace SmartStore.Web.Framework.Security -{ - public class RequireHttpsByConfigAttribute : FilterAttribute, IAuthorizationFilter - { - public RequireHttpsByConfigAttribute(SslRequirement sslRequirement) - { - this.SslRequirement = sslRequirement; - } - - public Lazy SecuritySettings { get; set; } - public Lazy StoreContext { get; set; } - public Lazy WebHelper { get; set; } - - 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)) - return; - - if (!DataSettings.DatabaseIsInstalled()) - return; - - var securitySettings = SecuritySettings.Value; - var isLocalRequest = filterContext.HttpContext.Request.IsLocal; - - if (!securitySettings.UseSslOnLocalhost && isLocalRequest) - return; - - var webHelper = WebHelper.Value; - - var currentConnectionSecured = webHelper.IsCurrentConnectionSecured(); - var storeContext = StoreContext.Value; - var currentStore = storeContext.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) - { - if (currentStore != null && currentStore.GetSecurityMode() > HttpSecurityMode.Unsecured) - { - // redirect to HTTPS version of page - // string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl; - - string url = webHelper.GetThisPageUrl(true, true); - filterContext.Result = new RedirectResult(url, !isLocalRequest); - } - } - } - break; - case SslRequirement.No: - { - if (currentConnectionSecured) - { - // redirect to HTTP version of page - // string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl; - string url = webHelper.GetThisPageUrl(true, false); - filterContext.Result = new RedirectResult(url, !isLocalRequest); - } - } - break; - case SslRequirement.Retain: - { - //do nothing - } - break; - default: - throw new SmartException("Unsupported SslRequirement parameter"); - } - } - - public SslRequirement SslRequirement { get; set; } - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/Security/RewriteUrlAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/RewriteUrlAttribute.cs new file mode 100644 index 0000000000..f63ad4b5ba --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Security/RewriteUrlAttribute.cs @@ -0,0 +1,137 @@ +using System; +using System.Web.Mvc; +using SmartStore.Core; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Security; +using SmartStore.Core.Domain.Seo; + +namespace SmartStore.Web.Framework.Security +{ + public class RewriteUrlAttribute : FilterAttribute, IAuthorizationFilter + { + public RewriteUrlAttribute(SslRequirement sslRequirement) + { + this.SslRequirement = sslRequirement; + this.Order = 100; + } + + public Lazy SeoSettings { get; set; } + public Lazy SecuritySettings { get; set; } + public Lazy StoreContext { get; set; } + public Lazy WebHelper { get; set; } + + 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)) + return; + + if (!DataSettings.DatabaseIsInstalled()) + return; + + var securitySettings = SecuritySettings.Value; + var isLocalRequest = filterContext.HttpContext.Request.IsLocal; + + if (!securitySettings.UseSslOnLocalhost && isLocalRequest) + return; + + var webHelper = WebHelper.Value; + + var currentConnectionSecured = webHelper.IsCurrentConnectionSecured(); + var currentStore = StoreContext.Value.CurrentStore; + + if (currentStore.ForceSslForAllPages) + { + // all pages are forced to be SSL no matter of the specified value + this.SslRequirement = SslRequirement.Yes; + } + + var securityMode = currentStore.GetSecurityMode(); + + switch (this.SslRequirement) + { + case SslRequirement.Yes: + { + if (!currentConnectionSecured && securityMode > HttpSecurityMode.Unsecured) + { + // Redirect to HTTPS version of page + string url = webHelper.GetThisPageUrl(true, true); + filterContext.Result = new RedirectResult(url, !isLocalRequest); + } + } + break; + case SslRequirement.No: + { + if (currentConnectionSecured) + { + // Redirect to HTTP version of page + string url = webHelper.GetThisPageUrl(true, false); + filterContext.Result = new RedirectResult(url, !isLocalRequest); + } + } + break; + case SslRequirement.Retain: + { + // Do nothing + } + break; + default: + throw new SmartException("Unsupported SslRequirement parameter"); + } + + ApplyCanonicalHostNameRule(filterContext, isLocalRequest, securityMode); + } + + private void ApplyCanonicalHostNameRule(AuthorizationContext filterContext, bool isLocalRequest, HttpSecurityMode securityMode) + { + var rule = SeoSettings.Value.CanonicalHostNameRule; + if (rule == CanonicalHostNameRule.NoRule) + return; + + var uri = filterContext.Result != null + ? new Uri(((RedirectResult)filterContext.Result).Url) + : filterContext.HttpContext.Request.Url; + + if (uri.IsLoopback) + { + // allows testing of "localtest.me" + return; + } + + var 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 (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; + } + } + + if (hasPrefix && rule == CanonicalHostNameRule.OmitWww) + { + url = url.Replace("://www.", "://"); + filterContext.Result = new RedirectResult(url, !isLocalRequest); + } + + if (!hasPrefix && rule == CanonicalHostNameRule.RequireWww) + { + url = url.Replace("{0}://".FormatInvariant(protocol), startsWith); + filterContext.Result = new RedirectResult(url, !isLocalRequest); + } + } + + public SslRequirement SslRequirement { get; set; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs deleted file mode 100644 index 2c9188b241..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Web.Mvc; -using SmartStore.Core; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Seo; -using SmartStore.Core.Infrastructure; - -namespace SmartStore.Web.Framework.Seo -{ - public class CanonicalHostNameAttribute : FilterAttribute, IAuthorizationFilter - { - public Lazy SeoSettings { get; set; } - public Lazy WebHelper { get; set; } - public Lazy StoreContext { get; set; } - - public virtual void OnAuthorization(AuthorizationContext filterContext) - { - if (filterContext == null) - throw new ArgumentNullException("filterContext"); - - if (filterContext.IsChildAction) - return; - - // only redirect for GET requests - if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) - return; - - if (!DataSettings.DatabaseIsInstalled()) - return; - - var rule = SeoSettings.Value.CanonicalHostNameRule; - if (rule == CanonicalHostNameRule.NoRule) - return; - - var uri = filterContext.HttpContext.Request.Url; - - if (uri.IsLoopback) - { - // allows testing of "localtest.me" - return; - } - - var 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) - { - var securityMode = StoreContext.Value.CurrentStore.GetSecurityMode(); - if (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; - } - } - - if (hasPrefix && rule == CanonicalHostNameRule.OmitWww) - { - url = url.Replace("://www.", "://"); - filterContext.Result = new RedirectResult(url, true); - } - - if (!hasPrefix && rule == CanonicalHostNameRule.RequireWww) - { - url = url.Replace("{0}://".FormatInvariant(protocol), startsWith); - filterContext.Result = new RedirectResult(url, true); - } - - } - - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj index 3f045c8f77..bcf02806b0 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj +++ b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj @@ -1,5 +1,6 @@  - + + Debug AnyCPU @@ -214,6 +215,7 @@ Properties\AssemblyVersionInfo.cs + @@ -226,8 +228,12 @@ + + + + @@ -280,7 +286,6 @@ - @@ -288,6 +293,7 @@ + @@ -297,12 +303,6 @@ - - - - - - @@ -312,7 +312,10 @@ - + + + + @@ -340,13 +343,12 @@ - + - @@ -379,11 +381,24 @@ - - - - - + + + + + + + + + + + + + + + + + + @@ -391,7 +406,7 @@ - + diff --git a/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs b/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs index 26e9c264c2..67097db01a 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs +++ b/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs @@ -14,19 +14,10 @@ using SmartStore.Core.Infrastructure; using SmartStore.Core.Events; using SmartStore.Core.Data; +using SmartStore.Collections; namespace SmartStore.Web.Framework { - public class HttpModuleInitializedEvent - { - public HttpModuleInitializedEvent(HttpApplication application) - { - Application = application; - } - - public HttpApplication Application { get; private set; } - } - /// /// Request event sequence: /// - BeginRequest @@ -55,7 +46,12 @@ public HttpModuleInitializedEvent(HttpApplication application) public class SmartUrlRoutingModule : IHttpModule { private static readonly object _contextKey = new object(); - private static readonly ConcurrentBag _routes = new ConcurrentBag(); + + private static readonly ICollection> _actions = + new SyncedCollection>(new List>()) { ReadLockFree = true }; + + private static readonly ICollection _routes = + new SyncedCollection(new List()) { ReadLockFree = true }; static SmartUrlRoutingModule() { @@ -92,15 +88,32 @@ public void Init(HttpApplication application) // 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. - EngineContext.Current.Resolve().Publish(new HttpModuleInitializedEvent(application)); + 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 /// diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs index 3dfae1a5dc..5698f20851 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Web.Mvc; +using SmartStore.Core; namespace SmartStore.Web.Framework.Theming { @@ -15,18 +16,37 @@ namespace SmartStore.Web.Framework.Theming /// 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 virtual void OnResultExecuting(ResultExecutingContext filterContext) { - if (filterContext == null || filterContext.Result == null) + if (filterContext?.Result == null) return; - // add extra view location formats to all view results (even the partial ones) - // {0} is appended by view engine - filterContext.RouteData.DataTokens["ExtraAreaViewLocations"] = new string[] + if (!filterContext.Result.IsHtmlViewResult()) + return; + + if (filterContext.HttpContext.Response.StatusCode < 300) { - "~/Administration/Views/{1}/{0}", - "~/Administration/Views/Shared/{0}" - }; + 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; + } + } + } } 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 1e3e3fbd6c..62bf5fd53e 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs @@ -7,6 +7,7 @@ using BundleTransformer.SassAndScss.Translators; using SmartStore.Core.Infrastructure; using SmartStore.Core.Logging; +using SmartStore.Utilities.Threading; namespace SmartStore.Web.Framework.Theming.Assets { @@ -71,7 +72,7 @@ private IAsset TranslateInternal(IAsset asset) if (validationMode || !TryGetCachedAsset(asset, out result)) { - lock (String.Intern("CachedAsset:" + asset.VirtualPath)) + using (KeyedLock.Lock("CachedAsset:" + asset.VirtualPath)) { if (validationMode || !TryGetCachedAsset(asset, out result)) { diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs index 7ae70c3a2c..edb20c5c8b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs @@ -13,8 +13,6 @@ public sealed class BundlingVirtualPathProvider : ThemingVirtualPathProvider { private readonly SassCheckedPathStack _sassCheckedPathStack; - private string _yoooooo; - public BundlingVirtualPathProvider(VirtualPathProvider previous) : base(previous) { @@ -49,15 +47,6 @@ public override bool FileExists(string virtualPath) } } - //if (virtualPath.Contains("slick.scss")) - //{ - // var xxx = true; - //// throw new Exception("dfsfs"); - //} - - //System.Diagnostics.Debug.WriteLine("VPATH: " + virtualPath); - _yoooooo += virtualPath + Environment.NewLine; - exists = base.FileExists(virtualPath); if (exists && sassPath != null) @@ -88,80 +77,20 @@ public override VirtualFile GetFile(string virtualPath) return base.GetFile(virtualPath); } - public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies) - { - var styleResult = ThemeHelper.IsStyleSheet(virtualPath); - if (styleResult.IsPreprocessor && !(styleResult.IsThemeVars || styleResult.IsModuleImports) && virtualPathDependencies != null) - { - // Exclude the special imports from the file dependencies list - return base.GetFileHash(virtualPath, ThemeHelper.RemoveVirtualImports(virtualPathDependencies.Cast())); - } - - return base.GetFileHash(virtualPath, virtualPathDependencies); - } - public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) { - var styleResult = ThemeHelper.IsStyleSheet(virtualPath); - - if (styleResult == null || styleResult.IsCss) - { - return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); - } - - if (styleResult.IsThemeVars || styleResult.IsModuleImports) + if (ThemeHelper.IsStyleValidationRequest()) { - return null; - } - - // Is Sass Or Less Or StyleBundle - - var arrPathDependencies = virtualPathDependencies.Cast().ToArray(); - - - // Exclude the special imports from the file dependencies list, - // 'cause this one cannot be monitored by the physical file system - var fileDependencies = ThemeHelper.RemoveVirtualImports(arrPathDependencies); - - if (fileDependencies == arrPathDependencies) - { - // No themevars or moduleimports import... so no special considerations here - return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); - } - - if (fileDependencies.Any()) - { - string cacheKey = null; - - var isThemeableAsset = (!styleResult.IsBundle && ThemeHelper.PathIsInheritableThemeFile(virtualPath)) - || (styleResult.IsBundle && fileDependencies.Any(x => ThemeHelper.PathIsInheritableThemeFile(x))); - - if (isThemeableAsset) + var themeVarsPath = virtualPathDependencies.Cast().FirstOrDefault(x => ThemeHelper.PathIsThemeVars(x)); + if (themeVarsPath.HasValue()) { - var theme = ThemeHelper.ResolveCurrentTheme(); - int storeId = ThemeHelper.ResolveCurrentStoreId(); - // invalidate the cache when variables change - cacheKey = FrameworkCacheConsumer.BuildThemeVarsCacheKey(theme.ThemeName, storeId); - - if (styleResult.IsSass && (ThemeHelper.IsStyleValidationRequest())) - { - // Special case: ensure that cached validation result gets nuked in a while, - // when ThemeVariableService publishes the entity changed messages. - return new CacheDependency(new string[0], new string[] { cacheKey }, utcStart); - } + return base.GetCacheDependency(virtualPath, new[] { themeVarsPath }, utcStart); } + } - var files = ThemingVirtualPathProvider.MapDependencyPaths(fileDependencies); - - return new CacheDependency( - files, - cacheKey == null ? new string[0] : new string[] { cacheKey }, - utcStart); - } - - return null; + return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); } - } + } internal class SassCheckedPathStack { @@ -200,7 +129,7 @@ public bool Check(StyleSheetResult styleResult, out TokenizedSassPath path) return false; } - if (currentPath.StyleResult.IsExplicit && lastPath.StyleResult.IsExplicit && currentPath.FileName == lastPath.FileName) + if (currentPath.StyleResult.IsBaseImport && lastPath.StyleResult.IsBaseImport && currentPath.FileName == lastPath.FileName) { return true; } @@ -222,7 +151,7 @@ public bool Check(StyleSheetResult styleResult, out TokenizedSassPath path) } // slick.(sass|css) > slick.scss - if (!currentPath.StyleResult.IsExplicit && _styleExtensions.Contains(currentPath.Extension) && currentPath.FileNameWithoutExtension == lastPath.FileNameWithoutExtension) + if (!currentPath.StyleResult.IsBaseImport && _styleExtensions.Contains(currentPath.Extension) && currentPath.FileNameWithoutExtension == lastPath.FileNameWithoutExtension) { return true; } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs index a834f3cfd6..f1eaf270da 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs @@ -13,6 +13,7 @@ using SmartStore.Core.Logging; using SmartStore.Core.Themes; using SmartStore.Services; +using SmartStore.Utilities.Threading; namespace SmartStore.Web.Framework.Theming.Assets { @@ -23,7 +24,6 @@ public class DefaultAssetCache : IAssetCache public const string UrlRewriteCode = "urlrewrite"; public static readonly IAssetCache Null = new NullCache(); - private static readonly object _lock = new object(); const string CacheKeyPrefix = "sm:AssetCacheEntry:"; private readonly bool _isEnabled; @@ -81,10 +81,8 @@ public CachedAssetEntry GetAsset(string virtualPath) Guard.NotEmpty(virtualPath, nameof(virtualPath)); - string themeName; - int storeId; - var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out themeName, out storeId); - + var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); + if (CacheFolder.DirectoryExists(cacheDirectoryName)) { try @@ -92,10 +90,7 @@ public CachedAssetEntry GetAsset(string virtualPath) var deps = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.dependencies")); var hash = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.hash")); - IEnumerable parsedDeps; - string currentHash; - - if (!TryValidate(virtualPath, deps, hash, out parsedDeps, out currentHash)) + 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); @@ -106,7 +101,7 @@ public CachedAssetEntry GetAsset(string virtualPath) var content = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.css")); if (content == null) { - lock (_lock) + using (KeyedLock.Lock(BuildLockKey(cacheDirectoryName))) { InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); return null; @@ -150,12 +145,10 @@ public CachedAssetEntry InsertAsset(string virtualPath, IEnumerable virt Guard.NotEmpty(virtualPath, nameof(virtualPath)); Guard.NotEmpty(content, nameof(content)); - lock (_lock) - { - string themeName; - int storeId; - var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out themeName, out storeId); + var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); + using (KeyedLock.Lock(BuildLockKey(cacheDirectoryName))) + { CacheFolder.CreateDirectory(cacheDirectoryName); try @@ -205,14 +198,11 @@ public CachedAssetEntry InsertAsset(string virtualPath, IEnumerable virt public void Clear() { - lock (_lock) + if (_env.TenantFolder.DirectoryExists("AssetCache")) { - if (_env.TenantFolder.DirectoryExists("AssetCache")) - { - _env.TenantFolder.TryDeleteDirectory("AssetCache"); - // Remove the eviction observer also - HttpRuntime.Cache.RemoveByPattern(CacheKeyPrefix); - } + _env.TenantFolder.TryDeleteDirectory("AssetCache"); + // Remove the eviction observer also + HttpRuntime.Cache.RemoveByPattern(CacheKeyPrefix); } } @@ -220,12 +210,10 @@ public bool InvalidateAsset(string virtualPath) { Guard.NotEmpty(virtualPath, nameof(virtualPath)); - lock (_lock) - { - string themeName; - int storeId; - var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out themeName, out storeId); + var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); + using (KeyedLock.Lock(BuildLockKey(cacheDirectoryName))) + { if (CacheFolder.DirectoryExists(cacheDirectoryName)) { return InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); @@ -235,6 +223,11 @@ public bool InvalidateAsset(string virtualPath) } } + 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 /// diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs index 720b884ee4..34b4557fe5 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs @@ -5,18 +5,46 @@ using System.Web.Hosting; using SmartStore.Core.Plugins; using SmartStore.Core.Data; +using System; +using SmartStore.Utilities; namespace SmartStore.Web.Framework.Theming.Assets { - public class ModuleImportsVirtualFile : VirtualFile + public sealed class ModuleImport { - private static readonly HashSet _adminImports; - private static readonly HashSet _publicImports; + 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(); + _adminImports = new HashSet(); + _publicImports = new HashSet(); if (DataSettings.DatabaseIsInstalled()) { @@ -35,24 +63,40 @@ private static void CollectModuleImports() if (!Directory.Exists(contentDir)) continue; - if (File.Exists(Path.Combine(contentDir, "public.scss"))) - { - _publicImports.Add($"{root}/{plugin.FolderName}/Content/public.scss"); - } + TryAddImport(plugin, _publicImports, "public.scss"); + TryAddImport(plugin, _adminImports, "admin.scss"); + } - if (File.Exists(Path.Combine(contentDir, "admin.scss"))) + void TryAddImport(PluginDescriptor plugin, HashSet imports, string name) + { + var physicalPath = Path.Combine(plugin.PhysicalPath, "Content", name); + if (File.Exists(physicalPath)) { - _adminImports.Add($"{root}/{plugin.FolderName}/Content/admin.scss"); + imports.Add(new ModuleImport + { + PhysicalPath = physicalPath, + VirtualPath = $"{root}/{plugin.FolderName}/Content/{name}", + PluginDescriptor = plugin, + IsAdmin = name.Contains("admin") + }); } } } - private readonly bool _isAdmin; + public static ModuleImport[] PublicImports + { + get => _publicImports.ToArray(); + } + + public static ModuleImport[] AdminImports + { + get => _adminImports.ToArray(); + } public ModuleImportsVirtualFile(string virtualPath, bool isAdmin) : base(virtualPath) { - _isAdmin = isAdmin; + IsAdmin = isAdmin; } public override bool IsDirectory @@ -60,19 +104,36 @@ 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; + var imports = IsAdmin ? _adminImports : _publicImports; foreach (var imp in imports) { - sb.AppendLine($"@import '{imp}';"); + 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(); @@ -86,5 +147,13 @@ private Stream GenerateStreamFromString(string value) } } + 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/CssHttpHandler.cs b/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs index 3a1cc2ac7c..e2fd7aa151 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs @@ -69,8 +69,7 @@ protected override string GetCacheKey(string assetVirtualPath, string bundleVirt if (IsThemeableRequest()) { - var httpContext = HttpContext.Current; - if (httpContext != null && httpContext.Request != null) + if (HttpContext.Current?.Request != null) { var qs = QueryString.Current; if (qs.Count > 0) diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs b/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs index 1ea6b7484b..dbbc5a5ab4 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs @@ -133,18 +133,18 @@ public InheritedThemeFileResult Resolve(string virtualPath) return null; } - bool isExplicit = false; + bool isBased = false; virtualPath = ThemeHelper.TokenizePath(virtualPath, out var requestedThemeName, out var relativePath, out var query); Func nullOrFile = () => { - return isExplicit - ? new InheritedThemeFileResult { IsExplicit = true, OriginalVirtualPath = virtualPath, Query = query } + return isBased + ? new InheritedThemeFileResult { IsBased = true, OriginalVirtualPath = virtualPath, Query = query } : null; }; - ThemeManifest currentTheme = ResolveTheme(requestedThemeName, relativePath, query, out isExplicit); + ThemeManifest currentTheme = ResolveTheme(requestedThemeName, relativePath, query, out isBased); if (currentTheme?.BaseTheme == null) { @@ -160,7 +160,7 @@ public InheritedThemeFileResult Resolve(string virtualPath) return nullOrFile(); } } - else if (isExplicit && currentTheme.BaseTheme != null) + else if (isBased && currentTheme.BaseTheme != null) { // A file from the base theme has been requested currentTheme = currentTheme.BaseTheme; @@ -188,7 +188,7 @@ public InheritedThemeFileResult Resolve(string virtualPath) ResultPhysicalPath = resultPhysicalPath, OriginalThemeName = requestedThemeName, ResultThemeName = actualLocation, - IsExplicit = isExplicit, + IsBased = isBased, Query = query }; } @@ -204,9 +204,9 @@ public InheritedThemeFileResult Resolve(string virtualPath) return result; } - private ThemeManifest ResolveTheme(string requestedThemeName, string relativePath, string query, out bool isExplicit) + private ThemeManifest ResolveTheme(string requestedThemeName, string relativePath, string query, out bool isBased) { - isExplicit = false; + isBased = false; ThemeManifest currentTheme; var isAdmin = EngineContext.Current.Resolve().IsAdmin; @@ -237,16 +237,16 @@ private ThemeManifest ResolveTheme(string requestedThemeName, string relativePat currentTheme = ThemeHelper.ResolveCurrentTheme(); - if (isPreprocessor && query != null && query.StartsWith("explicit", StringComparison.OrdinalIgnoreCase)) + 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 'explicit' query the resolution starting point + // 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); - isExplicit = true; + isBased = true; } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs new file mode 100644 index 0000000000..e36f9c9fd4 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace SmartStore.Web.Framework.Theming +{ + 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 37bb50a4b2..ad49bed1d2 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs @@ -1,11 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; using System.Web.Hosting; using SmartStore.Core.Themes; namespace SmartStore.Web.Framework.Theming { - internal class InheritedVirtualThemeFile : VirtualFile + internal class InheritedVirtualThemeFile : VirtualFile, IFileDependencyProvider { public InheritedVirtualThemeFile(InheritedThemeFileResult resolveResult) : base(DetermineVirtualPath(resolveResult)) @@ -39,5 +40,9 @@ private static string DetermineVirtualPath(InheritedThemeFileResult resolveResul } } - } + 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/SmartVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs index 0c9232dfb6..32c6d31fe8 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs @@ -146,7 +146,7 @@ private bool IsExtensionPath(string virtualPath, out string root, out string app } } - internal class DebugVirtualFile : VirtualFile + internal class DebugVirtualFile : VirtualFile, IFileDependencyProvider { public DebugVirtualFile(string virtualPath, string debugPath) : base(virtualPath) @@ -160,11 +160,16 @@ 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); + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeContext.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeContext.cs index 5145c9ea89..ce5b268dbf 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeContext.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeContext.cs @@ -147,7 +147,7 @@ public string GetRequestTheme() { try { - return (string)_httpContext.Request.RequestContext.RouteData.DataTokens[OverriddenThemeNameKey]; + return (string)_httpContext.Request?.RequestContext?.RouteData?.DataTokens[OverriddenThemeNameKey]; } catch { diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs index 949190ae40..074b0e48cb 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs @@ -26,7 +26,7 @@ static ThemeHelper() { ThemesBasePath = CommonHelper.GetAppSetting("sm:ThemesBasePath", "~/Themes/").EnsureEndsWith("/"); - var pattern = @"^{0}(.*)/(.+)(\.)(png|gif|jpg|jpeg|css|scss|js|cshtml|svg|json|liquid)(\?explicit)*$".FormatInvariant(ThemesBasePath); + 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); @@ -37,7 +37,7 @@ internal static IEnumerable RemoveVirtualImports(IEnumerable vir { Guard.NotNull(virtualPathDependencies, nameof(virtualPathDependencies)); - // determine the virtual themevarsscss import reference + // 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(); @@ -116,6 +116,22 @@ internal static bool IsStyleValidationRequest() 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") @@ -150,17 +166,6 @@ internal static StyleSheetResult IsStyleSheet(string path) } } } - else if (extension.Contains("?explicit")) - { - // Handle virtual Sass imports with '?explicit' query - // TBD: (mc) other query params could exist - - // Process again, this time without query - var pathWithoutQuery = path.Substring(0, path.IndexOf('?')); - var result = IsStyleSheet(pathWithoutQuery); - result.IsExplicit = true; - return result; - } return null; } @@ -204,7 +209,7 @@ internal class StyleSheetResult public bool IsCss { get; set; } public bool IsSass { get; set; } public bool IsBundle { get; set; } - public bool IsExplicit { get; set; } + public bool IsBaseImport { get; set; } public bool IsPreprocessor { diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs index 7ab2289ec8..b4bd7ab49b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs @@ -1,15 +1,12 @@ +using System.Collections.Generic; using System.IO; using System.Text; using System.Web.Hosting; namespace SmartStore.Web.Framework.Theming { - public class ThemeVarsVirtualFile : VirtualFile + public class ThemeVarsVirtualFile : VirtualFile, IFileDependencyProvider { - private readonly string _extension; - private readonly string _themeName; - private readonly int _storeId; - public ThemeVarsVirtualFile(string virtualPath, string themeName, int storeId) : this(virtualPath, Path.GetExtension(virtualPath), themeName, storeId) { @@ -18,24 +15,28 @@ 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; + Extension = extension; + ThemeName = themeName; + StoreId = storeId; } public override bool IsDirectory { get { return 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(); - if (_themeName.IsEmpty()) + if (ThemeName.IsEmpty()) return GenerateStreamFromString(string.Empty); - var css = repo.GetPreprocessorCss(_extension, _themeName, _storeId); + var css = repo.GetPreprocessorCss(Extension, ThemeName, StoreId); return GenerateStreamFromString(css); } @@ -52,5 +53,9 @@ private Stream GenerateStreamFromString(string value) } } - } + 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/ThemeableVirtualPathProviderViewEngine.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs index 9df61823c7..d7b0e936d2 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs @@ -185,6 +185,12 @@ protected virtual string ResolveViewPath( // "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(); diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs index dc4102e768..cef62c8959 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs @@ -8,6 +8,7 @@ using SmartStore.Core.Infrastructure; using SmartStore.Core.Themes; using SmartStore.Utilities; +using SmartStore.Web.Framework.Theming.Assets; namespace SmartStore.Web.Framework.Theming { @@ -31,7 +32,7 @@ public override bool FileExists(string virtualPath) var result = GetResolveResult(virtualPath); if (result != null) { - if (!result.IsExplicit) + if (!result.IsBased) { return true; } @@ -63,7 +64,7 @@ public override VirtualFile GetFile(string virtualPath) { // File is an inherited theme file. Set the result virtual path. virtualPath = result.ResultVirtualPath ?? result.OriginalVirtualPath; - if (!result.IsExplicit) + if (!result.IsBased) { file = new InheritedVirtualThemeFile(result); } @@ -89,7 +90,7 @@ public override string GetFileHash(string virtualPath, IEnumerable virtualPathDe return _previous.GetFileHash(virtualPath, virtualPathDependencies); } - var fileNames = MapDependencyPaths(virtualPathDependencies.Cast()); + var fileNames = MapDependencyPaths(virtualPathDependencies.Cast(), out _); var combiner = HashCodeCombiner.Start(); foreach (var fileName in fileNames) @@ -107,40 +108,41 @@ public override CacheDependency GetCacheDependency(string virtualPath, IEnumerab return null; } - return new CacheDependency(MapDependencyPaths(virtualPathDependencies.Cast()), utcStart); + var mappedPaths = MapDependencyPaths(virtualPathDependencies.Cast(), out var cacheKeys); + + return new CacheDependency(mappedPaths, cacheKeys, utcStart); } - internal static string[] MapDependencyPaths(IEnumerable virtualPathDependencies) + /// + /// Maps virtual to physical paths. Used to compute cache dependecies and file hashes. + /// + internal string[] MapDependencyPaths(IEnumerable virtualPathDependencies, out string[] cacheKeys) { - // Maps virtual to physical paths. Used to compute cache dependecies and file hashes. + cacheKeys = null; - var fileNames = new List(); + var mappedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + var cacheKeySet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var dep in virtualPathDependencies) { - string mappedPath = null; - var file = HostingEnvironment.VirtualPathProvider.GetFile(dep); + var file = GetFile(dep); - if (file is InheritedVirtualThemeFile file1) - { - mappedPath = file1.ResolveResult.ResultPhysicalPath; - } - else if (file is DebugVirtualFile file2) + if (file is IFileDependencyProvider provider) { - mappedPath = file2.PhysicalPath; + provider.AddFileDependencies(mappedPaths, cacheKeySet); } else if (file != null) { - mappedPath = HostingEnvironment.MapPath(file.VirtualPath); - } - - if (mappedPath.HasValue()) - { - fileNames.Add(mappedPath); + mappedPaths.Add(HostingEnvironment.MapPath(file.VirtualPath)); } } - return fileNames.ToArray(); + cacheKeys = cacheKeySet.ToArray(); + + var paths = mappedPaths.ToArray(); + Array.Sort(paths); + + return paths; } private static InheritedThemeFileResult GetResolveResult(string virtualPath) diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs index 6593d8fad7..63cf24718d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs @@ -10,6 +10,7 @@ using SmartStore.Core.Logging; using SmartStore.Core.Themes; using SmartStore.Services; +using SmartStore.Services.Cms; namespace SmartStore.Web.Framework.Theming { @@ -109,8 +110,16 @@ public IWorkContext WorkContext return _helper.Services.WorkContext; } } - - public override void InitHelpers() + + public ILinkResolver LinkResolver + { + get + { + return _helper.LinkResolver; + } + } + + public override void InitHelpers() { base.InitHelpers(); @@ -121,19 +130,19 @@ public override void InitHelpers() public HelperResult RenderWrappedSection(string name, object wrapperHtmlAttributes) { Action action = delegate(TextWriter tw) - { - var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(wrapperHtmlAttributes); - var tagBuilder = new TagBuilder("div"); - tagBuilder.MergeAttributes(htmlAttributes); - - var section = this.RenderSection(name, false); - if (section != null) - { - tw.Write(tagBuilder.ToString(TagRenderMode.StartTag)); - section.WriteTo(tw); - tw.Write(tagBuilder.ToString(TagRenderMode.EndTag)); - } - }; + { + var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(wrapperHtmlAttributes); + var tagBuilder = new TagBuilder("div"); + tagBuilder.MergeAttributes(htmlAttributes); + + var section = this.RenderSection(name, false); + if (section != null) + { + tw.Write(tagBuilder.ToString(TagRenderMode.StartTag)); + section.WriteTo(tw); + tw.Write(tagBuilder.ToString(TagRenderMode.EndTag)); + } + }; return new HelperResult(action); } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs index 8ac41dfef5..912fe7c258 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs @@ -12,6 +12,7 @@ using SmartStore.Core.Domain; using SmartStore.Services.Customers; using SmartStore.Core.Domain.Security; +using SmartStore.Services.Cms; namespace SmartStore.Web.Framework.Theming { @@ -42,12 +43,13 @@ public WebViewPageHelper() public IThemeRegistry ThemeRegistry { get; set; } public IThemeContext ThemeContext { get; set; } public IMobileDeviceHelper MobileDeviceHelper { get; set; } + public ILinkResolver LinkResolver { get; set; } public void Initialize(ViewContext viewContext) { if (!_initialized) { - _controllerContext = viewContext.GetMasterControllerContext(); + _controllerContext = viewContext.GetRootControllerContext(); _initialized = true; } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/BlockHandlerBase.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/BlockHandlerBase.cs deleted file mode 100644 index 3f35dedf67..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/BlockHandlerBase.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Web; -using System.Web.Mvc; -using System.Web.Mvc.Html; -using Newtonsoft.Json; -using SmartStore.Services; -using SmartStore.Services.Localization; -using SmartStore.Utilities; -using SmartStore.Web.Framework.Localization; - -namespace SmartStore.Web.Framework.UI.Blocks -{ - public abstract class BlockHandlerBase : IBlockHandler where T : IBlock - { - public ICommonServices Services { get; set; } - - public ILocalizedEntityService LocalizedEntityService { get; set; } - - public virtual T Create(IBlockEntity entity) - { - return Activator.CreateInstance(); - } - - public virtual T Load(IBlockEntity entity, StoryViewMode viewMode) - { - Guard.NotNull(entity, nameof(entity)); - - var block = Create(entity); - var json = entity.Model; - - if (json.IsEmpty()) - { - return block; - } - - JsonConvert.PopulateObject(json, block); - - return block; - } - - public virtual bool IsValid(T block) - { - return true; - } - - public virtual void Save(T block, IBlockEntity entity) - { - Guard.NotNull(entity, nameof(entity)); - - if (block == null) - { - return; - } - - var settings = new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.Objects, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - ObjectCreationHandling = ObjectCreationHandling.Replace, - NullValueHandling = NullValueHandling.Ignore - }; - - entity.Model = JsonConvert.SerializeObject(block, Formatting.None, settings); - } - - 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) - { - RenderByChildAction(element, templates, htmlHelper, textWriter); - } - - protected void RenderByChildAction(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 routeInfo = templates.Select(x => GetRoute(element, x)).FirstOrDefault(); - if (routeInfo == null) - { - throw new InvalidOperationException("The return value of the 'GetRoute()' method cannot be NULL."); - } - - //routeInfo.RouteValues["model"] = element.Block; - - var originalWriter = htmlHelper.ViewContext.Writer; - htmlHelper.ViewContext.Writer = textWriter; - - using (new ActionDisposable(() => htmlHelper.ViewContext.Writer = originalWriter)) - { - htmlHelper.RenderAction(routeInfo.Action, routeInfo.Controller, routeInfo.RouteValues); - } - } - - protected abstract RouteInfo GetRoute(IBlockContainer element, string template); - - /// - /// Add locales for localizable entities - /// - /// Localizable model - /// Language service - /// Locales - /// Configure action - protected virtual void AddLocales(IList locales, Action configure) where TLocalizedModelLocal : ILocalizedModelLocal - { - var languageService = Services.Resolve(); - - foreach (var language in languageService.GetAllLanguages(true)) - { - var locale = Activator.CreateInstance(); - locale.LanguageId = language.Id; - if (configure != null) - { - configure.Invoke(locale, locale.LanguageId); - } - locales.Add(locale); - } - } - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/BlockMetadata.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/BlockMetadata.cs deleted file mode 100644 index 952d7690be..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/BlockMetadata.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; - -namespace SmartStore.Web.Framework.UI.Blocks -{ - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class BlockAttribute : Attribute - { - public BlockAttribute(string systemName) - { - Guard.NotNull(systemName, nameof(systemName)); - - SystemName = systemName; - } - - public string SystemName { get; set; } - public string FriendlyName { get; set; } - public string Icon { get; set; } - public int DisplayOrder { get; set; } - public bool IsInternal { get; set; } - } - - public interface IBlockMetadata - { - string SystemName { get; } - string FriendlyName { get; } - string Icon { get; } - int DisplayOrder { get; } - bool IsInternal { get; } - bool IsInbuilt { get; } - Type BlockClrType { get; } - Type BlockHandlerClrType { get; } - } - - public class BlockMetadata : IBlockMetadata - { - public string SystemName { get; set; } - public string FriendlyName { 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; } - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlock.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlock.cs deleted file mode 100644 index fa013959c2..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlock.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Web.Routing; - -namespace SmartStore.Web.Framework.UI.Blocks -{ - public interface IBlock - { - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockContainer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockContainer.cs deleted file mode 100644 index 1cd523a213..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockContainer.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace SmartStore.Web.Framework.UI.Blocks -{ - public interface IBlockContainer - { - string BlockType { get; } - IBlock Block { get; } - IBlockMetadata Metadata { get; } - //IBlockHandler Handler { get; } - - string Title { get; } - bool IsInversed { get; } - - string HtmlId { get; } - string CssClass { get; } - string CssStyle { get; } - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockHandler.cs b/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockHandler.cs deleted file mode 100644 index c43a491e0e..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/Blocks/IBlockHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web; -using System.Web.Mvc; - -namespace SmartStore.Web.Framework.UI.Blocks -{ - public enum StoryViewMode - { - Public, - Preview, - GridEdit, - Edit - } - - public interface IBlockHandler - { - void Render(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHeper); - IHtmlString ToHtmlString(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper); - } - - public interface IBlockHandler : IBlockHandler where T : IBlock - { - T Create(IBlockEntity entity); - - T Load(IBlockEntity entity, StoryViewMode viewMode); - - bool IsValid(T block); - - void Save(T block, IBlockEntity entity); - - string Clone(IBlockEntity sourceEntity, IBlockEntity clonedEntity); - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Breadcrumb.cs b/src/Presentation/SmartStore.Web.Framework/UI/Breadcrumb.cs index be8e5eee0d..43b71c4ea0 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Breadcrumb.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Breadcrumb.cs @@ -39,7 +39,5 @@ public IReadOnlyList Trail return _trail; } } - - } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs index 241a3f953e..744553f48d 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs @@ -11,6 +11,7 @@ using SmartStore.Services.Seo; using SmartStore.Web.Framework.Theming.Assets; using SmartStore.Core.Themes; +using SmartStore.Utilities.Threading; namespace SmartStore.Web.Framework.UI { @@ -31,13 +32,11 @@ public class BundleBuilder : IBundleBuilder private readonly IWorkContext _workContext; private readonly IThemeContext _themeContext; - private static readonly object s_lock = new object(); - public BundleBuilder(IStoreContext storeContext, IWorkContext workContext, IThemeContext themeContext) { - this._storeContext = storeContext; - this._workContext = workContext; - this._themeContext = themeContext; + _storeContext = storeContext; + _workContext = workContext; + _themeContext = themeContext; } public string Build(BundleType type, IEnumerable files) @@ -45,11 +44,11 @@ public string Build(BundleType type, IEnumerable files) if (files == null || !files.Any()) return string.Empty; - string bundleVirtualPath = this.GetBundleVirtualPath(type, files); + var bundleVirtualPath = this.GetBundleVirtualPath(type, files); var bundleFor = BundleTable.Bundles.GetBundleFor(bundleVirtualPath); if (bundleFor == null) { - lock (s_lock) + using (KeyedLock.Lock("BundleBuilder.Build." + bundleVirtualPath)) { bundleFor = BundleTable.Bundles.GetBundleFor(bundleVirtualPath); if (bundleFor == null) @@ -101,7 +100,7 @@ protected virtual string GetBundleVirtualPath(BundleType type, IEnumerable x)) + foreach (var file in files) { hashInput += file; hashInput += ","; diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs index 3746291113..bb034c7061 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs @@ -95,13 +95,12 @@ public TBuilder WithRenderer(Type rendererType) Guard.NotNull(rendererType, nameof(rendererType)); Guard.Implements>(rendererType); - var renderer = Activator.CreateInstance(rendererType) as ComponentRenderer; - if (renderer != null) - { - 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) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs index 43b2d6dcfc..455b58f2f3 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs @@ -1,10 +1,10 @@ using System; -using System.Web.Mvc; using System.ComponentModel; +using System.Web.Mvc; using SmartStore.Core; namespace SmartStore.Web.Framework.UI -{ +{ public class ComponentFactory : IHideObjectMembers { public ComponentFactory(HtmlHelper helper) @@ -56,7 +56,7 @@ public virtual PagerBuilder Pager(int pageCount) public virtual PagerBuilder Pager(IPageable model) { - Guard.NotNull(model, "model"); + Guard.NotNull(model, nameof(model)); return new PagerBuilder(new Pager(), this.HtmlHelper).Model(model); } @@ -70,6 +70,11 @@ 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 } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs index 1bef5d85ee..36d21bec1a 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs @@ -51,43 +51,40 @@ public virtual void VerifyState() } } - protected void WriteHtml(HtmlTextWriter writer) - { - this.VerifyState(); - this.Component.Id = SanitizeId(this.Component.Id); - - this.WriteHtmlCore(writer); - } - - protected virtual void WriteHtmlCore(HtmlTextWriter writer) - { - throw new NotImplementedException(); - } + protected string SanitizeId(string id) + { + return id.SanitizeHtmlId(); + } - public virtual void Render() + public virtual void Render() { - var output = ((System.Web.Mvc.WebViewPage)this.HtmlHelper.ViewDataContainer).Output; - var str = ToHtmlString(); - output.Write(str); + WriteHtml(ViewContext.Writer); } public virtual string ToHtmlString() { using (var stringWriter = new StringWriter()) { - using (var htmlWriter = new HtmlTextWriter(stringWriter)) - { - this.WriteHtml(htmlWriter); - 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 string SanitizeId(string id) - { - return id.SanitizeHtmlId(); - } + using (var htmlWriter = new HtmlTextWriter(writer)) + { + WriteHtmlCore(htmlWriter); + } + } + + protected virtual void WriteHtmlCore(HtmlTextWriter writer) + { + throw new NotImplementedException(); + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs index b9382d6e5b..2d8e269b44 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs @@ -41,8 +41,16 @@ private static void RenderAttrs(HtmlHelper html, IDictionary att } var first = true; - foreach (var pair in attrs.Where(x => x.Value != null)) + + foreach (var key in attrs.Keys) { + var value = attrs[key]; + + if (value == null) + { + continue; + } + if (!first) { output.Write(" "); @@ -50,8 +58,8 @@ private static void RenderAttrs(HtmlHelper html, IDictionary att first = false; - output.Write(pair.Key + "=\""); - HttpUtility.HtmlAttributeEncode(pair.Value.ToString(), output); + output.Write(key + "=\""); + HttpUtility.HtmlAttributeEncode(value.ToString(), output); output.Write("\""); } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/Menu.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/Menu.cs new file mode 100644 index 0000000000..f17eae41e2 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/Menu.cs @@ -0,0 +1,11 @@ +namespace SmartStore.Web.Framework.UI +{ + public class Menu : Component + { + public override bool NameIsRequired => true; + + public string Template { 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 new file mode 100644 index 0000000000..d3b2e75f5e --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuBuilder.cs @@ -0,0 +1,26 @@ +using System.Web.Mvc; + +namespace SmartStore.Web.Framework.UI +{ + public class MenuBuilder : ComponentBuilder, TModel> + { + public MenuBuilder(Menu component, HtmlHelper htmlHelper) + : base(component, htmlHelper) + { + } + + public MenuBuilder Template(string 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; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuCreatedEvent.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuCreatedEvent.cs deleted file mode 100644 index d9ba06c2a3..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuCreatedEvent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using SmartStore.Collections; - -namespace SmartStore.Web.Framework.UI -{ - public class MenuCreatedEvent - { - public MenuCreatedEvent(string menuName, TreeNode root, string selectedItemToken) - { - Guard.NotEmpty(menuName, nameof(menuName)); - Guard.NotNull(root, nameof(root)); - - MenuName = menuName; - Root = root; - SelectedItemToken = selectedItemToken; - } - - public string MenuName { get; private set; } - public TreeNode Root { get; private set; } - public string SelectedItemToken { get; set; } - } -} 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 7ddb4c4eac..6284b9b052 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs @@ -1,20 +1,42 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Web.Mvc; +using System.Web.Routing; +using Newtonsoft.Json; using SmartStore.Collections; +using SmartStore.Core.Domain.Cms; +using SmartStore.Core.Localization; +using SmartStore.Services.Localization; namespace SmartStore.Web.Framework.UI -{ - public static class MenuExtensions +{ + public static class MenuExtensions { public static IEnumerable> GetBreadcrumb(this TreeNode node) { - var breadcrumb = node.Trail.Where(x => !x.IsRoot); - return breadcrumb; + Guard.NotNull(node, nameof(node)); + + return node.Trail.Where(x => !x.IsRoot); } + public static string GetItemText(this TreeNode node, Localizer localizer) + { + string result = null; + + if (node.Value.ResKey.HasValue()) + { + result = localizer(node.Value.ResKey).Text; + } + + if (!result.HasValue() || result.IsCaseInsensitiveEqual(node.Value.ResKey)) + { + result = node.Value.Text; + } + + return result; + } + /// /// Gets the state of node within the passed currentPath, which is the navigation breadcrumb. /// @@ -71,5 +93,124 @@ public static NodePathState GetNodePathState(this TreeNode node, IList return state; } - } + + /// + /// Applies serialized route informations to a tree node. + /// + /// Tree node. + /// JSON serialized route data. + public static void ApplyRouteData(this TreeNode node, string data) + { + if (data.HasValue()) + { + var routeValues = JsonConvert.DeserializeObject(data); + var routeName = string.Empty; + + if (routeValues.TryGetValue("routename", out var val)) + { + routeName = val as string; + routeValues.Remove("routename"); + } + + if (routeName.HasValue()) + { + node.Value.Route(routeName, routeValues); + } + else + { + node.Value.Action(routeValues); + } + } + } + + /// + /// Creates a menu model. + /// + /// Menu. + /// Controller context to resolve current node. Can be null. + /// Menu model. + public static MenuModel CreateModel(this IMenu menu, string template, ControllerContext context) + { + Guard.NotNull(menu, nameof(menu)); + + var model = new MenuModel + { + Name = menu.Name, + Template = template ?? menu.Name, + Root = menu.Root + }; + + var currentNode = menu.ResolveCurrentNode(context); + + model.Path = currentNode != null + ? currentNode.Trail.Where(x => !x.IsRoot).ToList() + : new List>(); + + menu.ResolveElementCounts(model.SelectedNode, false); + + return model; + } + + /// + /// Converts a list of menu items into a tree. + /// + /// Origin of the tree. + /// List of menu items. + /// Menu item providers. + /// Tree of menu items. + public static TreeNode GetTree( + this IEnumerable items, + string origin, + IDictionary> itemProviders) + { + Guard.NotNull(items, nameof(items)); + Guard.NotNull(itemProviders, nameof(itemProviders)); + + if (!items.Any()) + { + return new TreeNode(new MenuItem()); + } + + // Prepare root node. It represents the MenuRecord. + var menu = items.First().Menu; + var rootItem = new MenuItem + { + Text = menu.GetLocalized(x => x.Title), + EntityId = 0 + }; + var root = new TreeNode(rootItem) + { + Id = menu.SystemName + }; + + AddChildItems(root, 0); + + return root; + + void AddChildItems(TreeNode parentNode, int parentItemId) + { + if (parentNode == null) + { + return; + } + + var entities = items.Where(x => x.ParentItemId == parentItemId).OrderBy(x => x.DisplayOrder); + + foreach (var entity in entities) + { + if (entity.ProviderName.HasValue() && itemProviders.TryGetValue(entity.ProviderName, out var provider)) + { + var newNode = provider.Value.Append(new MenuItemProviderRequest + { + Origin = origin, + Parent = parentNode, + Entity = entity + }); + + AddChildItems(newNode, entity.Id); + } + } + } + } + } } 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 09786612db..bccc320ead 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using SmartStore.Utilities; namespace SmartStore.Web.Framework.UI { public class MenuItem : NavigationItem, ICloneable { + private string _id; + public MenuItem() { this.Attributes = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -14,13 +17,34 @@ public MenuItem() /// 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; } - /// - /// The total count of contained elements (like the count of products within a category) - /// - public int? ElementsCount { get; set; } + public int MenuItemId { get; set; } + + /// + /// The total count of contained elements (like the count of products within a category) + /// + public int? ElementsCount { get; set; } - public string Id { get; set; } + /// + /// Unique identifier. + /// + public string Id + { + get + { + if (_id == null) + { + _id = CommonHelper.GenerateRandomDigitCode(10); + } + + return _id; + } + set + { + _id = value; + } + } public string ResKey { get; set; } 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 db03c5b813..785aa05415 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItemBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItemBuilder.cs @@ -1,12 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using SmartStore.Utilities; namespace SmartStore.Web.Framework.UI { - public class MenuItemBuilder : NavigationItemBuilder { private readonly IList _permissionNames; diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/NavigationModel.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuModel.cs similarity index 77% rename from src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/NavigationModel.cs rename to src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuModel.cs index 6941f23b5a..b68a8dd6a9 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/NavigationModel.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuModel.cs @@ -1,15 +1,17 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using SmartStore.Collections; namespace SmartStore.Web.Framework.UI { - public class NavigationModel + public class MenuModel { private TreeNode _selectedNode; private bool _seekedSelectedNode; + public string Name { get; set; } + public string Template { get; set; } + public TreeNode Root { get; set; } public IList> Path { get; set; } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuRenderer.cs new file mode 100644 index 0000000000..7490164935 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuRenderer.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +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; + + if (route != null) + { + HtmlHelper.RenderAction(route.Action, route.Controller, route.RouteValues); + } + else + { + HtmlHelper.RenderAction("Menu", "Common", new + { + area = "", + name = Component.Name, + template = Component.Template + }); + } + } + + 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", "Common", new { area = "", name = c.Name, template = c.Template }); + } + } + + 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", "Common", new { area = "", name = c.Name, template = c.Template }).ToHtmlString(); + } + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponent.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponent.cs new file mode 100644 index 0000000000..e3982af7fe --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponent.cs @@ -0,0 +1,84 @@ +using System; +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; + + protected NavigatableComponent() + { + this.RouteValues = new RouteValueDictionary(); + } + + public string ActionName + { + get + { + return _actionName; + } + set + { + _actionName = value; + _routeName = (string)(_url = null); + } + } + + public string ControllerName + { + get + { + return _controllerName; + } + set + { + _controllerName = value; + _routeName = (string)(_url = null); + } + } + + public string RouteName + { + get + { + return _routeName; + } + set + { + _routeName = value; + _controllerName = _actionName = (string)(_url = null); + } + } + + public RouteValueDictionary RouteValues + { + get; + set; + } + + public string Url + { + get + { + return _url; + } + set + { + _url = value; + _routeName = _controllerName = (string)(_actionName = null); + this.RouteValues.Clear(); + + } + } + + public ModifiedParameter ModifiedParam + { + get; + protected set; + } + } +} 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 dfaa5d4603..d023c08cb5 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs @@ -30,15 +30,10 @@ public enum PagerStyle Blog } - public class Pager : Component, INavigatable + public class Pager : NavigatableComponent { private ILocalizationService _localizationService; - private string _actionName; - private string _controllerName; - private string _routeName; - private string _url; - private string _firstButtonText; private string _lastButtonText; private string _nextButtonText; @@ -51,6 +46,7 @@ public Pager() } public Pager(ILocalizationService localizationService) + : base() { _localizationService = localizationService; @@ -63,7 +59,6 @@ public Pager(ILocalizationService localizationService) this.MaxPagesToDisplay = 8; this.Alignment = PagerAlignment.Centered; this.Size = PagerSize.Medium; - this.RouteValues = new RouteValueDictionary(); this.ModifiedParam = new ModifiedParameter("i"); } @@ -166,72 +161,5 @@ public string CurrentPageText _currentPageText = value; } } - - - public string ActionName - { - get - { - return _actionName; - } - set - { - _actionName = value; - _routeName = (string)(_url = null); - } - } - - public string ControllerName - { - get - { - return _controllerName; - } - set - { - _controllerName = value; - _routeName = (string)(_url = null); - } - } - - public string RouteName - { - get - { - return _routeName; - } - set - { - _routeName = value; - _controllerName = _actionName = (string)(_url = null); - } - } - - public RouteValueDictionary RouteValues - { - get; - set; - } - - public string Url - { - get - { - return _url; - } - set - { - _url = value; - _routeName = _controllerName = (string)(_actionName = null); - this.RouteValues.Clear(); - - } - } - - public ModifiedParameter ModifiedParam - { - get; - private set; - } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ViewBasedComponentRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ViewBasedComponentRenderer.cs index 686aecfe04..0ef84c00a0 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ViewBasedComponentRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ViewBasedComponentRenderer.cs @@ -1,9 +1,8 @@ -using System; -using System.Web.Mvc.Html; +using System.Web.Mvc.Html; namespace SmartStore.Web.Framework.UI { - public class ViewBasedComponentRenderer : ComponentRenderer where TComponent : Component + public class ViewBasedComponentRenderer : ComponentRenderer where TComponent : Component { private readonly string _viewName; @@ -12,21 +11,26 @@ public ViewBasedComponentRenderer() { } - public ViewBasedComponentRenderer(string viewName) + public ViewBasedComponentRenderer(string viewName) { Guard.NotEmpty(viewName, nameof(viewName)); _viewName = viewName; } - public override void Render() + protected virtual string GetViewName() + { + return "Components/" + _viewName; + } + + public override void Render() { - HtmlHelper.RenderPartial("Components/" + _viewName, base.Component); + HtmlHelper.RenderPartial(GetViewName(), Component); } public override string ToHtmlString() { - return HtmlHelper.Partial("Components/" + _viewName, base.Component).ToString(); + return HtmlHelper.Partial(GetViewName(), Component).ToString(); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs index d8e2cfa3fc..e6b1186294 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs @@ -13,13 +13,13 @@ public static class LayoutExtensions public static void AddTitleParts(this HtmlHelper html, params string[] parts) { - var pageAssetsBuilder = EngineContext.Current.Resolve(); + var pageAssetsBuilder = EngineContext.Current.Resolve(); pageAssetsBuilder.AddTitleParts(parts); } public static void AppendTitleParts(this HtmlHelper html, params string[] parts) { - var pageAssetsBuilder = EngineContext.Current.Resolve(); + var pageAssetsBuilder = EngineContext.Current.Resolve(); pageAssetsBuilder.AppendTitleParts(parts); } @@ -27,7 +27,7 @@ public static MvcHtmlString SmartTitle(this HtmlHelper html, bool addDefaultTitl { var pageAssetsBuilder = EngineContext.Current.Resolve(); html.AppendTitleParts(parts); - return MvcHtmlString.Create(html.Encode(pageAssetsBuilder.GenerateTitle(addDefaultTitle))); + return MvcHtmlString.Create(pageAssetsBuilder.GenerateTitle(addDefaultTitle)); } #endregion @@ -50,7 +50,7 @@ public static MvcHtmlString SmartMetaDescription(this HtmlHelper html, params st { var pageAssetsBuilder = EngineContext.Current.Resolve(); html.AppendMetaDescriptionParts(parts); - return MvcHtmlString.Create(html.Encode(pageAssetsBuilder.GenerateMetaDescription())); + return MvcHtmlString.Create(pageAssetsBuilder.GenerateMetaDescription()); } #endregion @@ -73,7 +73,7 @@ public static MvcHtmlString SmartMetaKeywords(this HtmlHelper html, params strin { var pageAssetsBuilder = EngineContext.Current.Resolve(); html.AppendMetaKeywordParts(parts); - return MvcHtmlString.Create(html.Encode(pageAssetsBuilder.GenerateMetaKeywords())); + return MvcHtmlString.Create(pageAssetsBuilder.GenerateMetaKeywords()); } #endregion @@ -309,7 +309,7 @@ public static void AddBodyCssClass(this HtmlHelper html, string cssClassName) public static MvcHtmlString BodyCssClass(this HtmlHelper html) { var pageAssetsBuilder = EngineContext.Current.Resolve(); - return MvcHtmlString.Create(html.Encode(pageAssetsBuilder.GenerateBodyCssClasses())); + return MvcHtmlString.Create(pageAssetsBuilder.GenerateBodyCssClasses()); } public static void SetHtmlId(this HtmlHelper html, string htmlId) @@ -321,7 +321,7 @@ public static void SetHtmlId(this HtmlHelper html, string htmlId) public static MvcHtmlString HtmlId(this HtmlHelper html) { var pageAssetsBuilder = EngineContext.Current.Resolve(); - var result = html.Encode(pageAssetsBuilder.GenerateHtmlId()); + var result = pageAssetsBuilder.GenerateHtmlId(); try { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Icons/IconDescription.cs b/src/Presentation/SmartStore.Web.Framework/UI/Icons/IconDescription.cs index 01f95a80f1..739c940afb 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Icons/IconDescription.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Icons/IconDescription.cs @@ -1,10 +1,11 @@ using System; using System.Linq; using Newtonsoft.Json; +using SmartStore.Utilities; namespace SmartStore.Web.Framework.UI { - public class IconDescription + public class IconDescription : IEquatable { [JsonIgnore] public string Name { get; set; } @@ -72,6 +73,24 @@ public string GetCssClass(string style) return string.Concat(prefix, " fa-", Name); } + public override bool Equals(object other) + { + return this.Equals(other as IconDescription); + } + + public bool Equals(IconDescription other) + { + if (other == null) + return false; + + return this.Name == other.Name; + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start().Add(typeof(IconDescription)).Add(this.Name).CombinedHash; + } + public class Search { [JsonProperty("terms")] diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Icons/IconExplorer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Icons/IconExplorer.cs index fcda23274b..b12bf0c581 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Icons/IconExplorer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Icons/IconExplorer.cs @@ -16,7 +16,7 @@ public interface IIconExplorer ICollection All { get; } Multimap SearchMap { get; } IconDescription GetIconByName(string name); - IEnumerable FindIcons(string searchTerm); + IEnumerable FindIcons(string searchTerm, bool relaxed = false); } public class IconExplorer : IIconExplorer @@ -54,7 +54,7 @@ private void EnsureIsLoaded() catch (Exception ex) { Logger.Error(ex); - _icons = new Dictionary(); + _icons = new Dictionary(StringComparer.OrdinalIgnoreCase); } foreach (var kvp in _icons) @@ -157,19 +157,39 @@ public IconDescription GetIconByName(string name) return description; } - public IEnumerable FindIcons(string searchTerm) + public IEnumerable FindIcons(string searchTerm, bool relaxed = false) { Guard.NotEmpty(searchTerm, nameof(searchTerm)); EnsureIsLoaded(); + var hasExactMatch = false; + + if (_icons.TryGetValue(searchTerm, out var description)) + { + hasExactMatch = true; + yield return description; + } + if (_searchMap.ContainsKey(searchTerm)) { var names = _searchMap[searchTerm]; foreach (var name in names) { - if (_icons.TryGetValue(name, out var description)) + if (_icons.TryGetValue(name, out var description2)) + { + hasExactMatch = true; + yield return description2; + } + } + } + + if (relaxed && !hasExactMatch) + { + foreach (var kvp in _icons) + { + if (kvp.Key.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) > -1) { - yield return description; + yield return kvp.Value; } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/DatabaseMenu.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/DatabaseMenu.cs new file mode 100644 index 0000000000..3722376973 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/DatabaseMenu.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using SmartStore.Collections; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Cms; +using SmartStore.Core.Logging; +using SmartStore.Core.Search; +using SmartStore.Services; +using SmartStore.Services.Catalog; +using SmartStore.Services.Cms; +using SmartStore.Services.Search; + +namespace SmartStore.Web.Framework.UI +{ + /// + /// A generic implementation of which represents a + /// entity retrieved by . + /// + internal class DatabaseMenu : MenuBase + { + private static object s_lock = new object(); + + private readonly Lazy _catalogSearchService; + private readonly Lazy _categoryService; + private readonly IMenuStorage _menuStorage; + private readonly ILogger _logger; + private readonly HttpContextBase _httpContext; + private readonly DbQuerySettings _querySettings; + private readonly Lazy _catalogSettings; + private readonly Lazy _searchSettings; + private readonly IDictionary> _menuItemProviders; + + public DatabaseMenu( + string menuName, + ICommonServices services, + Lazy catalogSearchService, + Lazy categoryService, + IMenuStorage menuStorage, + ILogger logger, + HttpContextBase httpContext, + IMenuPublisher menuPublisher, + DbQuerySettings querySettings, + Lazy catalogSettings, + Lazy searchSettings, + IEnumerable> menuItemProviders) + { + Guard.NotEmpty(menuName, nameof(menuName)); + + Name = menuName; + + Services = services; + _catalogSearchService = catalogSearchService; + _categoryService = categoryService; + _menuStorage = menuStorage; + _logger = logger; + _httpContext = httpContext; + MenuPublisher = menuPublisher; + _querySettings = querySettings; + _catalogSettings = catalogSettings; + _searchSettings = searchSettings; + _menuItemProviders = menuItemProviders.ToDictionarySafe(x => x.Metadata.ProviderName, x => x); + } + + public override string Name { get; } + + public override bool ApplyPermissions => true; + + public override void ResolveElementCounts(TreeNode curNode, bool deep = false) + { + if (curNode == null || !ContainsProvider("catalog") || !_catalogSettings.Value.ShowCategoryProductNumber) + { + return; + } + + try + { + using (Services.Chronometer.Step($"DatabaseMenu.ResolveElementsCount() for {curNode.Value.Text.NaIfEmpty()}")) + { + // Perf: only resolve counts for categories in the current path. + while (curNode != null) + { + if (curNode.Children.Any(x => !x.Value.ElementsCount.HasValue)) + { + lock (s_lock) + { + if (curNode.Children.Any(x => !x.Value.ElementsCount.HasValue)) + { + var nodes = deep ? curNode.SelectNodes(x => true, false) : curNode.Children.AsEnumerable(); + nodes = nodes.Where(x => x.Value.EntityId != 0); + + foreach (var node in nodes) + { + var categoryIds = new HashSet(); + + if (_catalogSettings.Value.ShowCategoryProductNumberIncludingSubcategories) + { + // Include subcategories. + node.Traverse(x => + { + categoryIds.Add(x.Value.EntityId); + }, true); + } + else + { + categoryIds.Add(node.Value.EntityId); + } + + var context = new CatalogSearchQuery() + .VisibleOnly() + .VisibleIndividuallyOnly(true) + .WithCategoryIds(null, categoryIds.ToArray()) + .HasStoreId(Services.StoreContext.CurrentStoreIdIfMultiStoreMode) + .BuildFacetMap(false) + .BuildHits(false); + + if (!_searchSettings.Value.IncludeNotAvailable) + { + context = context.AvailableOnly(true); + } + + node.Value.ElementsCount = _catalogSearchService.Value.Search(context).TotalHitsCount; + } + } + } + } + + curNode = curNode.Parent; + } + } + } + catch (Exception ex) + { + _logger.Error(ex); + } + } + + public override TreeNode ResolveCurrentNode(ControllerContext context) + { + if (context == null || !ContainsProvider("catalog")) + { + return base.ResolveCurrentNode(context); + } + + TreeNode currentNode = null; + + try + { + var rootContext = context.GetRootControllerContext(); + + int currentCategoryId = GetRequestValue(rootContext, "currentCategoryId") ?? GetRequestValue(rootContext, "categoryId"); + int currentProductId = 0; + + if (currentCategoryId == 0) + { + currentProductId = GetRequestValue(rootContext, "currentProductId") ?? GetRequestValue(rootContext, "productId"); + } + + if (currentCategoryId == 0 && currentProductId == 0) + { + // Possibly not a category node of a menu where the category tree is attached to. + return base.ResolveCurrentNode(rootContext); + } + + var cacheKey = $"sm.temp.category.breadcrumb.{currentCategoryId}-{currentProductId}"; + currentNode = Services.RequestCache.Get(cacheKey, () => + { + var root = Root; + TreeNode node = null; + + if (currentCategoryId > 0) + { + node = root.SelectNodeById(currentCategoryId) ?? root.SelectNode(x => x.Value.EntityId == currentCategoryId); + } + + if (node == null && currentProductId > 0) + { + var productCategories = _categoryService.Value.GetProductCategoriesByProductId(currentProductId); + if (productCategories.Any()) + { + currentCategoryId = productCategories[0].Category.Id; + node = root.SelectNodeById(currentCategoryId) ?? root.SelectNode(x => x.Value.EntityId == currentCategoryId); + } + } + + return node; + }); + } + catch (Exception ex) + { + _logger.Error(ex); + } + + return currentNode; + } + + protected override TreeNode Build() + { + var entities = _menuStorage.GetMenuItems(Name, Services.StoreContext.CurrentStore.Id); + var tree = entities.GetTree("DatabaseMenu", _menuItemProviders); + + return tree; + } + + protected override string GetCacheKey() + { + var cacheKey = "{0}-{1}-{2}".FormatInvariant( + Services.WorkContext.WorkingLanguage.Id, + _querySettings.IgnoreMultiStore ? 0 : Services.StoreContext.CurrentStore.Id, + _querySettings.IgnoreAcl ? "0" : Services.WorkContext.CurrentCustomer.GetRolesIdent()); + + return cacheKey; + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/DatabaseMenuResolver.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/DatabaseMenuResolver.cs new file mode 100644 index 0000000000..d28d3ed30f --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/DatabaseMenuResolver.cs @@ -0,0 +1,33 @@ +using Autofac; +using SmartStore.Services; +using SmartStore.Services.Cms; + +namespace SmartStore.Web.Framework.UI +{ + public class DatabaseMenuResolver : IMenuResolver + { + protected readonly IComponentContext _ctx; + protected readonly IMenuStorage _menuStorage; + + public DatabaseMenuResolver( + IComponentContext ctx, + IMenuStorage menuStorage) + { + _ctx = ctx; + _menuStorage = menuStorage; + } + + public int Order => 1; + + public bool Exists(string menuName) + { + return _menuStorage.MenuExists(menuName); + } + + public IMenu Resolve(string name) + { + var menu = _ctx.ResolveNamed("database", new NamedParameter("menuName", name)); + return menu; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/DependencyMenuResolver.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/DependencyMenuResolver.cs new file mode 100644 index 0000000000..a2e0d917c9 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/DependencyMenuResolver.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace SmartStore.Web.Framework.UI +{ + public class DependencyMenuResolver : IMenuResolver + { + private readonly IDictionary _menus; + + public DependencyMenuResolver(IEnumerable menus) + { + _menus = menus.ToDictionarySafe(x => x.Name, x => x); + } + + public int Order => 0; + + public bool Exists(string menuName) + { + Guard.NotEmpty(menuName, nameof(menuName)); + + return _menus.ContainsKey(menuName); + } + + public IMenu Resolve(string menuName) + { + _menus.TryGetValue(menuName, out var menu); + return menu; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/IMenu.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/IMenu.cs new file mode 100644 index 0000000000..9315e1bf5f --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/IMenu.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using SmartStore.Collections; + +namespace SmartStore.Web.Framework.UI +{ + /// + /// A hierarchical navigation menu + /// + public interface IMenu + { + /// + /// Gets the menu system name (e.g. main, footer, service, admin etc.) + /// + string Name { get; } + + /// + /// Gets the root node of the menu + /// + TreeNode Root { get; } + + /// + /// Whether menu items should be hidden based on permission names + /// + bool ApplyPermissions { get; } + + /// + /// Resolves the contained elements count (e.g. the products count on a category page). + /// + /// The current node + /// false resolves counts for direct children of only, true traverses the whole sub-tree + void ResolveElementCounts(TreeNode curNode, bool deep = false); + + /// + /// Resolves the current node. + /// + /// Controller context. + /// The current menu item node. + TreeNode ResolveCurrentNode(ControllerContext context); + + /// + /// Gets all cached trees from the underlying cache storage + /// + /// A dictionary of trees (Key: cache key, Value: tree instance) + /// + /// Multiple trees are created per menu depending + /// on language, customer-(roles), store and other parameters. + /// This method does not create anything, but returns all + /// previously processed and cached menu variations. + /// + IDictionary> GetAllCachedMenus(); + + /// + /// Removes the menu from the application cache + /// + void ClearCache(); + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/IMenuResolver.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/IMenuResolver.cs new file mode 100644 index 0000000000..ff436eccbe --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/IMenuResolver.cs @@ -0,0 +1,10 @@ +namespace SmartStore.Web.Framework.UI +{ + public interface IMenuResolver + { + int Order { get; } + + bool Exists(string menuName); + IMenu Resolve(string menuName); + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/IMenuService.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/IMenuService.cs new file mode 100644 index 0000000000..067fb58b3a --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/IMenuService.cs @@ -0,0 +1,38 @@ +using SmartStore.Collections; + +namespace SmartStore.Web.Framework.UI +{ + /// + /// Main service for menus + /// + public partial interface IMenuService + { + /// + /// Gets a menu instance + /// + /// Name of a code-based or a persisted user menu. + /// Menu instance + IMenu GetMenu(string name); + + /// + /// Gets the root node of a menu + /// + /// Name of a code-based or a persisted user menu. + /// The root menu item node. + TreeNode GetRootNode(string menuName); + + /// + /// Resolves all element counts for a tree subset, e.g. resolves the number of products in categories. + /// + /// Name of a code-based or a persisted user menu. + /// The node to begin resolution. + /// true: process ALL children of , false: process only direct children of . + void ResolveElementCounts(string menuName, TreeNode curNode, bool deep = false); + + /// + /// Removes all cached menu variations for + /// + /// Name of a code-based or a persisted user menu. + void ClearCache(string menuName); + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuBase.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuBase.cs new file mode 100644 index 0000000000..0ff44e23bb --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuBase.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using SmartStore.Collections; +using SmartStore.Core.Logging; +using SmartStore.Services; + +namespace SmartStore.Web.Framework.UI +{ + public abstract class MenuBase : IMenu + { + /// + /// Key for Menu caching + /// + /// + /// {0} : Menu name + /// {1} : Menu specific key suffix + /// + internal const string MENU_KEY = "pres:menu:{0}-{1}"; + internal const string MENU_PATTERN_KEY = "pres:menu:{0}*"; + + private TreeNode _currentNode; + private bool _currentNodeResolved; + private List _providers; + + public abstract string Name { get; } + + public virtual bool ApplyPermissions => true; + + public TreeNode Root + { + get + { + var cacheKey = MENU_KEY.FormatInvariant(Name, GetCacheKey()); + var rootNode = Services.Cache.Get(cacheKey, () => + { + using (Services.Chronometer.Step($"Build menu '{Name}'")) + { + var root = Build(); + + MenuPublisher.RegisterMenus(root, Name); + + if (ApplyPermissions) + { + DoApplyPermissions(root); + } + + Services.EventPublisher.Publish(new MenuBuiltEvent(Name, root)); + + return root; + } + }); + + return rootNode; + } + } + + protected virtual void DoApplyPermissions(TreeNode root) + { + // Hide based on permissions + root.Traverse(x => + { + if (!MenuItemAccessPermitted(x.Value)) + { + x.Value.Visible = false; + } + }); + + // Hide dropdown nodes when no child is visible + root.Traverse(x => + { + var item = x.Value; + if (!item.IsGroupHeader && !item.HasRoute()) + { + if (!x.Children.Any(child => child.Value.Visible)) + { + item.Visible = false; + } + } + }); + } + + protected abstract string GetCacheKey(); + + protected abstract TreeNode Build(); + + public virtual void ResolveElementCounts(TreeNode curNode, bool deep = false) + { + } + + public virtual TreeNode ResolveCurrentNode(ControllerContext context) + { + if (!_currentNodeResolved) + { + _currentNode = Root.SelectNode(x => x.Value.IsCurrent(context), true); + _currentNodeResolved = true; + } + + return _currentNode; + } + + public IDictionary> GetAllCachedMenus() + { + var cache = Services.Cache; + var keys = cache.Keys(MENU_PATTERN_KEY.FormatInvariant(this.Name)); + + var trees = new Dictionary>(keys.Count()); + + foreach (var key in keys) + { + var tree = cache.Get>(key); + if (tree != null) + { + trees[key] = tree; + } + } + + return trees; + } + + public void ClearCache() + { + Services.Cache.RemoveByPattern(MENU_PATTERN_KEY.FormatInvariant(this.Name)); + } + + #region Dependencies + + public ICommonServices Services { get; set; } + + public IMenuPublisher MenuPublisher { get; set; } + + #endregion + + #region Utilities + + protected virtual bool ContainsProvider(string provider) + { + Guard.NotEmpty(provider, nameof(provider)); + + if (_providers == null) + { + _providers = Root.GetMetadata>("Providers") ?? new List(); + } + + return _providers.Contains(provider); + } + + protected virtual T GetRequestValue(ControllerContext context, string name) + { + Guard.NotNull(context, nameof(context)); + Guard.NotEmpty(name, nameof(name)); + + var value = context.RouteData?.Values[name]?.ToString(); + if (value.IsEmpty()) + { + value = context.HttpContext?.Request?.Form?.GetValues(name)?.FirstOrDefault(); + if (value.IsEmpty()) + { + context.HttpContext?.Request?.QueryString?.GetValues(name)?.FirstOrDefault(); + } + } + + return value.Convert(); + } + + private bool MenuItemAccessPermitted(MenuItem item) + { + var result = true; + + if (item.PermissionNames.HasValue()) + { + var permitted = item.PermissionNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Any(x => Services.Permissions.Authorize(x.Trim())); + if (!permitted) + { + result = false; + } + } + + return result; + } + + #endregion + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBuiltEvent.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuBuiltEvent.cs similarity index 56% rename from src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBuiltEvent.cs rename to src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuBuiltEvent.cs index b678ac1456..ff4ab34139 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBuiltEvent.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuBuiltEvent.cs @@ -1,11 +1,10 @@ -using System; -using SmartStore.Collections; +using SmartStore.Collections; namespace SmartStore.Web.Framework.UI { - public class SiteMapBuiltEvent + public class MenuBuiltEvent { - public SiteMapBuiltEvent(string name, TreeNode root) + public MenuBuiltEvent(string name, TreeNode root) { Name = name; Root = root; diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuInvalidator.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuInvalidator.cs new file mode 100644 index 0000000000..f7b0d12296 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuInvalidator.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core; +using SmartStore.Core.Data.Hooks; +using SmartStore.Core.Domain.Cms; +using SmartStore.Services.Cms; + +namespace SmartStore.Web.Framework.UI +{ + public partial class MenuInvalidator : DbSaveHook + { + private readonly Lazy _menuService; + private readonly Lazy _menuStorage; + + public MenuInvalidator(Lazy menuService, Lazy menuStorage) + { + _menuService = menuService; + _menuStorage = menuStorage; + } + + protected override void OnDeleted(BaseEntity entity, IHookedEntity entry) + { + HookObject(entity, entry); + } + + protected override void OnInserted(BaseEntity entity, IHookedEntity entry) + { + HookObject(entity, entry); + } + + protected override void OnUpdated(BaseEntity entity, IHookedEntity entry) + { + HookObject(entity, entry); + } + + private void HookObject(BaseEntity entity, IHookedEntity entry) + { + if (entry.Entity is MenuRecord mr) + { + _menuService.Value.ClearCache(mr.SystemName); + } + else if (entry.Entity is MenuItemRecord mir) + { + var menu = mir.Menu ?? _menuStorage.Value.GetMenuById(mir.MenuId); + if (menu != null) + { + _menuService.Value.ClearCache(menu.SystemName); + } + } + else + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuService.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuService.cs new file mode 100644 index 0000000000..c02ea08601 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/MenuService.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using SmartStore.Collections; + +namespace SmartStore.Web.Framework.UI +{ + public partial class MenuService : IMenuService + { + protected readonly IMenuResolver[] _menuResolvers; + + public MenuService(IEnumerable menuResolvers) + { + _menuResolvers = menuResolvers.OrderBy(x => x.Order).ToArray(); + } + + public virtual IMenu GetMenu(string name) + { + if (name.HasValue()) + { + foreach (var resolver in _menuResolvers) + { + if (resolver.Exists(name)) + { + return resolver.Resolve(name); + } + } + } + + return null; + } + + public virtual TreeNode GetRootNode(string menuName) + { + return GetMenu(menuName)?.Root; + } + + public virtual void ResolveElementCounts(string menuName, TreeNode curNode, bool deep = false) + { + GetMenu(menuName)?.ResolveElementCounts(curNode, deep); + } + + public virtual void ClearCache(string menuName) + { + GetMenu(menuName)?.ClearCache(); + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuInvalidator.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuInvalidator.cs new file mode 100644 index 0000000000..d9e8273392 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuInvalidator.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data.Entity; +using System.Threading.Tasks; +using SmartStore.Core.Caching; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Cms; +using SmartStore.Core.Events; +using SmartStore.Services.Catalog; + +namespace SmartStore.Web.Framework.UI.Menus.Providers +{ + /// + /// Invalidates all menus that contain the + /// + public class CatalogMenuInvalidator : IConsumer + { + private readonly IMenuService _menuService; + private readonly CatalogSettings _catalogSettings; + private readonly ICacheManager _cache; + private readonly IRepository _rs; + + private List _invalidated = new List(); + private List _countsResetted = new List(); + + public CatalogMenuInvalidator( + IMenuService menuService, + CatalogSettings catalogSettings, + ICacheManager cache, + IRepository rs) + { + _menuService = menuService; + _catalogSettings = catalogSettings; + _cache = cache; + _rs = rs; + } + + public async Task HandleAsync(CategoryTreeChangedEvent eventMessage) + { + var affectedMenuNames = await _rs.TableUntracked + .Where(x => x.ProviderName == "catalog") + .Select(x => x.Menu.SystemName) + .Distinct() + .ToListAsync(); + + foreach (var menuName in affectedMenuNames) + { + var reason = eventMessage.Reason; + + if (reason == CategoryTreeChangeReason.ElementCounts) + { + ResetElementCounts(menuName); + } + else + { + Invalidate(menuName); + } + } + } + + private void Invalidate(string menuName) + { + if (!_invalidated.Contains(menuName)) + { + _menuService.GetMenu(menuName)?.ClearCache(); + _invalidated.Add(menuName); + } + } + + private void ResetElementCounts(string menuName) + { + if (!_countsResetted.Contains(menuName) && _catalogSettings.ShowCategoryProductNumber) + { + var allCachedMenus = _menuService.GetMenu(menuName)?.GetAllCachedMenus(); + foreach (var kvp in allCachedMenus) + { + bool dirty = false; + kvp.Value.Traverse(x => + { + if (x.Value.ElementsCount.HasValue) + { + dirty = true; + x.Value.ElementsCount = null; + } + }, true); + + if (dirty) + { + _cache.Put(kvp.Key, kvp.Value); + } + } + + _countsResetted.Add(menuName); + } + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuItemProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuItemProvider.cs new file mode 100644 index 0000000000..259965448d --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuItemProvider.cs @@ -0,0 +1,167 @@ +using System.Collections.Generic; +using System.Linq; +using SmartStore.Collections; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Localization; +using SmartStore.Services; +using SmartStore.Services.Catalog; +using SmartStore.Services.Cms; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Services.Seo; +using SmartStore.Utilities; + +namespace SmartStore.Web.Framework.UI +{ + [MenuItemProvider("catalog", AppendsMultipleItems = true)] + public class CatalogMenuItemProvider : MenuItemProviderBase + { + private readonly ICommonServices _services; + private readonly ICategoryService _categoryService; + private readonly IPictureService _pictureService; + private readonly ILinkResolver _linkResolver; + + public CatalogMenuItemProvider( + ICommonServices services, + ICategoryService categoryService, + IPictureService pictureService, + ILinkResolver linkResolver) + { + _services = services; + _categoryService = categoryService; + _pictureService = pictureService; + _linkResolver = linkResolver; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public override TreeNode Append(MenuItemProviderRequest request) + { + if (request.IsEditMode) + { + var item = ConvertToMenuItem(request); + item.Summary = T("Providers.MenuItems.FriendlyName.Catalog"); + item.Icon = "fa fa-cubes"; + + AppendToParent(request, item); + } + else + { + var tree = _categoryService.GetCategoryTree(0, false, _services.StoreContext.CurrentStore.Id); + var allPictureIds = tree.Flatten().Select(x => x.PictureId.GetValueOrDefault()); + var allPictureInfos = _pictureService.GetPictureInfos(allPictureIds); + var randomId = CommonHelper.GenerateRandomInteger(0, 1000000); + + if (request.Entity.BeginGroup) + { + AppendToParent(request, new MenuItem + { + IsGroupHeader = true, + Text = request.Entity.GetLocalized(x => x.ShortDescription) + }); + } + + // Do not append the root itself. + foreach (var child in tree.Children) + { + AppendToParent(request, ConvertNode(request, child, allPictureInfos, ref randomId)); + } + } + + // Do not traverse appended items. + return null; + + // TBD: Cache invalidation workflow changes, because the catalog tree + // is now contained within other menus. Invalidating the tree now means: + // invalidate all containing menus also. + } + + protected override void ApplyLink(MenuItemProviderRequest request, TreeNode node) + { + // Void, does nothing here. + } + + private TreeNode ConvertNode( + MenuItemProviderRequest request, + TreeNode categoryNode, + IDictionary allPictureInfos, + ref int randomId) + { + var node = categoryNode.Value; + var name = node.Id > 0 ? node.GetLocalized(x => x.Name) : null; + + var menuItem = new MenuItem + { + Id = randomId++.ToString(), + EntityId = node.Id, + EntityName = nameof(Category), + MenuItemId = request.Entity.Id, + Text = name?.Value ?? node.Name, + Rtl = name?.CurrentLanguage?.Rtl ?? false, + BadgeText = node.Id > 0 ? node.GetLocalized(x => x.BadgeText) : null, + BadgeStyle = (BadgeStyle)node.BadgeStyle, + RouteName = node.Id > 0 ? "Category" : "HomePage", + }; + + // Handle external link + if (node.ExternalLink.HasValue()) + { + var link = _linkResolver.Resolve(node.ExternalLink); + if (link.Status == LinkStatus.Ok) + { + menuItem.Url = link.Link; + } + } + + if (menuItem.Url.IsEmpty()) + { + if (node.Id > 0) + { + menuItem.RouteName = "Category"; + menuItem.RouteValues.Add("SeName", node.GetSeName()); + } + else + { + menuItem.RouteName = "HomePage"; + } + } + + // Picture + if (node.Id > 0 && node.ParentCategoryId == 0 && node.Published && node.PictureId != null) + { + menuItem.ImageId = node.PictureId; + } + + // Apply inheritable properties. + menuItem.Visible = request.Entity.Published; + menuItem.PermissionNames = request.Entity.PermissionNames; + + if (request.Entity.NoFollow) + { + menuItem.LinkHtmlAttributes.Add("rel", "nofollow"); + } + + if (request.Entity.NewWindow) + { + menuItem.LinkHtmlAttributes.Add("target", "_blank"); + } + + var convertedNode = new TreeNode(menuItem) + { + Id = categoryNode.Id + }; + + if (categoryNode.HasChildren) + { + foreach (var childNode in categoryNode.Children) + { + convertedNode.Append(ConvertNode(request, childNode, allPictureInfos, ref randomId)); + } + } + + return convertedNode; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/EntityMenuItemProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/EntityMenuItemProvider.cs new file mode 100644 index 0000000000..f24844164b --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/EntityMenuItemProvider.cs @@ -0,0 +1,72 @@ +using SmartStore.Collections; +using SmartStore.Core.Localization; +using SmartStore.Services.Cms; + +namespace SmartStore.Web.Framework.UI +{ + // INFO: The provider's SystemName is also the edit template name > Views/Shared/EditorTemplates/MenuItem.{SystemName}.cshtml. + // Model is: string + [MenuItemProvider("entity")] + public class EntityMenuItemProvider : MenuItemProviderBase + { + private readonly ILinkResolver _linkResolver; + + public EntityMenuItemProvider(ILinkResolver linkResolver) + { + _linkResolver = linkResolver; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + protected override void ApplyLink(MenuItemProviderRequest request, TreeNode node) + { + // Always resolve against current store, current customer and working language. + var result = _linkResolver.Resolve(request.Entity.Model); + node.Value.Url = result.Link; + node.Value.ImageId = result.PictureId; + + if (node.Value.Text.IsEmpty()) + { + node.Value.Text = result.Label; + } + + switch (result.Type) + { + case LinkType.Product: + case LinkType.Category: + case LinkType.Manufacturer: + case LinkType.Topic: + if (request.IsEditMode) + { + // Info: node.Value.EntityId is MenuItemRecord.Id for editing MenuItemRecord. + } + else + { + node.Value.EntityId = result.Id; + node.Value.EntityName = result.Type.ToString(); + } + break; + } + + if (request.IsEditMode) + { + var info = result.Type.GetLinkTypeInfo(); + node.Value.Summary = T(info.ResKey); + node.Value.Icon = info.Icon; + + if (node.Value.Url.IsEmpty()) + { + node.Value.Text = null; + node.Value.ResKey = "Admin.ContentManagement.Menus.SpecifyLinkTarget"; + } + } + else + { + // For edit mode, only apply MenuItemRecord.Published. + node.Value.Visible = result.Status == LinkStatus.Ok; + } + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/IMenuItemProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/IMenuItemProvider.cs new file mode 100644 index 0000000000..412f306bc8 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/IMenuItemProvider.cs @@ -0,0 +1,39 @@ +using SmartStore.Collections; +using SmartStore.Core.Domain.Cms; + +namespace SmartStore.Web.Framework.UI +{ + public interface IMenuItemProvider + { + /// + /// Converts a object and appends it to the parent tree node. + /// + /// Contains information about the request to the provider. + /// Appended node. + TreeNode Append(MenuItemProviderRequest request); + } + + + public class MenuItemProviderRequest + { + /// + /// Represents the origin for the creation of the tree. + /// + public string Origin { get; set; } + + /// + /// Node to which items are to be appended. + /// + public TreeNode Parent { get; set; } + + /// + /// Entity that is converted to a menu item. + /// + public MenuItemRecord Entity { get; set; } + + /// + /// Inidicates whether the request is for backend menu editing. + /// + public bool IsEditMode => Origin.IsCaseInsensitiveEqual("EditMenu"); + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderBase.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderBase.cs new file mode 100644 index 0000000000..c46dca62ba --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderBase.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using SmartStore.Collections; +using SmartStore.Core.Domain.Cms; +using SmartStore.Services.Localization; + +namespace SmartStore.Web.Framework.UI +{ + public abstract class MenuItemProviderBase : IMenuItemProvider + { + public IIconExplorer IconExplorer { get; set; } + + public virtual TreeNode Append(MenuItemProviderRequest request) + { + Guard.NotNull(request, nameof(request)); + Guard.NotNull(request.Parent, nameof(request.Parent)); + Guard.NotNull(request.Entity, nameof(request.Entity)); + + // Add group header item. + if (request.Entity.BeginGroup && !request.IsEditMode) + { + AppendToParent(request, new MenuItem + { + IsGroupHeader = true, + Text = request.Entity.GetLocalized(x => x.ShortDescription) + }); + } + + var node = AppendToParent(request, ConvertToMenuItem(request)); + + ApplyLink(request, node); + + return node; + } + + protected virtual TreeNode AppendToParent(MenuItemProviderRequest request, MenuItem item) + { + return AppendToParent(request, new TreeNode(item)); + } + + protected virtual TreeNode AppendToParent(MenuItemProviderRequest request, TreeNode node) + { + var root = request.Parent.Root; + var providers = root.GetMetadata>("Providers"); + var provider = request.Entity.ProviderName; + + node.SetMetadata("Provider", provider); + + if (providers == null) + { + root.SetMetadata("Providers", new List { provider }); + } + else if (!providers.Contains(provider)) + { + providers.Add(provider); + } + + if (node.Id == null && node.Value.Id.HasValue()) + { + node.Id = node.Value.Id; + } + + return request.Parent.Append(node); + } + + /// + /// Converts the passed menu item entity to a object. + /// + /// The entity to convert. + /// Menu item. + protected virtual MenuItem ConvertToMenuItem(MenuItemProviderRequest request) + { + var entity = request.Entity; + var title = entity.GetLocalized(x => x.Title); + string shortDescription = entity.GetLocalized(x => x.ShortDescription); + + var menuItem = new MenuItem + { + EntityId = entity.Id, + EntityName = nameof(MenuItemRecord), + MenuItemId = entity.Id, + Text = title, + Summary = shortDescription, + Visible = entity.Published, + Rtl = title?.CurrentLanguage?.Rtl ?? false, + PermissionNames = entity.PermissionNames + }; + + if (shortDescription.HasValue()) + { + menuItem.LinkHtmlAttributes.Add("title", shortDescription); + } + + if (entity.NoFollow) + { + menuItem.LinkHtmlAttributes.Add("rel", "nofollow"); + } + + if (entity.NewWindow) + { + menuItem.LinkHtmlAttributes.Add("target", "_blank"); + } + + if (entity.CssClass.HasValue()) + { + menuItem.LinkHtmlAttributes.Add("class", entity.CssClass); + } + + if (entity.HtmlId.HasValue()) + { + menuItem.LinkHtmlAttributes.Add("id", entity.HtmlId); + menuItem.Id = entity.HtmlId; + } + + if (entity.Icon.HasValue() && !request.IsEditMode) + { + menuItem.Icon = IconExplorer.GetIconByName(entity.Icon).GetCssClass(entity.Style); + } + + // For future use: entity.ShowExpanded + + return menuItem; + } + + /// + /// Generates and applies the link to the converted object. + /// + /// Contains information about the request to the provider. + /// The newly created menu item node to apply the generated link to. + protected abstract void ApplyLink(MenuItemProviderRequest request, TreeNode node); + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderMetadata.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderMetadata.cs new file mode 100644 index 0000000000..f4590ef6fb --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderMetadata.cs @@ -0,0 +1,41 @@ +using System; +using SmartStore.Core.Domain.Cms; + +namespace SmartStore.Web.Framework.UI +{ + /// + /// Applies metadata to menu item provider types which implement . + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class MenuItemProviderAttribute : Attribute + { + public MenuItemProviderAttribute(string providerName) + { + Guard.NotEmpty(providerName, nameof(providerName)); + + ProviderName = providerName; + } + + /// + /// Unique name of the provider. + /// + public string ProviderName { get; set; } + + /// + /// Indicates that the provider appends multiple items to the tree. + /// The corresponding cannot have child elements and certain properties such as title, short description etc. are ignored. + /// + public bool AppendsMultipleItems { get; set; } + } + + + /// + /// Represents menu item provider registration metadata. + /// + public class MenuItemProviderMetadata + { + public string ProviderName { get; set; } + + public bool AppendsMultipleItems { get; set; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/RouteMenuItemProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/RouteMenuItemProvider.cs new file mode 100644 index 0000000000..7d4e5690ca --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/RouteMenuItemProvider.cs @@ -0,0 +1,37 @@ +using SmartStore.Collections; +using SmartStore.Core.Localization; + +namespace SmartStore.Web.Framework.UI +{ + [MenuItemProvider("route")] + public class RouteMenuItemProvider : MenuItemProviderBase + { + public RouteMenuItemProvider() + { + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + protected override void ApplyLink(MenuItemProviderRequest request, TreeNode node) + { + try + { + node.ApplyRouteData(request.Entity.Model); + } + catch { } + + if (request.IsEditMode) + { + node.Value.Summary = T("Providers.MenuItems.FriendlyName.Route"); + node.Value.Icon = "fas fa-directions"; + + if (!node.Value.HasRoute()) + { + node.Value.Text = null; + node.Value.ResKey = "Admin.ContentManagement.Menus.SpecifyLinkTarget"; + } + } + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/UserMenuFilter.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/UserMenuFilter.cs new file mode 100644 index 0000000000..0fb3e9a3bb --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/UserMenuFilter.cs @@ -0,0 +1,50 @@ +using System; +using System.Web.Mvc; +using SmartStore.Services.Cms; + +namespace SmartStore.Web.Framework.UI +{ + public class UserMenuFilter : IResultFilter + { + private readonly IWidgetProvider _widgetProvider; + private readonly IMenuStorage _menuStorage; + + public UserMenuFilter(IWidgetProvider widgetProvider, IMenuStorage menuStorage) + { + _widgetProvider = widgetProvider; + _menuStorage = menuStorage; + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { + if (filterContext.IsChildAction || !filterContext.Result.IsHtmlViewResult()) + { + return; + } + + ProcessUserMenus(); + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + } + + /// + /// Registers actions to render user menus in widget zones. + /// + private void ProcessUserMenus() + { + var menusInfo = _menuStorage.GetUserMenuInfos(); + + foreach (var info in menusInfo) + { + _widgetProvider.RegisterAction( + info.WidgetZones, + "Menu", + "Common", + new { area = "", name = info.SystemName, template = info.Template }, + info.DisplayOrder); + } + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMap.cs b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMap.cs deleted file mode 100644 index 00f95bf721..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMap.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using SmartStore.Collections; - -namespace SmartStore.Web.Framework.UI -{ - public interface ISiteMap - { - /// - /// Gets the sitemap/menu name (e.g. admin, catalog etc.) - /// - string Name { get; } - - /// - /// Gets the root node of the sitemap - /// - TreeNode Root { get; } - - /// - /// Whether menu items should be hidden based on permission names - /// - bool ApplyPermissions { get; } - - /// - /// Resolves the contained elements count (e.g. the products count in the catalog sitemap) - /// - /// The current node - /// false resolves counts for direct children of only, true traverses the whole sub-tree - void ResolveElementCounts(TreeNode curNode, bool deep = false); - - /// - /// Gets all cached trees from the underlying cache storage - /// - /// A dictionary of trees (Key: cache key, Value: tree instance) - /// - /// Multiple trees are created per sitemap depending - /// on language, customer-(roles), store and other parameters. - /// This method does not create anything, but returns all - /// previously processed and cached sitemap variations. - /// - IDictionary> GetAllCachedTrees(); - - /// - /// Removes the sitemap from the application cache - /// - void ClearCache(); - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMapService.cs b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMapService.cs deleted file mode 100644 index 3d461886dc..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMapService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Web.Mvc; -using SmartStore.Collections; - -namespace SmartStore.Web.Framework.UI -{ - public interface ISiteMapService - { - ISiteMap GetSiteMap(string name); - TreeNode GetRootNode(string mapName); - TreeNode GetCurrentNode(string mapName, ControllerContext controllerContext); - void ResolveElementCounts(string mapName, TreeNode curNode, bool deep = false); - void ClearCache(string mapName); - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBase.cs b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBase.cs deleted file mode 100644 index 688b369707..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBase.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Linq; -using SmartStore.Core.Logging; -using SmartStore.Collections; -using SmartStore.Services; -using System.Collections.Generic; - -namespace SmartStore.Web.Framework.UI -{ - public abstract class SiteMapBase : ISiteMap - { - /// - /// Key for SiteMap caching - /// - /// - /// {0} : sitemap name - /// {1} : sitemap specific key suffix - /// - internal const string SITEMAP_KEY = "pres:sitemap:{0}-{1}"; - internal const string SITEMAP_PATTERN_KEY = "pres:sitemap:{0}*"; - - public abstract string Name { get; } - - public virtual bool ApplyPermissions - { - get { return true; } - } - - public TreeNode Root - { - get - { - var cacheKey = SITEMAP_KEY.FormatInvariant(this.Name, GetCacheKey()); - var rootNode = Services.Cache.Get(cacheKey, () => - { - using (Services.Chronometer.Step("Build SiteMap '{0}'".FormatInvariant(this.Name))) - { - var root = Build(); - - // Menu publisher - MenuPublisher.RegisterMenus(root, this.Name); - - // Apply permission - if (this.ApplyPermissions) - { - DoApplyPermissions(root); - } - - // Event - Services.EventPublisher.Publish(new SiteMapBuiltEvent(this.Name, root)); - - return root; - } - }); - - return rootNode; - } - } - - protected virtual void DoApplyPermissions(TreeNode root) - { - // Hide based on permissions - root.Traverse(x => { - if (!MenuItemAccessPermitted(x.Value)) - { - x.Value.Visible = false; - } - }); - - // Hide dropdown nodes when no child is visible - root.Traverse(x => - { - var item = x.Value; - if (!item.IsGroupHeader && !item.HasRoute()) - { - if (!x.Children.Any(child => child.Value.Visible)) - { - item.Visible = false; - } - } - }); - } - - private bool MenuItemAccessPermitted(MenuItem item) - { - var result = true; - - if (item.PermissionNames.HasValue()) - { - var permitted = item.PermissionNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Any(x => Services.Permissions.Authorize(x.Trim())); - if (!permitted) - { - result = false; - } - } - - return result; - } - - protected abstract string GetCacheKey(); - - protected abstract TreeNode Build(); - - public virtual void ResolveElementCounts(TreeNode curNode, bool deep = false) - { - } - - public IDictionary> GetAllCachedTrees() - { - var cache = Services.Cache; - var keys = cache.Keys(SITEMAP_PATTERN_KEY.FormatInvariant(this.Name)); - - var trees = new Dictionary>(keys.Count()); - - foreach (var key in keys) - { - var tree = cache.Get>(key); - if (tree != null) - { - trees[key] = tree; - } - } - - return trees; - } - - public void ClearCache() - { - Services.Cache.RemoveByPattern(SITEMAP_PATTERN_KEY.FormatInvariant(this.Name)); - } - - #region Dependencies - - public ICommonServices Services { get; set; } - - public IMenuPublisher MenuPublisher { get; set; } - - #endregion - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapService.cs b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapService.cs deleted file mode 100644 index 3f1eda8a26..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapService.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Mvc; -using SmartStore.Collections; - -namespace SmartStore.Web.Framework.UI -{ - public class SiteMapService : ISiteMapService - { - private readonly IEnumerable _siteMaps; - private TreeNode _currentNode; - private bool _currentNodeResolved; - - public SiteMapService(IEnumerable siteMaps) - { - _siteMaps = siteMaps; - } - - public virtual ISiteMap GetSiteMap(string name) - { - Guard.NotEmpty(name, nameof(name)); - - var map = _siteMaps.First(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); - return map; - } - - public TreeNode GetRootNode(string mapName) - { - return GetSiteMap(mapName).Root; - } - - public TreeNode GetCurrentNode(string mapName, ControllerContext controllerContext) - { - if (!_currentNodeResolved) - { - _currentNode = GetSiteMap(mapName).Root.SelectNode(x => x.Value.IsCurrent(controllerContext), true); - _currentNodeResolved = true; - } - - return _currentNode; - } - - public void ResolveElementCounts(string mapName, TreeNode curNode, bool deep = false) - { - GetSiteMap(mapName).ResolveElementCounts(curNode, deep); - } - - public void ClearCache(string mapName) - { - GetSiteMap(mapName).ClearCache(); - } - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs b/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs index 7d349f7e4a..2c33fc64e0 100644 --- a/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs @@ -16,7 +16,7 @@ public void Validate(T model, ModelStateDictionary modelState) if (!result.IsValid) { - foreach (var error in result.Errors) + foreach (var error in result.Errors.DistinctBy(x => x.ErrorMessage)) { try { @@ -33,11 +33,14 @@ public bool Validate(T model, List warnings) if (!result.IsValid) { - foreach (var error in result.Errors) - warnings.Add(error.ErrorMessage); + foreach (var error in result.Errors.DistinctBy(x => x.ErrorMessage)) + { + warnings.Add(error.ErrorMessage); + } return false; } + return true; } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs index c1e0f4415e..b76b8b635b 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs @@ -7,9 +7,9 @@ namespace SmartStore.Web.Framework.WebApi.Caching { public static class WebApiCachingControllingData { - private static object _lock = new object(); + private static readonly object _lock = new object(); - public static string Key { get { return "WebApiControllingData"; } } + public static string Key { get; } = "WebApiControllingData"; public static void Remove() { diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingUserData.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingUserData.cs index 6f0a2a5c31..e6f8c62cf1 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingUserData.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingUserData.cs @@ -14,7 +14,7 @@ namespace SmartStore.Web.Framework.WebApi.Caching { public static class WebApiCachingUserData { - private static object _lock = new object(); + private static readonly object _lock = new object(); /// /// Lazy storing... fired on app shut down. Note that items with CacheItemPriority.NotRemovable are not removed when the cache is emptied. @@ -60,7 +60,7 @@ private static void OnDataRemoved(string key, object value, CacheItemRemovedReas } } - public static string Key { get { return "WebApiUserData"; } } + public static string Key { get; } = "WebApiUserData"; public static void Remove() { diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiExtension.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiExtension.cs index 0e6567ce8a..20a8480a0c 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiExtension.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiExtension.cs @@ -121,7 +121,7 @@ public static void DeleteLocalFiles(this MultipartFormDataStreamProvider provide try { foreach (var file in provider.FileData) - FileSystemHelper.Delete(file.LocalFileName); + FileSystemHelper.DeleteFile(file.LocalFileName); } catch { } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebStoreContext.cs b/src/Presentation/SmartStore.Web.Framework/WebStoreContext.cs index b6e97e2b1d..8ec5458aea 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebStoreContext.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebStoreContext.cs @@ -1,39 +1,74 @@ using System; using System.Linq; +using System.Collections.Generic; using System.Web; using SmartStore.Core; using SmartStore.Core.Domain.Stores; +using System.Collections.Concurrent; +using SmartStore.Core.Data; +using SmartStore.Core.Caching; +using SmartStore.Core.Data.Hooks; using SmartStore.Core.Infrastructure.DependencyManagement; -using SmartStore.Services.Stores; -using SmartStore.Core.Fakes; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Web.Framework { - /// - /// Store context for web application - /// - public partial class WebStoreContext : IStoreContext + public partial class WebStoreContext : DbSaveHook, IStoreContext { + public class StoreEntityCache + { + public IDictionary Stores { get; set; } + public IDictionary HostMap { get; set; } + public int PrimaryStoreId { get; set; } + + internal Store GetPrimaryStore() + { + return Stores.Get(PrimaryStoreId); + } + + internal Store GetStoreById(int id) + { + return Stores.Get(id); + } + + internal Store GetStoreByHostName(string host) + { + if (!string.IsNullOrEmpty(host) && HostMap.TryGetValue(host, out var id)) + { + return Stores.Get(id); + } + + return null; + } + } + internal const string OverriddenStoreIdKey = "OverriddenStoreId"; + const string CacheKey = "stores:all"; - private readonly Work _storeService; - private readonly IWebHelper _webHelper; - private readonly HttpContextBase _httpContext; + private readonly Lazy> _rs; + private readonly Lazy _httpContext; + private readonly ICacheManager _cache; private Store _currentStore; - public WebStoreContext(Work storeService) + public WebStoreContext(Lazy> rs, Lazy httpContext, ICacheManager cache) + { + _rs = rs; + _httpContext = httpContext; + _cache = cache; + } + + public int? GetRequestStore() { - _storeService = storeService; - _httpContext = HttpContext.Current != null ? (HttpContextBase)new HttpContextWrapper(HttpContext.Current) : new FakeHttpContext("~/"); - _webHelper = new WebHelper(_httpContext); + return _httpContext.Value.SafeGetHttpRequest()?.RequestContext?.RouteData?.DataTokens?.Get(OverriddenStoreIdKey)?.Convert(); } public void SetRequestStore(int? storeId) { - try + var dataTokens = _httpContext.Value.SafeGetHttpRequest()?.RequestContext?.RouteData?.DataTokens; + + if (dataTokens != null) { - var dataTokens = _httpContext.Request.RequestContext.RouteData.DataTokens; if (storeId.GetValueOrDefault() > 0) { dataTokens[OverriddenStoreIdKey] = storeId.Value; @@ -45,57 +80,27 @@ public void SetRequestStore(int? storeId) _currentStore = null; } - catch { } } - public int? GetRequestStore() + public int? GetPreviewStore() { - try + var cookie = _httpContext.Value.GetPreviewModeCookie(false); + if (cookie != null) { - var value = _httpContext.Request.RequestContext.RouteData?.DataTokens?.Get(OverriddenStoreIdKey); - if (value != null) + var value = cookie.Values[OverriddenStoreIdKey]; + if (value.HasValue()) { - return (int)value; + return value.ToInt(); } - - return null; - } - catch - { - return null; } + + return null; } public void SetPreviewStore(int? storeId) { - try - { - _httpContext.SetPreviewModeValue(OverriddenStoreIdKey, storeId.HasValue ? storeId.Value.ToString() : null); - _currentStore = null; - } - catch { } - } - - public int? GetPreviewStore() - { - try - { - var cookie = _httpContext.GetPreviewModeCookie(false); - if (cookie != null) - { - var value = cookie.Values[OverriddenStoreIdKey]; - if (value.HasValue()) - { - return value.ToInt(); - } - } - - return null; - } - catch - { - return null; - } + _httpContext.Value.SetPreviewModeValue(OverriddenStoreIdKey, storeId.HasValue ? storeId.Value.ToString() : null); + _currentStore = null; } /// @@ -107,30 +112,27 @@ public Store CurrentStore { if (_currentStore == null) { + var cachedStores = GetCachedStores(); + int? storeOverride = GetRequestStore() ?? GetPreviewStore(); if (storeOverride.HasValue) { - // the store to be used can be overwritten on request basis (e.g. for theme preview, editing etc.) - _currentStore = _storeService.Value.GetStoreById(storeOverride.Value); + // The store to be used can be overwritten on request basis (e.g. for theme preview, editing etc.) + _currentStore = cachedStores.GetStoreById(storeOverride.Value); } if (_currentStore == null) { - // ty to determine the current store by HTTP_HOST - var host = _webHelper.ServerVariables("HTTP_HOST"); - var allStores = _storeService.Value.GetAllStores(); - var store = allStores.FirstOrDefault(s => s.ContainsHostValue(host)); - - if (store == null) - { - // load the first found store - store = allStores.FirstOrDefault(); - } - - if (store == null) - throw new Exception("No store could be loaded"); - - _currentStore = store; + // Try to determine the current store by HTTP_HOST + var hostName = _httpContext.Value.SafeGetHttpRequest()?.ServerVariables["HTTP_HOST"]; + + _currentStore = + // Try to resolve the current store by HTTP_HOST + cachedStores.GetStoreByHostName(hostName) ?? + // Then resolve primary store + cachedStores.GetPrimaryStore() ?? + // No way + throw new Exception("No store could be loaded."); } } @@ -142,16 +144,61 @@ public Store CurrentStore } } - /// - /// IsSingleStoreMode ? 0 : CurrentStore.Id - /// public int CurrentStoreIdIfMultiStoreMode { get { - return _storeService.Value.IsSingleStoreMode() ? 0 : CurrentStore.Id; + return GetCachedStores().Stores.Count <= 1 ? 0 : CurrentStore.Id; } } + protected StoreEntityCache GetCachedStores() + { + return _cache.Get(CacheKey, () => + { + var entry = new StoreEntityCache(); + + var allStores = _rs.Value.TableUntracked + .Expand(x => x.PrimaryStoreCurrency) + .Expand(x => x.PrimaryExchangeRateCurrency) + .OrderBy(x => x.DisplayOrder) + .ThenBy(x => x.Name) + .ToList(); + + // Detach all entities... you never know. + allStores.Each(x => _rs.Value.Context.DetachEntity(x)); + + entry.Stores = allStores.ToDictionary(x => x.Id); + entry.HostMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var store in allStores) + { + var hostValues = store.ParseHostValues(); + foreach (var host in hostValues) + { + entry.HostMap[host] = store.Id; + } + } + + if (allStores.Count > 0) + { + entry.PrimaryStoreId = allStores.FirstOrDefault().Id; + } + + return entry; + }); + } + + public override void OnAfterSave(IHookedEntity entry) + { + if (entry.Entity is Store || entry.Entity is Currency) + { + _cache.Remove(CacheKey); + } + else + { + throw new NotSupportedException(); + } + } } } diff --git a/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss b/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss index 2426cbdd0f..a1427d316c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss +++ b/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss @@ -262,7 +262,7 @@ body { &.sticky { position: fixed; - z-index: 600; + z-index: $zindex-sticky; left: var(--content-padding-x); right: var(--content-padding-x); padding: calc(var(--content-padding-y) / 2) var(--content-padding-x); @@ -436,17 +436,29 @@ legend { text-align: center; &:hover { - color: $gray-600; + color: $gray-700; } > i { - font-size: 1.25rem; + font-size: 1rem; } } &:hover > .hint { opacity: 1; } + + .col-form-label-sm + .hint { + width: 1rem; + margin-bottom: 0; + line-height: $input-line-height-sm; + padding-top: calc(#{$input-padding-y-sm} + #{$input-border-width}); + padding-bottom: calc(#{$input-padding-y-sm} + #{$input-border-width}); + + > i { + font-size: inherit; + } + } } @@ -486,12 +498,16 @@ legend { left: 0; right: 0; top: 0; - width: 100%; height: 3px; - z-index: 999; + z-index: $zindex-sticky + 1; background-color: #ffab40; transition: opacity .05s ease-in; + .sticky-header & { + left: var(--content-padding-x); + right: var(--content-padding-x); + } + &.busy { opacity: 1; } @@ -1387,6 +1403,33 @@ table.payment-method-features td { } } +// +// Menus +// -------------------------------------------------------------- + +.menu-item-list { + .menu-item { + position: relative; + overflow: visible; + padding-left: 0px; + + .item-commands { + position: absolute; + left: 0; + top: 0; + bottom: 0; + + > .btn { + position: relative; + } + } + + &:hover .item-commands { + opacity: 1; + } + } +} + // // Misc @@ -1407,11 +1450,6 @@ table.payment-method-features td { } } - -// -// Misc -// -------------------------------------------------------------- - .cke_chrome.cke_focus { border-color: $input-focus-border-color; box-shadow: none; diff --git a/src/Presentation/SmartStore.Web/Administration/Content/_rtl.scss b/src/Presentation/SmartStore.Web/Administration/Content/_rtl.scss index e5490a3adc..bc996b8d57 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/_rtl.scss +++ b/src/Presentation/SmartStore.Web/Administration/Content/_rtl.scss @@ -254,6 +254,11 @@ .t-grid-header .t-header { text-align: right; + + .t-grid-filter { + left: 0; + right: initial; + } } .t-grid-edit-row .t-numerictextbox { diff --git a/src/Presentation/SmartStore.Web/Administration/Content/theme.scss b/src/Presentation/SmartStore.Web/Administration/Content/theme.scss index a9953de8c5..a45bc8dda9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/theme.scss +++ b/src/Presentation/SmartStore.Web/Administration/Content/theme.scss @@ -123,7 +123,7 @@ // -// Telerik adaptation: +// Vendor adaptations: // Must come latest because of css // specificity. // -------------------------------------------------- diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CategoryController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CategoryController.cs index e34b95b077..b389aac80f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CategoryController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CategoryController.cs @@ -317,7 +317,7 @@ from c in categories var mainList = query.ToList(); - var mruList = new MostRecentlyUsedList( + var mruList = new TrimmedBuffer( _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedCategories), _catalogSettings.MostRecentlyUsedCategoriesMaxSize) .Reverse() diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs index a1f462b301..34016582ba 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -51,7 +50,7 @@ namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public class CommonController : AdminControllerBase { const string CHECKUPDATE_CACHEKEY_PREFIX = "admin:common:checkupdateresult"; @@ -69,11 +68,11 @@ public class CommonController : AdminControllerBase private readonly ILocalizationService _localizationService; private readonly Lazy _imageCache; private readonly Lazy _importProfileService; + private readonly Lazy _iconExplorer; private readonly IGenericAttributeService _genericAttributeService; private readonly IDbCache _dbCache; private readonly ITaskScheduler _taskScheduler; private readonly ICommonServices _services; - private readonly Lazy _siteMapService; public CommonController( Lazy paymentService, @@ -89,11 +88,11 @@ public CommonController( ILocalizationService localizationService, Lazy imageCache, Lazy importProfileService, + Lazy iconExplorer, IGenericAttributeService genericAttributeService, IDbCache dbCache, ITaskScheduler taskScheduler, - ICommonServices services, - Lazy siteMapService) + ICommonServices services) { _paymentService = paymentService; _shippingService = shippingService; @@ -108,15 +107,13 @@ public CommonController( _localizationService = localizationService; _imageCache = imageCache; _importProfileService = importProfileService; + _iconExplorer = iconExplorer; _genericAttributeService = genericAttributeService; _dbCache = dbCache; _taskScheduler = taskScheduler; _services = services; - _siteMapService = siteMapService; } - #region Navbar & Menu - [ChildActionOnly] public ActionResult Navbar() { @@ -132,15 +129,6 @@ public ActionResult Navbar() return PartialView(); } - [ChildActionOnly] - public ActionResult Menu() - { - var rootNode = _siteMapService.Value.GetRootNode("admin"); - return PartialView(rootNode); - } - - #endregion - #region CheckUpdate public async Task CheckUpdate(bool enforce = false) @@ -282,6 +270,45 @@ private bool InstallablePackageExists() #region UI Helpers + [HttpPost] + public JsonResult SearchIcons(string term, string selected = null, int page = 1) + { + const int pageSize = 250; + + var icons = _iconExplorer.Value.All.AsEnumerable(); + + if (term.HasValue()) + { + icons = _iconExplorer.Value.FindIcons(term, true); + } + + var result = new PagedList(icons, page - 1, pageSize); + + if (selected.HasValue() && term.IsEmpty()) + { + var selIcon = _iconExplorer.Value.GetIconByName(selected); + if (!selIcon.IsPro && !result.Contains(selIcon)) + { + result.Insert(0, selIcon); + } + } + + return Json(new + { + results = result.Select(x => new + { + id = x.Name, + text = x.Name, + hasRegularStyle = x.HasRegularStyle, + isBrandIcon = x.IsBrandIcon, + isPro = x.IsPro, + label = x.Label, + styles = x.Styles + }), + pagination = new { more = result.HasNextPage } + }); + } + [HttpPost] public JsonResult SetSelectedTab(string navId, string tabId, string path) { @@ -851,7 +878,7 @@ public ActionResult MaintenanceDeleteImageCache() [HttpPost, ActionName("Maintenance")] [FormValueRequired("delete-guests")] - public ActionResult MaintenanceDeleteGuests(MaintenanceModel model) + public async Task MaintenanceDeleteGuests(MaintenanceModel model) { if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMaintenance)) return AccessDeniedView(); @@ -862,7 +889,7 @@ public ActionResult MaintenanceDeleteGuests(MaintenanceModel model) DateTime? endDateValue = (model.DeleteGuests.EndDate == null) ? null : (DateTime?)_dateTimeHelper.Value.ConvertToUtcTime(model.DeleteGuests.EndDate.Value, _dateTimeHelper.Value.CurrentTimeZone).AddDays(1); - model.DeleteGuests.NumberOfDeletedCustomers = _customerService.DeleteGuestCustomers(startDateValue, endDateValue, model.DeleteGuests.OnlyWithoutShoppingCart); + model.DeleteGuests.NumberOfDeletedCustomers = await _customerService.DeleteGuestCustomersAsync(startDateValue, endDateValue, model.DeleteGuests.OnlyWithoutShoppingCart); return View(model); } @@ -910,7 +937,7 @@ public ActionResult MaintenanceDeleteFiles(MaintenanceModel model) if ((!startDateValue.HasValue || startDateValue.Value < info.CreationTimeUtc) && (!endDateValue.HasValue || info.CreationTimeUtc < endDateValue.Value)) { - if (FileSystemHelper.Delete(fullPath)) + if (FileSystemHelper.DeleteFile(fullPath)) ++model.DeleteExportedFiles.NumberOfDeletedFiles; } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs index 6228ace363..679533bd2e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs @@ -558,19 +558,21 @@ public ActionResult Create() public ActionResult Create(CustomerModel model, bool continueEditing, FormCollection form) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCustomers)) + { return AccessDeniedView(); + } - if (model.Email.HasValue()) + /// Validate unique user. + if (model.Email.HasValue() && _customerService.GetCustomerByEmail(model.Email) != null) { - var tempCust = _customerService.GetCustomerByEmail(model.Email); - if (tempCust != null) - ModelState.AddModelError("", "Email is already registered"); + ModelState.AddModelError("", T("Account.Register.Errors.EmailAlreadyExists")); } - if (model.Username.HasValue() && _customerSettings.CustomerLoginType != CustomerLoginType.Email) + + if (model.Username.HasValue() && + _customerSettings.CustomerLoginType != CustomerLoginType.Email && + _customerService.GetCustomerByUsername(model.Username) != null) { - var tempCust = _customerService.GetCustomerByEmail(model.Username); - if (tempCust != null) - ModelState.AddModelError("", "Username is already registered"); + ModelState.AddModelError("", T("Account.Register.Errors.UsernameAlreadyExists")); } // Validate customer roles. @@ -581,15 +583,19 @@ public ActionResult Create(CustomerModel model, bool continueEditing, FormCollec { foreach (var customerRole in allCustomerRoles) { - if (model.SelectedCustomerRoleIds.Contains(customerRole.Id)) - newCustomerRoles.Add(customerRole); + if (model.SelectedCustomerRoleIds.Contains(customerRole.Id)) + { + newCustomerRoles.Add(customerRole); + } } } var customerRolesError = ValidateCustomerRoles(newCustomerRoles); if (customerRolesError.HasValue()) + { ModelState.AddModelError("", customerRolesError); + } var allowManagingCustomerRoles = _permissionService.Authorize(StandardPermissionProvider.ManageCustomerRoles); @@ -660,7 +666,7 @@ public ActionResult Create(CustomerModel model, bool continueEditing, FormCollec _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Fax, model.Fax); // Password. - if (!String.IsNullOrWhiteSpace(model.Password)) + if (model.Password.HasValue()) { var changePassRequest = new ChangePasswordRequest(model.Email, false, _customerSettings.DefaultPasswordFormat, model.Password); var changePassResult = _customerRegistrationService.ChangePassword(changePassRequest); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs index 3ad1460048..d971885b87 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Mime; using System.Text; using System.Web.Mvc; @@ -30,6 +31,7 @@ using SmartStore.Services.Messages; using SmartStore.Services.Security; using SmartStore.Services.Tasks; +using SmartStore.Utilities; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; @@ -1304,9 +1306,15 @@ public ActionResult DownloadLogFile(int id) return RedirectToAction("List"); } + [HttpGet] public ActionResult DownloadExportFile(int id, string name, bool? isDeployment) { - string message = null; + if (PathHelper.HasInvalidFileNameChars(name)) + { + return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Invalid file name."); + } + + string message = null; string path = null; if (Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) @@ -1317,8 +1325,10 @@ public ActionResult DownloadExportFile(int id, string name, bool? isDeployment) if (deployment != null) { var deploymentFolder = deployment.GetDeploymentFolder(); - if (deploymentFolder.HasValue()) - path = Path.Combine(deploymentFolder, name); + if (deploymentFolder.HasValue()) + { + path = Path.Combine(deploymentFolder, name); + } } } else @@ -1327,8 +1337,10 @@ public ActionResult DownloadExportFile(int id, string name, bool? isDeployment) if (profile != null) { path = Path.Combine(profile.GetExportFolder(true), name); - if (!System.IO.File.Exists(path)) - path = Path.Combine(profile.GetExportFolder(false), name); + if (!System.IO.File.Exists(path)) + { + path = Path.Combine(profile.GetExportFolder(false), name); + } } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs index 77927bf619..ef2131818c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Mime; using System.Text; using System.Web.Mvc; @@ -66,7 +67,7 @@ private bool IsValidImportFile(string path, out string error) catch (Exception ex) { error = ex.ToAllMessages(); - FileSystemHelper.Delete(path); + FileSystemHelper.DeleteFile(path); return false; } } @@ -397,7 +398,7 @@ public ActionResult Create(ImportProfileModel model) { var importFileDestination = Path.Combine(profile.GetImportFolder(true, true), model.TempFileName); - FileSystemHelper.Copy(importFile, importFileDestination, true, true); + FileSystemHelper.CopyFile(importFile, importFileDestination, true, true); return RedirectToAction("Edit", new { id = profile.Id }); } @@ -611,7 +612,7 @@ public JsonResult FileUpload(int id) if (id == 0) { var path = Path.Combine(FileSystemHelper.TempDirTenant(), postedFile.FileName); - FileSystemHelper.Delete(path); + FileSystemHelper.DeleteFile(path); success = postedFile.Stream.ToFile(path); if (success) @@ -733,9 +734,15 @@ public ActionResult DownloadLogFile(int id) return RedirectToAction("List"); } + [HttpGet] public ActionResult DownloadImportFile(int id, string name) { - string message = null; + if (PathHelper.HasInvalidFileNameChars(name)) + { + return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Invalid file name."); + } + + string message = null; if (Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) { @@ -744,8 +751,10 @@ public ActionResult DownloadImportFile(int id, string name) { var path = Path.Combine(profile.GetImportFolder(true), name); - if (!System.IO.File.Exists(path)) - path = Path.Combine(profile.GetImportFolder(false), name); + if (!System.IO.File.Exists(path)) + { + path = Path.Combine(profile.GetImportFolder(false), name); + } if (System.IO.File.Exists(path)) { @@ -753,8 +762,10 @@ public ActionResult DownloadImportFile(int id, string name) { var stream = new FileStream(path, FileMode.Open); - var result = new FileStreamResult(stream, MimeTypes.MapNameToMimeType(path)); - result.FileDownloadName = Path.GetFileName(path); + var result = new FileStreamResult(stream, MimeTypes.MapNameToMimeType(path)) + { + FileDownloadName = Path.GetFileName(path) + }; return result; } @@ -787,7 +798,7 @@ public ActionResult DeleteImportFile(int id, string name) if (profile != null) { var path = Path.Combine(profile.GetImportFolder(true), name); - FileSystemHelper.Delete(path); + FileSystemHelper.DeleteFile(path); } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs index f5388be5d7..72b56965a2 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs @@ -177,7 +177,7 @@ private void PrepareAvailableLanguageModel( model.TranslatedPercentage = resources.TranslatedPercentage; model.IsDownloadRunning = state != null && state.Id == resources.Id; model.UpdatedOn = _dateTimeHelper.ConvertToUserTime(resources.UpdatedOn, DateTimeKind.Utc); - model.UpdatedOnString = model.UpdatedOn.RelativeFormat(false, "f"); + model.UpdatedOnString = resources.UpdatedOn.RelativeFormat(true, "f"); model.FlagImageFileName = GetFlagFileName(resources.Language.Culture); if (language != null && lastImportInfos.TryGetValue(language.Id, out LastResourcesImportInfo info)) @@ -804,7 +804,7 @@ public async Task ImportXml(int id, FormCollection form, ImportMod } finally { - FileSystemHelper.Delete(tempFilePath); + FileSystemHelper.DeleteFile(tempFilePath); } return RedirectToAction("Edit", new { id = language.Id }); @@ -882,7 +882,7 @@ private void DownloadCore(ILifetimeScope scope, CancellationToken ct, LanguageDo asyncState.Remove(); } - FileSystemHelper.Delete(tempFilePath); + FileSystemHelper.DeleteFile(tempFilePath); } } @@ -890,17 +890,14 @@ public async Task Download(int setId) { if (_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) { - var ctx = new LanguageDownloadContext(setId); - ctx.AvailableResources = await CheckAvailableResources(); + var ctx = new LanguageDownloadContext(setId) + { + AvailableResources = await CheckAvailableResources() + }; - if (ctx.AvailableResources.Resources.Any()) + if (ctx.AvailableResources.Resources.Any()) { - var task = AsyncRunner.Run( - (container, ct, obj) => DownloadCore(container, ct, obj as LanguageDownloadContext), - ctx, - CancellationToken.None, - TaskCreationOptions.None, - TaskScheduler.Default).ConfigureAwait(false); + var task = AsyncRunner.Run((c, ct, obj) => DownloadCore(c, ct, obj as LanguageDownloadContext), ctx); } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ManufacturerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ManufacturerController.cs index 1411cbb70d..6b58b74b47 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ManufacturerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ManufacturerController.cs @@ -207,7 +207,7 @@ public ActionResult AllManufacturers(string label, int selectedId) var mainList = list.ToList(); - var mruList = new MostRecentlyUsedList( + var mruList = new TrimmedBuffer( _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedManufacturers), _catalogSettings.MostRecentlyUsedManufacturersMaxSize) .Reverse() diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/MenuController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/MenuController.cs new file mode 100644 index 0000000000..c864ad81cc --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/MenuController.cs @@ -0,0 +1,579 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using SmartStore.Admin.Models.Menus; +using SmartStore.ComponentModel; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Cms; +using SmartStore.Core.Domain.Common; +using SmartStore.Services.Cms; +using SmartStore.Services.Customers; +using SmartStore.Services.Localization; +using SmartStore.Services.Security; +using SmartStore.Services.Stores; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Modelling; +using SmartStore.Web.Framework.Security; +using SmartStore.Web.Framework.UI; +using Telerik.Web.Mvc; + +namespace SmartStore.Admin.Controllers +{ + [AdminAuthorize] + public class MenuController : AdminControllerBase + { + private readonly IMenuStorage _menuStorage; + private readonly ILanguageService _languageService; + private readonly ILocalizedEntityService _localizedEntityService; + private readonly IStoreMappingService _storeMappingService; + private readonly IAclService _aclService; + private readonly ICustomerService _customerService; + private readonly IDictionary> _menuItemProviders; + private readonly AdminAreaSettings _adminAreaSettings; + + public MenuController( + IMenuStorage menuStorage, + ILanguageService languageService, + ILocalizedEntityService localizedEntityService, + IStoreMappingService storeMappingService, + IAclService aclService, + ICustomerService customerService, + IEnumerable> menuItemProviders, + AdminAreaSettings adminAreaSettings) + { + _menuStorage = menuStorage; + _languageService = languageService; + _localizedEntityService = localizedEntityService; + _storeMappingService = storeMappingService; + _aclService = aclService; + _customerService = customerService; + _menuItemProviders = menuItemProviders.ToDictionarySafe(x => x.Metadata.ProviderName, x => x); + _adminAreaSettings = adminAreaSettings; + } + + public ActionResult Index() + { + return RedirectToAction("List"); + } + + public ActionResult List() + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + var model = new MenuRecordListModel + { + GridPageSize = _adminAreaSettings.GridPageSize, + AvailableStores = Services.StoreService.GetAllStores().ToSelectListItems() + }; + + return View(model); + } + + [HttpPost, GridAction(EnableCustomBinding = true)] + public ActionResult List(GridCommand command, MenuRecordListModel model) + { + var gridModel = new GridModel(); + + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + var menus = _menuStorage.GetAllMenus(model.SystemName, model.StoreId, true, command.Page - 1, command.PageSize); + + gridModel.Data = menus.Select(x => + { + var itemModel = new MenuRecordModel(); + MiniMapper.Map(x, itemModel); + + return itemModel; + }); + + gridModel.Total = menus.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + NotifyAccessDenied(); + } + + return new JsonResult + { + MaxJsonLength = int.MaxValue, + Data = gridModel + }; + } + + public ActionResult Create() + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + var model = new MenuRecordModel(); + PrepareModel(model, null); + AddLocales(_languageService, model.Locales); + + return View(model); + } + + [HttpPost, ValidateInput(false), ParameterBasedOnFormName("save-continue", "continueEditing")] + public ActionResult Create(MenuRecordModel model, bool continueEditing, FormCollection form) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + if (ModelState.IsValid) + { + var menu = MiniMapper.Map(model); + menu.WidgetZone = string.Join(",", model.WidgetZone ?? new string[0]).NullEmpty(); + + _menuStorage.InsertMenu(menu); + + SaveStoreMappings(menu, model); + SaveAclMappings(menu, model); + UpdateLocales(menu, model); + + Services.EventPublisher.Publish(new ModelBoundEvent(model, menu, form)); + + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + return continueEditing ? RedirectToAction("Edit", new { id = menu.Id }) : RedirectToAction("List"); + } + + PrepareModel(model, null); + + return View(model); + } + + public ActionResult Edit(int id) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + var menu = _menuStorage.GetMenuById(id); + if (menu == null) + { + return HttpNotFound(); + } + + var model = MiniMapper.Map(menu); + model.WidgetZone = menu.WidgetZone.SplitSafe(","); + + PrepareModel(model, menu); + AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Title = menu.GetLocalized(x => x.Title, languageId, false, false); + }); + + return View(model); + } + + [HttpPost, ValidateInput(false), ParameterBasedOnFormName("save-continue", "continueEditing")] + public ActionResult Edit(MenuRecordModel model, bool continueEditing, FormCollection form) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + var menu = _menuStorage.GetMenuById(model.Id); + if (menu == null) + { + return HttpNotFound(); + } + + if (ModelState.IsValid) + { + MiniMapper.Map(model, menu); + menu.WidgetZone = string.Join(",", model.WidgetZone ?? new string[0]).NullEmpty(); + + _menuStorage.UpdateMenu(menu); + + SaveStoreMappings(menu, model); + SaveAclMappings(menu, model); + UpdateLocales(menu, model); + + Services.EventPublisher.Publish(new ModelBoundEvent(model, menu, form)); + + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + return continueEditing ? RedirectToAction("Edit", menu.Id) : RedirectToAction("List"); + } + + PrepareModel(model, menu); + + return View(model); + } + + [HttpPost, ActionName("Delete")] + public ActionResult DeleteConfirmed(int id) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + var menu = _menuStorage.GetMenuById(id); + if (menu == null) + { + return HttpNotFound(); + } + + if (menu.IsSystemMenu) + { + NotifyError(T("Admin.ContentManagement.Menus.CannotBeDeleted")); + return RedirectToAction("Edit", new { id = menu.Id }); + } + + _menuStorage.DeleteMenu(menu); + + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + return RedirectToAction("List"); + } + + #region Menu items + + // Ajax. + public ActionResult ItemList(int id) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + NotifyAccessDenied(); + return new EmptyResult(); + } + + var model = new MenuRecordModel { Id = id }; + PrepareModel(model, null); + + return PartialView(model); + } + + // Do not use model binding because of input validation. + public ActionResult CreateItem(string providerName, int menuId, int parentItemId) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + var menu = _menuStorage.GetMenuById(menuId); + if (menu == null) + { + return HttpNotFound(); + } + + var model = new MenuItemRecordModel + { + ProviderName = providerName, + MenuId = menuId, + ParentItemId = parentItemId, + Published = true + }; + + PrepareModel(model, null); + AddLocales(_languageService, model.Locales); + + return View(model); + } + + // Do not name parameter "model" because of property of same name. + [HttpPost, ValidateInput(false), ParameterBasedOnFormName("save-item-continue", "continueEditing")] + public ActionResult CreateItem(MenuItemRecordModel itemModel, bool continueEditing, FormCollection form) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + if (ModelState.IsValid) + { + var item = MiniMapper.Map(itemModel); + item.ParentItemId = itemModel.ParentItemId ?? 0; + item.PermissionNames = string.Join(",", itemModel.PermissionNames ?? new string[0]).NullEmpty(); + + _menuStorage.InsertMenuItem(item); + + UpdateLocales(item, itemModel); + + Services.EventPublisher.Publish(new ModelBoundEvent(itemModel, item, form)); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + + if (continueEditing) + { + return RedirectToAction("EditItem", new { id = item.Id }); + } + + return RedirectToAction("Edit", new { id = item.MenuId }); + } + + PrepareModel(itemModel, null); + + return View(itemModel); + } + + public ActionResult EditItem(int id) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + var item = _menuStorage.GetMenuItemById(id); + if (item == null) + { + return HttpNotFound(); + } + + var model = MiniMapper.Map(item); + model.ParentItemId = item.ParentItemId == 0 ? (int?)null : item.ParentItemId; + model.PermissionNames = item.PermissionNames.SplitSafe(","); + + PrepareModel(model, item); + AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Title = item.GetLocalized(x => x.Title, languageId, false, false); + locale.ShortDescription = item.GetLocalized(x => x.ShortDescription, languageId, false, false); + }); + + return View(model); + } + + // Do not name parameter "model" because of property of same name. + [HttpPost, ValidateInput(false), ParameterBasedOnFormName("save-item-continue", "continueEditing")] + public ActionResult EditItem(MenuItemRecordModel itemModel, bool continueEditing, FormCollection form) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return AccessDeniedView(); + } + + var item = _menuStorage.GetMenuItemById(itemModel.Id); + if (item == null) + { + return HttpNotFound(); + } + + if (ModelState.IsValid) + { + MiniMapper.Map(itemModel, item); + item.ParentItemId = itemModel.ParentItemId ?? 0; + item.PermissionNames = string.Join(",", itemModel.PermissionNames ?? new string[0]).NullEmpty(); + + _menuStorage.UpdateMenuItem(item); + + UpdateLocales(item, itemModel); + + Services.EventPublisher.Publish(new ModelBoundEvent(itemModel, item, form)); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + + if (continueEditing) + { + return RedirectToAction("EditItem", new { id = item.Id }); + } + + return RedirectToAction("Edit", new { id = item.MenuId }); + } + + PrepareModel(itemModel, item); + + return View(itemModel); + } + + // Ajax. + [HttpPost] + public ActionResult MoveItem(int menuId, int sourceId, string direction) + { + if (menuId == 0 || sourceId == 0 || direction.IsEmpty()) + { + return new EmptyResult(); + } + + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + NotifyAccessDenied(); + return new EmptyResult(); + } + + using (var scope = new DbContextScope(ctx: Services.DbContext, autoCommit: false)) + { + var allItems = _menuStorage.GetMenuItems(menuId, 0, true).ToDictionary(x => x.Id, x => x); + var sourceItem = allItems[sourceId]; + + var siblings = allItems.Select(x => x.Value) + .Where(x => x.ParentItemId == sourceItem.ParentItemId) + .OrderBy(x => x.DisplayOrder) + .ToList(); + + var index = siblings.IndexOf(sourceItem) + (direction == "up" ? -1 : 1); + if (index >= 0 && index < siblings.Count) + { + var targetItem = siblings[index]; + + // Ensure unique display order starting from 1. + var count = 0; + siblings.Each(x => x.DisplayOrder = ++count); + + // Swap display order of source and target item. + var tmp = sourceItem.DisplayOrder; + sourceItem.DisplayOrder = targetItem.DisplayOrder; + targetItem.DisplayOrder = tmp; + + scope.Commit(); + } + } + + return RedirectToAction("ItemList", new { id = menuId }); + } + + [HttpPost] + public ActionResult DeleteItem(int id) + { + var isAjax = Request.IsAjaxRequest(); + + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMenus)) + { + return isAjax ? new EmptyResult() : AccessDeniedView(); + } + + var item = _menuStorage.GetMenuItemById(id); + if (item == null) + { + if (isAjax) + { + return new EmptyResult(); + } + + return HttpNotFound(); + } + + var menuId = item.MenuId; + _menuStorage.DeleteMenuItem(item); + + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + + return RedirectToAction(isAjax ? "ItemList" : "Edit", new { id = menuId }); + } + + #endregion + + #region Utilities + + private void PrepareModel(MenuRecordModel model, MenuRecord entity) + { + var templateNames = new string[] { "LinkList", "ListGroup", "Dropdown", "Navbar" }; + + if (entity != null && ModelState.IsValid) + { + model.SelectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(entity); + model.SelectedCustomerRoleIds = _aclService.GetCustomerRoleIdsWithAccessTo(entity); + + model.IsCustomTemplate = entity.Template.HasValue() && !templateNames.Contains(entity.Template); + } + + model.Locales = new List(); + model.AvailableStores = Services.StoreService.GetAllStores().ToSelectListItems(model.SelectedStoreIds); + model.AvailableCustomerRoles = _customerService.GetAllCustomerRoles(true).ToSelectListItems(model.SelectedCustomerRoleIds); + + model.AllTemplates = templateNames + .Select(x => new SelectListItem { Text = x, Value = x, Selected = x.IsCaseInsensitiveEqual(entity?.Template) }) + .ToList(); + + model.AllProviders = _menuItemProviders.Values + .Select(x => new SelectListItem + { + Text = T("Providers.MenuItems.FriendlyName." + x.Metadata.ProviderName), + Value = x.Metadata.ProviderName + }) + .ToList(); + + var entities = _menuStorage.GetMenuItems(model.Id, 0, true); + model.ItemTree = entities.GetTree("EditMenu", _menuItemProviders); + } + + private void PrepareModel(MenuItemRecordModel model, MenuItemRecord entity) + { + Lazy provider = null; + var entities = _menuStorage.GetMenuItems(model.MenuId, 0, true).ToDictionary(x => x.Id, x => x); + + model.Locales = new List(); + model.AllItems = new List(); + + if (_menuItemProviders.TryGetValue(model.ProviderName, out provider)) + { + model.ProviderAppendsMultipleItems = provider.Metadata.AppendsMultipleItems; + } + + // Preset max display order to always insert item at the end. + if (entity == null && entities.Any()) + { + var item = entities + .Select(x => x.Value) + .Where(x => x.ParentItemId == model.ParentItemId) + .OrderByDescending(x => x.DisplayOrder) + .FirstOrDefault(); + + model.DisplayOrder = (item?.DisplayOrder ?? 0) + 1; + } + + // Create list for selecting parent item. + var tree = entities.Values.GetTree("EditMenu", _menuItemProviders); + + tree.Traverse(x => + { + if (entity != null && entity.Id == x.Value.EntityId) + { + // Ignore. Element cannot be parent itself. + model.TitlePlaceholder = x.Value.Text; + } + else if (entities.TryGetValue(x.Value.EntityId, out var record) && + _menuItemProviders.TryGetValue(record.ProviderName, out provider) && + provider.Metadata.AppendsMultipleItems) + { + // Ignore. Element cannot have child nodes. + } + else if (!x.Value.IsGroupHeader) + { + var path = string.Join(" » ", x.Trail.Skip(1).Select(y => y.Value.Text)); + model.AllItems.Add(new SelectListItem + { + Text = path, + Value = x.Value.EntityId.ToString(), + Selected = entity != null && entity.ParentItemId == x.Value.EntityId + }); + } + }); + } + + private void UpdateLocales(MenuRecord entity, MenuRecordModel model) + { + if (model.Locales != null) + { + foreach (var localized in model.Locales) + { + _localizedEntityService.SaveLocalizedValue(entity, x => x.Title, localized.Title, localized.LanguageId); + } + } + } + + private void UpdateLocales(MenuItemRecord entity, MenuItemRecordModel model) + { + if (model.Locales != null) + { + foreach (var localized in model.Locales) + { + _localizedEntityService.SaveLocalizedValue(entity, x => x.Title, localized.Title, localized.LanguageId); + _localizedEntityService.SaveLocalizedValue(entity, x => x.ShortDescription, localized.ShortDescription, localized.LanguageId); + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/MessageTemplateController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/MessageTemplateController.cs index db5273490c..509b57af78 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/MessageTemplateController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/MessageTemplateController.cs @@ -21,6 +21,7 @@ using SmartStore.Core.Email; using System.Threading.Tasks; using System.Web.Caching; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Admin.Controllers { @@ -197,7 +198,8 @@ private void PrepareLastModelTree(MessageTemplate template) [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] [FormValueRequired("save", "save-continue")] - public ActionResult Edit(MessageTemplateModel model, bool continueEditing) + [ValidateInput(false)] + public ActionResult Edit(MessageTemplateModel model, bool continueEditing, FormCollection form) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageMessageTemplates)) return AccessDeniedView(); @@ -222,6 +224,8 @@ public ActionResult Edit(MessageTemplateModel model, bool continueEditing) // locales UpdateLocales(messageTemplate, model); + Services.EventPublisher.Publish(new ModelBoundEvent(model, messageTemplate, form)); + NotifySuccess(_localizationService.GetResource("Admin.ContentManagement.MessageTemplates.Updated")); return continueEditing ? RedirectToAction("Edit", messageTemplate.Id) : RedirectToAction("List"); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs index 3c1afff0f8..a65ccdc5c6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs @@ -68,7 +68,7 @@ public ActionResult List() Id = x.Id, CustomerInfo = x.IsRegistered() ? x.Email : T("Admin.Customers.Guest").Text, LastIpAddress = x.LastIpAddress, - Location = _geoCountryLookup.LookupCountryName(x.LastIpAddress), + Location = _geoCountryLookup.LookupCountry(x.LastIpAddress)?.Name.EmptyNull(), LastActivityDate = _dateTimeHelper.ConvertToUserTime(x.LastActivityDateUtc, DateTimeKind.Utc), LastVisitedPage = x.GetAttribute(SystemCustomerAttributeNames.LastVisitedPage) }; @@ -95,7 +95,7 @@ public ActionResult List(GridCommand command) Id = x.Id, CustomerInfo = x.IsRegistered() ? x.Email : T("Admin.Customers.Guest").Text, LastIpAddress = x.LastIpAddress, - Location = _geoCountryLookup.LookupCountryName(x.LastIpAddress), + Location = _geoCountryLookup.LookupCountry(x.LastIpAddress)?.Name.EmptyNull(), LastActivityDate = _dateTimeHelper.ConvertToUserTime(x.LastActivityDateUtc, DateTimeKind.Utc), LastVisitedPage = x.GetAttribute(SystemCustomerAttributeNames.LastVisitedPage) }; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs index 60a828e07d..36f63cceb1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs @@ -880,19 +880,48 @@ public ActionResult Index() public ActionResult List(OrderListModel model) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { return AccessDeniedView(); + } - var allStores = Services.StoreService.GetAllStores(); + var allStores = Services.StoreService.GetAllStores(); + var paymentMethods = _paymentService.LoadAllPaymentMethods(); - model.AvailableOrderStatuses = OrderStatus.Pending.ToSelectList(false).ToList(); + model.AvailableOrderStatuses = OrderStatus.Pending.ToSelectList(false).ToList(); model.AvailablePaymentStatuses = PaymentStatus.Pending.ToSelectList(false).ToList(); model.AvailableShippingStatuses = ShippingStatus.NotYetShipped.ToSelectList(false).ToList(); - model.AvailableStores = allStores - .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) - .ToList(); + model.AvailableStores = allStores + .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) + .ToList(); + + model.AvailablePaymentMethods = paymentMethods + .Select(x => new SelectListItem + { + Text = _pluginMediator.GetLocalizedFriendlyName(x.Metadata).NullEmpty() ?? x.Metadata.FriendlyName.NullEmpty() ?? x.Metadata.SystemName, + Value = x.Metadata.SystemName + }) + .ToList(); + + var paymentMethodsCounts = model.AvailablePaymentMethods + .GroupBy(x => x.Text) + .Select(x => new { Name = x.Key.EmptyNull(), Count = x.Count() }) + .ToDictionarySafe(x => x.Name, x => x.Count); + + model.AvailablePaymentMethods = model.AvailablePaymentMethods + .OrderBy(x => x.Text) + .Select(x => + { + if (paymentMethodsCounts[x.Text] > 1) + { + x.Text = "{0} ({1})".FormatInvariant(x.Text, x.Value); + } + + return x; + }) + .ToList(); - model.GridPageSize = _adminAreaSettings.GridPageSize; + model.GridPageSize = _adminAreaSettings.GridPageSize; return View(model); } @@ -917,7 +946,7 @@ public ActionResult OrderList(GridCommand command, OrderListModel model) Provider paymentMethod = null; var orders = _orderService.SearchOrders(model.StoreId, 0, startDateValue, endDateValue, orderStatusIds, paymentStatusIds, shippingStatusIds, - model.CustomerEmail, model.OrderGuid, model.OrderNumber, command.Page - 1, command.PageSize, model.CustomerName); + model.CustomerEmail, model.OrderGuid, model.OrderNumber, command.Page - 1, command.PageSize, model.CustomerName, model.PaymentMethods.SplitSafe(",")); gridModel.Data = orders.Select(x => { @@ -1034,7 +1063,7 @@ public ActionResult GoToOrderId(OrderListModel model) #region Export / Import - [HttpPost, Compress] + [HttpPost] public ActionResult ExportPdf(bool all, string selectedIds = null) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) @@ -1839,6 +1868,7 @@ public ActionResult AddProductToOrderDetails(int orderId, int productId) } [HttpPost] + [ValidateInput(false)] public ActionResult AddProductToOrderDetails(int orderId, int productId, bool adjustInventory, bool? updateTotals, ProductVariantQuery query, FormCollection form) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/PictureController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/PictureController.cs index 2b8c3601b1..c831a228fd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/PictureController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/PictureController.cs @@ -1,4 +1,6 @@ -using System.Web.Mvc; +using System; +using System.Linq; +using System.Web.Mvc; using SmartStore.Core.Domain.Media; using SmartStore.Data.Utilities; using SmartStore.Services.Media; @@ -38,8 +40,9 @@ public ActionResult AsyncUpload(bool isTransient = false, bool validate = true) } var picture = _pictureService.InsertPicture(postedFile.Buffer, postedFile.ContentType, null, true, isTransient, validate); - return Json( - new { + + return Json(new + { success = true, pictureId = picture.Id, imageUrl = _pictureService.GetUrl(picture, _mediaSettings.ProductThumbPictureSize, host: "") diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs index 5c58b7b2f2..fe7b22b10a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs @@ -102,22 +102,22 @@ private LicensingData PrepareLicenseLabelModel(LicenseLabelModel model, PluginDe model.IsLicensable = true; model.LicenseUrl = Url.Action("LicensePlugin", new { systemName = pluginDescriptor.SystemName }); - var license = LicenseChecker.GetLicense(pluginDescriptor.SystemName, url); - if (license == null) + var cachedLicense = LicenseChecker.GetLicense(pluginDescriptor.SystemName, url); + if (cachedLicense == null) { - // Licensed plugin has not been used yet -> Check state. - var unused = LicenseChecker.CheckState(pluginDescriptor.SystemName, url); + // Licensed plugin has not been used yet -> Check state. + model.LicenseState = LicenseChecker.CheckState(pluginDescriptor.SystemName, url); // And try to get license data again. - license = LicenseChecker.GetLicense(pluginDescriptor.SystemName, url); + cachedLicense = LicenseChecker.GetLicense(pluginDescriptor.SystemName, url); } - if (license != null) + if (cachedLicense != null) { // Licensed plugin has been used. - model.LicenseState = license.State; - model.TruncatedLicenseKey = license.TruncatedLicenseKey; - model.RemainingDemoUsageDays = license.RemainingDemoDays; + model.LicenseState = cachedLicense.State; + model.TruncatedLicenseKey = cachedLicense.TruncatedLicenseKey; + model.RemainingDemoUsageDays = cachedLicense.RemainingDemoDays; } else { @@ -125,7 +125,7 @@ private LicensingData PrepareLicenseLabelModel(LicenseLabelModel model, PluginDe model.HideLabel = true; } - return license; + return cachedLicense; } return null; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs index b6063da050..d5c2c13446 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs @@ -1024,7 +1024,12 @@ public ActionResult ProductList(GridCommand command, ProductListModel model) { var gridModel = new GridModel(); - if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + gridModel.Data = Enumerable.Empty(); + NotifyAccessDenied(); + } + else { var fields = new List { "name" }; if (_searchSettings.SearchFields.Contains("sku")) @@ -1063,17 +1068,17 @@ public ActionResult ProductList(GridCommand command, ProductListModel model) //_mediaSettings.CartThumbPictureSize gridModel.Data = products.Select(x => { - var productModel = new ProductModel - { - Sku = x.Sku, - Published = x.Published, - ProductTypeLabelHint = x.ProductTypeLabelHint, - Name = x.Name, - Id = x.Id, - StockQuantity = x.StockQuantity, - Price = x.Price, - LimitedToStores = x.LimitedToStores - }; + var productModel = new ProductModel + { + Sku = x.Sku, + Published = x.Published, + ProductTypeLabelHint = x.ProductTypeLabelHint, + Name = x.Name, + Id = x.Id, + StockQuantity = x.StockQuantity, + Price = x.Price, + LimitedToStores = x.LimitedToStores + }; var defaultPicture = pictureInfos.Get(x.MainPictureId.GetValueOrDefault()); productModel.PictureThumbnailUrl = _pictureService.GetUrl(defaultPicture, _mediaSettings.CartThumbPictureSize, true); @@ -1088,12 +1093,6 @@ public ActionResult ProductList(GridCommand command, ProductListModel model) gridModel.Total = products.TotalCount; } - else - { - gridModel.Data = Enumerable.Empty(); - - NotifyAccessDenied(); - } return new JsonResult { @@ -1556,8 +1555,10 @@ public ActionResult ProductCategoryInsert(GridCommand command, ProductModel.Prod _categoryService.InsertProductCategory(productCategory); - var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedCategories), - model.Category, _catalogSettings.MostRecentlyUsedCategoriesMaxSize); + var mru = new TrimmedBuffer( + _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedCategories), + model.Category, + _catalogSettings.MostRecentlyUsedCategoriesMaxSize); _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedCategories, mru.ToString()); } @@ -1582,8 +1583,10 @@ public ActionResult ProductCategoryUpdate(GridCommand command, ProductModel.Prod if (categoryChanged) { - var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedCategories), - model.Category, _catalogSettings.MostRecentlyUsedCategoriesMaxSize); + var mru = new TrimmedBuffer( + _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedCategories), + model.Category, + _catalogSettings.MostRecentlyUsedCategoriesMaxSize); _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedCategories, mru.ToString()); } @@ -1664,8 +1667,10 @@ public ActionResult ProductManufacturerInsert(GridCommand command, ProductModel. _manufacturerService.InsertProductManufacturer(productManufacturer); - var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedManufacturers), - model.Manufacturer, _catalogSettings.MostRecentlyUsedManufacturersMaxSize); + var mru = new TrimmedBuffer( + _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedManufacturers), + model.Manufacturer, + _catalogSettings.MostRecentlyUsedManufacturersMaxSize); _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedManufacturers, mru.ToString()); } @@ -1691,8 +1696,10 @@ public ActionResult ProductManufacturerUpdate(GridCommand command, ProductModel. if (manufacturerChanged) { - var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedManufacturers), - model.Manufacturer, _catalogSettings.MostRecentlyUsedManufacturersMaxSize); + var mru = new TrimmedBuffer( + _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedManufacturers), + model.Manufacturer, + _catalogSettings.MostRecentlyUsedManufacturersMaxSize); _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedManufacturers, mru.ToString()); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/QuantityUnitController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/QuantityUnitController.cs index 3649c5522f..6c429e4e48 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/QuantityUnitController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/QuantityUnitController.cs @@ -3,7 +3,6 @@ using System.Web.Mvc; using SmartStore.Admin.Models.Directory; using SmartStore.Core.Domain.Directory; -using SmartStore.Services; using SmartStore.Services.Directory; using SmartStore.Services.Localization; using SmartStore.Services.Security; @@ -17,45 +16,20 @@ namespace SmartStore.Admin.Controllers [AdminAuthorize] public class QuantityUnitController : AdminControllerBase { - #region Fields - private readonly IQuantityUnitService _quantityUnitService; private readonly ILocalizedEntityService _localizedEntityService; private readonly ILanguageService _languageService; - private readonly ICommonServices _services; - - #endregion - - #region Constructors public QuantityUnitController( IQuantityUnitService quantityUnitService, ILocalizedEntityService localizedEntityService, - ILanguageService languageService, - ICommonServices services) + ILanguageService languageService) { _quantityUnitService = quantityUnitService; _localizedEntityService = localizedEntityService; _languageService = languageService; - _services = services; } - #endregion - - #region Utilities - - [NonAction] - public void UpdateLocales(QuantityUnit quantityUnit, QuantityUnitModel model) - { - foreach (var localized in model.Locales) - { - _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.Name, localized.Name, localized.LanguageId); - _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.Description, localized.Description, localized.LanguageId); - } - } - - #endregion - #region Methods public ActionResult Index() @@ -65,20 +39,25 @@ public ActionResult Index() public ActionResult List() { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } - var quantityUnitModel = _quantityUnitService.GetAllQuantityUnits().Select(x => x.ToModel()).ToList(); + var quantityUnitModel = _quantityUnitService.GetAllQuantityUnits() + .Select(x => x.ToModel()) + .ToList(); var gridModel = new GridModel { Data = quantityUnitModel, Total = quantityUnitModel.Count() }; + return View(gridModel); } - //ajax + // Ajax. public ActionResult AllQuantityUnits(string label, int selectedId) { var quantityUnits = _quantityUnitService.GetAllQuantityUnits(); @@ -104,11 +83,12 @@ public ActionResult AllQuantityUnits(string label, int selectedId) public ActionResult Create() { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } var model = new QuantityUnitModel(); - //locales AddLocales(_languageService, model.Locales); return View(model); @@ -117,40 +97,46 @@ public ActionResult Create() [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] public ActionResult Create(QuantityUnitModel model, bool continueEditing) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } if (ModelState.IsValid) { var quantityUnit = model.ToEntity(); _quantityUnitService.InsertQuantityUnit(quantityUnit); - //locales + UpdateLocales(quantityUnit, model); NotifySuccess(T("Admin.Configuration.QuantityUnit.Added")); return continueEditing ? RedirectToAction("Edit", new { id = quantityUnit.Id }) : RedirectToAction("List"); } - //If we got this far, something failed, redisplay form return View(model); } public ActionResult Edit(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } var quantityUnit = _quantityUnitService.GetQuantityUnitById(id); if (quantityUnit == null) + { return RedirectToAction("List"); + } var model = quantityUnit.ToModel(); AddLocales(_languageService, model.Locales, (locale, languageId) => { locale.Name = quantityUnit.GetLocalized(x => x.Name, languageId, false, false); - locale.Description = quantityUnit.GetLocalized(x => x.Description, languageId, false, false); + locale.NamePlural = quantityUnit.GetLocalized(x => x.NamePlural, languageId, false, false); + locale.Description = quantityUnit.GetLocalized(x => x.Description, languageId, false, false); }); return View(model); @@ -159,12 +145,16 @@ public ActionResult Edit(int id) [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] public ActionResult Edit(QuantityUnitModel model, bool continueEditing) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } var quantityUnit = _quantityUnitService.GetQuantityUnitById(model.Id); if (quantityUnit == null) + { return RedirectToAction("List"); + } if (ModelState.IsValid) { @@ -181,39 +171,57 @@ public ActionResult Edit(QuantityUnitModel model, bool continueEditing) return View(model); } + [HttpPost] + public ActionResult Save(FormCollection formValues) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { + return AccessDeniedView(); + } + + return RedirectToAction("List", "QuantityUnit"); + } + [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } var quantityUnit = _quantityUnitService.GetQuantityUnitById(id); if (quantityUnit == null) - //No delivery time found with the specified id + { return RedirectToAction("List"); + } try { - _quantityUnitService.DeleteQuantityUnit(quantityUnit); - NotifySuccess(T("Admin.Configuration.QuantityUnits.Deleted")); + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); return RedirectToAction("List"); } - catch (Exception exc) + catch (Exception ex) { - NotifyError(exc); + NotifyError(ex); return RedirectToAction("Edit", new { id = quantityUnit.Id }); } } - [HttpPost] - public ActionResult Save(FormCollection formValues) - { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); + #endregion - return RedirectToAction("List", "QuantityUnit"); + #region Utilities + + private void UpdateLocales(QuantityUnit quantityUnit, QuantityUnitModel model) + { + foreach (var localized in model.Locales) + { + _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.Name, localized.Name, localized.LanguageId); + _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.NamePlural, localized.NamePlural, localized.LanguageId); + _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.Description, localized.Description, localized.LanguageId); + } } #endregion diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs index 20b1de8c8a..15dced9578 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using SmartStore.Admin.Models.Messages; @@ -191,7 +192,7 @@ public ActionResult Requeue(QueuedEmailModel queuedEmailModel) } [HttpPost, ActionName("Edit"), FormValueRequired("sendnow")] - public ActionResult SendNow(QueuedEmailModel queuedEmailModel) + public async Task SendNow(QueuedEmailModel queuedEmailModel) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageMessageQueue)) return AccessDeniedView(); @@ -200,7 +201,7 @@ public ActionResult SendNow(QueuedEmailModel queuedEmailModel) if (queuedEmail == null) return RedirectToAction("List"); - var result = _queuedEmailService.SendEmail(queuedEmail); + var result = await _queuedEmailService.SendEmailAsync(queuedEmail); if (result) NotifySuccess(_localizationService.GetResource("Admin.Common.TaskSuccessfullyProcessed")); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs index f741ba1a7c..f759a170bb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs @@ -459,7 +459,7 @@ private void DownloadDir(string path) // create zip from temp folder var tempZip = Path.Combine(FileSystemHelper.TempDirTenant(), folder.Name + ".zip"); - FileSystemHelper.Delete(tempZip); + FileSystemHelper.DeleteFile(tempZip); ZipFile.CreateFromDirectory(tempDir, tempZip, CompressionLevel.Fastest, false); @@ -469,7 +469,7 @@ private void DownloadDir(string path) Response.TransmitFile(tempZip); Response.Flush(); - FileSystemHelper.Delete(tempZip); + FileSystemHelper.DeleteFile(tempZip); FileSystemHelper.ClearDirectory(tempDir, true); Response.End(); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/SecurityController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/SecurityController.cs index 097cfd497b..8beda6fa82 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/SecurityController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/SecurityController.cs @@ -12,7 +12,7 @@ namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public class SecurityController : AdminControllerBase { #region Fields @@ -34,10 +34,28 @@ public SecurityController(IWorkContext workContext, this._customerService = customerService; } - #endregion  + #endregion #region Methods + // Ajax. + public ActionResult AllAccessPermissions(string selected) + { + var permissions = _permissionService.GetAllPermissionRecords(); + var selectedArr = selected.SplitSafe(","); + + var data = permissions + .Select(x => new + { + id = x.SystemName, + text = x.Name, // TODO: localization. + selected = selectedArr.Contains(x.SystemName) + }) + .ToList(); + + return new JsonResult { Data = data, JsonRequestBehavior = JsonRequestBehavior.AllowGet }; + } + public ActionResult AccessDenied(string pageUrl) { var currentCustomer = _workContext.CurrentCustomer; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs index f6985819bb..521c96998a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs @@ -24,6 +24,7 @@ using SmartStore.Core.Search; using SmartStore.Core.Search.Facets; using SmartStore.Services; +using SmartStore.Services.Catalog; using SmartStore.Services.Common; using SmartStore.Services.Customers; using SmartStore.Services.Directory; @@ -79,7 +80,7 @@ public partial class SettingController : AdminControllerBase private readonly Lazy _mediaMover; private readonly Lazy _catalogSearchQueryAliasMapper; private readonly Lazy _forumSearchQueryAliasMapper; - private readonly Lazy _siteMapService; + private readonly Lazy _menuService; private StoreDependingSettingHelper _storeDependingSettings; @@ -109,7 +110,7 @@ public SettingController( Lazy mediaMover, Lazy catalogSearchQueryAliasMapper, Lazy forumSearchQueryAliasMapper, - Lazy siteMapService) + Lazy menuService) { _countryService = countryService; _stateProvinceService = stateProvinceService; @@ -132,7 +133,7 @@ public SettingController( _mediaMover = mediaMover; _catalogSearchQueryAliasMapper = catalogSearchQueryAliasMapper; _forumSearchQueryAliasMapper = forumSearchQueryAliasMapper; - _siteMapService = siteMapService; + _menuService = menuService; } #endregion @@ -600,8 +601,7 @@ public ActionResult Catalog(CatalogSettings catalogSettings, CatalogSettingsMode if (catalogSettings.MaxItemsToDisplayInCatalogMenu != model.MaxItemsToDisplayInCatalogMenu) { // Clear cached navigation model. - var siteMap = _siteMapService.Value.GetSiteMap("catalog"); - siteMap.ClearCache(); + _menuService.Value.ClearCache("Main"); } catalogSettings = model.ToEntity(catalogSettings); @@ -1056,6 +1056,8 @@ public ActionResult GeneralCommon() // SEO. var seoSettings = _services.Settings.LoadSetting(storeScope); MiniMapper.Map(seoSettings, model.SeoSettings); + // Fix Disallows joined with comma in MiniMapper (we need NewLine). + model.SeoSettings.ExtraRobotsDisallows = string.Join(Environment.NewLine, seoSettings.ExtraRobotsDisallows); StoreDependingSettings.GetOverrideKeys(seoSettings, model.SeoSettings, storeScope, _services.Settings, false); @@ -1221,120 +1223,135 @@ public ActionResult GeneralCommon(GeneralCommonSettingsModel model, FormCollecti public ActionResult ChangeEnryptionKey(GeneralCommonSettingsModel model) { if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) + { return AccessDeniedView(); + } // Set page timeout to 5 minutes. Server.ScriptTimeout = 300; var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); var securitySettings = _services.Settings.LoadSetting(storeScope); + var oldEncryptionPrivateKey = securitySettings.EncryptionKey; + var newEncryptionPrivateKey = model.SecuritySettings.EncryptionKey.EmptyNull(); - try + if (newEncryptionPrivateKey.IsEmpty() || newEncryptionPrivateKey.Length != 16) { - if (model.SecuritySettings.EncryptionKey == null) - model.SecuritySettings.EncryptionKey = ""; - - model.SecuritySettings.EncryptionKey = model.SecuritySettings.EncryptionKey.Trim(); - - var newEncryptionPrivateKey = model.SecuritySettings.EncryptionKey; - if (newEncryptionPrivateKey.IsEmpty() || newEncryptionPrivateKey.Length != 16) - throw new SmartException(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.TooShort")); + NotifyError(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.TooShort")); + return RedirectToAction("GeneralCommon"); + } - var oldEncryptionPrivateKey = securitySettings.EncryptionKey; - if (oldEncryptionPrivateKey == newEncryptionPrivateKey) - throw new SmartException(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.TheSame")); + if (oldEncryptionPrivateKey == newEncryptionPrivateKey) + { + NotifyError(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.TheSame")); + return RedirectToAction("GeneralCommon"); + } + try + { // Update encrypted order info. - var orders = _orderService.LoadAllOrders(); - foreach (var order in orders) + var orderQuery = _orderService.GetOrders(0, 0, null, null, null, null, null, null, null); + orderQuery = orderQuery.OrderByDescending(x => x.CreatedOnUtc); + IPagedList orders = null; + var pageIndex = 0; + + do { - // New credit card encryption. - string decryptedCardType = _encryptionService.DecryptText(order.CardType, oldEncryptionPrivateKey); - string decryptedCardName = _encryptionService.DecryptText(order.CardName, oldEncryptionPrivateKey); - string decryptedCardNumber = _encryptionService.DecryptText(order.CardNumber, oldEncryptionPrivateKey); - string decryptedMaskedCreditCardNumber = _encryptionService.DecryptText(order.MaskedCreditCardNumber, oldEncryptionPrivateKey); - string decryptedCardCvv2 = _encryptionService.DecryptText(order.CardCvv2, oldEncryptionPrivateKey); - string decryptedCardExpirationMonth = _encryptionService.DecryptText(order.CardExpirationMonth, oldEncryptionPrivateKey); - string decryptedCardExpirationYear = _encryptionService.DecryptText(order.CardExpirationYear, oldEncryptionPrivateKey); - - string encryptedCardType = _encryptionService.EncryptText(decryptedCardType, newEncryptionPrivateKey); - string encryptedCardName = _encryptionService.EncryptText(decryptedCardName, newEncryptionPrivateKey); - string encryptedCardNumber = _encryptionService.EncryptText(decryptedCardNumber, newEncryptionPrivateKey); - string encryptedMaskedCreditCardNumber = _encryptionService.EncryptText(decryptedMaskedCreditCardNumber, newEncryptionPrivateKey); - string encryptedCardCvv2 = _encryptionService.EncryptText(decryptedCardCvv2, newEncryptionPrivateKey); - string encryptedCardExpirationMonth = _encryptionService.EncryptText(decryptedCardExpirationMonth, newEncryptionPrivateKey); - string encryptedCardExpirationYear = _encryptionService.EncryptText(decryptedCardExpirationYear, newEncryptionPrivateKey); - - order.CardType = encryptedCardType; - order.CardName = encryptedCardName; - order.CardNumber = encryptedCardNumber; - order.MaskedCreditCardNumber = encryptedMaskedCreditCardNumber; - order.CardCvv2 = encryptedCardCvv2; - order.CardExpirationMonth = encryptedCardExpirationMonth; - order.CardExpirationYear = encryptedCardExpirationYear; - - // New direct debit encryption. - string decryptedAccountHolder = _encryptionService.DecryptText(order.DirectDebitAccountHolder, oldEncryptionPrivateKey); - string decryptedAccountNumber = _encryptionService.DecryptText(order.DirectDebitAccountNumber, oldEncryptionPrivateKey); - string decryptedBankCode = _encryptionService.DecryptText(order.DirectDebitBankCode, oldEncryptionPrivateKey); - string decryptedBankName = _encryptionService.DecryptText(order.DirectDebitBankName, oldEncryptionPrivateKey); - string decryptedBic = _encryptionService.DecryptText(order.DirectDebitBIC, oldEncryptionPrivateKey); - string decryptedCountry = _encryptionService.DecryptText(order.DirectDebitCountry, oldEncryptionPrivateKey); - string decryptedIban = _encryptionService.DecryptText(order.DirectDebitIban, oldEncryptionPrivateKey); - - string encryptedAccountHolder = _encryptionService.EncryptText(decryptedAccountHolder, newEncryptionPrivateKey); - string encryptedAccountNumber = _encryptionService.EncryptText(decryptedAccountNumber, newEncryptionPrivateKey); - string encryptedBankCode = _encryptionService.EncryptText(decryptedBankCode, newEncryptionPrivateKey); - string encryptedBankName = _encryptionService.EncryptText(decryptedBankName, newEncryptionPrivateKey); - string encryptedBic = _encryptionService.EncryptText(decryptedBic, newEncryptionPrivateKey); - string encryptedCountry = _encryptionService.EncryptText(decryptedCountry, newEncryptionPrivateKey); - string encryptedIban = _encryptionService.EncryptText(decryptedIban, newEncryptionPrivateKey); - - order.DirectDebitAccountHolder = encryptedAccountHolder; - order.DirectDebitAccountNumber = encryptedAccountNumber; - order.DirectDebitBankCode = encryptedBankCode; - order.DirectDebitBankName = encryptedBankName; - order.DirectDebitBIC = encryptedBic; - order.DirectDebitCountry = encryptedCountry; - order.DirectDebitIban = encryptedIban; - - _orderService.UpdateOrder(order); + orders = new PagedList(orderQuery, pageIndex++, 1000); + + foreach (var order in orders) + { + // New credit card encryption. + string decryptedCardType = _encryptionService.DecryptText(order.CardType, oldEncryptionPrivateKey); + string decryptedCardName = _encryptionService.DecryptText(order.CardName, oldEncryptionPrivateKey); + string decryptedCardNumber = _encryptionService.DecryptText(order.CardNumber, oldEncryptionPrivateKey); + string decryptedMaskedCreditCardNumber = _encryptionService.DecryptText(order.MaskedCreditCardNumber, oldEncryptionPrivateKey); + string decryptedCardCvv2 = _encryptionService.DecryptText(order.CardCvv2, oldEncryptionPrivateKey); + string decryptedCardExpirationMonth = _encryptionService.DecryptText(order.CardExpirationMonth, oldEncryptionPrivateKey); + string decryptedCardExpirationYear = _encryptionService.DecryptText(order.CardExpirationYear, oldEncryptionPrivateKey); + + string encryptedCardType = _encryptionService.EncryptText(decryptedCardType, newEncryptionPrivateKey); + string encryptedCardName = _encryptionService.EncryptText(decryptedCardName, newEncryptionPrivateKey); + string encryptedCardNumber = _encryptionService.EncryptText(decryptedCardNumber, newEncryptionPrivateKey); + string encryptedMaskedCreditCardNumber = _encryptionService.EncryptText(decryptedMaskedCreditCardNumber, newEncryptionPrivateKey); + string encryptedCardCvv2 = _encryptionService.EncryptText(decryptedCardCvv2, newEncryptionPrivateKey); + string encryptedCardExpirationMonth = _encryptionService.EncryptText(decryptedCardExpirationMonth, newEncryptionPrivateKey); + string encryptedCardExpirationYear = _encryptionService.EncryptText(decryptedCardExpirationYear, newEncryptionPrivateKey); + + order.CardType = encryptedCardType; + order.CardName = encryptedCardName; + order.CardNumber = encryptedCardNumber; + order.MaskedCreditCardNumber = encryptedMaskedCreditCardNumber; + order.CardCvv2 = encryptedCardCvv2; + order.CardExpirationMonth = encryptedCardExpirationMonth; + order.CardExpirationYear = encryptedCardExpirationYear; + + // New direct debit encryption. + string decryptedAccountHolder = _encryptionService.DecryptText(order.DirectDebitAccountHolder, oldEncryptionPrivateKey); + string decryptedAccountNumber = _encryptionService.DecryptText(order.DirectDebitAccountNumber, oldEncryptionPrivateKey); + string decryptedBankCode = _encryptionService.DecryptText(order.DirectDebitBankCode, oldEncryptionPrivateKey); + string decryptedBankName = _encryptionService.DecryptText(order.DirectDebitBankName, oldEncryptionPrivateKey); + string decryptedBic = _encryptionService.DecryptText(order.DirectDebitBIC, oldEncryptionPrivateKey); + string decryptedCountry = _encryptionService.DecryptText(order.DirectDebitCountry, oldEncryptionPrivateKey); + string decryptedIban = _encryptionService.DecryptText(order.DirectDebitIban, oldEncryptionPrivateKey); + + string encryptedAccountHolder = _encryptionService.EncryptText(decryptedAccountHolder, newEncryptionPrivateKey); + string encryptedAccountNumber = _encryptionService.EncryptText(decryptedAccountNumber, newEncryptionPrivateKey); + string encryptedBankCode = _encryptionService.EncryptText(decryptedBankCode, newEncryptionPrivateKey); + string encryptedBankName = _encryptionService.EncryptText(decryptedBankName, newEncryptionPrivateKey); + string encryptedBic = _encryptionService.EncryptText(decryptedBic, newEncryptionPrivateKey); + string encryptedCountry = _encryptionService.EncryptText(decryptedCountry, newEncryptionPrivateKey); + string encryptedIban = _encryptionService.EncryptText(decryptedIban, newEncryptionPrivateKey); + + order.DirectDebitAccountHolder = encryptedAccountHolder; + order.DirectDebitAccountNumber = encryptedAccountNumber; + order.DirectDebitBankCode = encryptedBankCode; + order.DirectDebitBankName = encryptedBankName; + order.DirectDebitBIC = encryptedBic; + order.DirectDebitCountry = encryptedCountry; + order.DirectDebitIban = encryptedIban; + + _orderService.UpdateOrder(order); + } } + while (orders.HasNextPage); - // Update user information. - // Optimization - load only users with PasswordFormat.Encrypted. - - var query = _customerService.GetAllCustomersByPasswordFormat(PasswordFormat.Encrypted).SourceQuery; + // Update user information. + var customerQuery = _customerService.GetAllCustomersByPasswordFormat(PasswordFormat.Encrypted).SourceQuery; + PagedList customers = null; + pageIndex = 0; - for (var pageIndex = 0; pageIndex < 9999999; ++pageIndex) - { - var customers = new PagedList(query, pageIndex, 1000); - - foreach (var customer in customers) - { - string decryptedPassword = _encryptionService.DecryptText(customer.Password, oldEncryptionPrivateKey); - string encryptedPassword = _encryptionService.EncryptText(decryptedPassword, newEncryptionPrivateKey); + do + { + customers = new PagedList(customerQuery, pageIndex++, 1000); - customer.Password = encryptedPassword; - } + if (customers.Any()) + { + foreach (var customer in customers) + { + var decryptedPassword = _encryptionService.DecryptText(customer.Password, oldEncryptionPrivateKey); + var encryptedPassword = _encryptionService.EncryptText(decryptedPassword, newEncryptionPrivateKey); - Services.DbContext.SaveChanges(); + customer.Password = encryptedPassword; + } - if (!customers.HasNextPage) - break; - } + Services.DbContext.SaveChanges(); + } + } + while (customers.HasNextPage); securitySettings.EncryptionKey = newEncryptionPrivateKey; _services.Settings.SaveSetting(securitySettings); NotifySuccess(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.Changed")); } - catch (Exception exc) + catch (Exception ex) { - NotifyError(exc); + NotifyError(ex); + Logger.Error(ex); } - return RedirectToAction("GeneralCommon"); + return RedirectToAction("GeneralCommon"); } [HttpPost] @@ -1554,7 +1571,7 @@ public ActionResult Search(SearchSettingsModel model, FormCollection form) return AccessDeniedView(); } - var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); var settings = Services.Settings.LoadSetting(storeScope); var fsettings = Services.Settings.LoadSetting(storeScope); @@ -1579,6 +1596,10 @@ public ActionResult Search(SearchSettingsModel model, FormCollection form) return Search(); } + CategoryTreeChangeReason? categoriesChange = model.AvailabilityFacet.IncludeNotAvailable != settings.IncludeNotAvailable + ? CategoryTreeChangeReason.ElementCounts + : (CategoryTreeChangeReason?)null; + ModelState.Clear(); MiniMapper.Map(model, settings); MiniMapper.Map(model.ForumSearchSettings, fsettings); @@ -1653,11 +1674,17 @@ public ActionResult Search(SearchSettingsModel model, FormCollection form) { _catalogSearchQueryAliasMapper.Value.ClearCommonFacetCache(); } + if (clearForumFacetCache) { _forumSearchQueryAliasMapper.Value.ClearCommonFacetCache(); } + if (categoriesChange.HasValue) + { + Services.EventPublisher.Publish(new CategoryTreeChangedEvent(categoriesChange.Value)); + } + return NotifyAndRedirect("Search"); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs index c8f64aee7c..b4f0844db8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs @@ -497,18 +497,14 @@ public ActionResult OptionDelete(int optionId, int specificationAttributeId, Gri return OptionList(specificationAttributeId, command); } - //ajax + // Ajax. [AcceptVerbs(HttpVerbs.Get)] - public ActionResult GetOptionsByAttributeId(string attributeId) + public ActionResult GetOptionsByAttributeId(int attributeId) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) return AccessDeniedView(); - // This action method gets called via an ajax request - if (string.IsNullOrEmpty(attributeId)) - throw new ArgumentNullException("attributeId"); - - var options = _specificationAttributeService.GetSpecificationAttributeOptionsBySpecificationAttribute(Convert.ToInt32(attributeId)); + var options = _specificationAttributeService.GetSpecificationAttributeOptionsBySpecificationAttribute(attributeId); var result = from o in options select new { id = o.Id, name = o.Name, text = o.Name }; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs index 84b773c057..bf5b9fb78b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs @@ -223,7 +223,8 @@ public ActionResult Create() } [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] - public ActionResult Create(TopicModel model, bool continueEditing) + [ValidateInput(false)] + public ActionResult Create(TopicModel model, bool continueEditing, FormCollection form) { if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageTopics)) return AccessDeniedView(); @@ -251,6 +252,8 @@ public ActionResult Create(TopicModel model, bool continueEditing) SaveAclMappings(topic, model); UpdateLocales(topic, model); + Services.EventPublisher.Publish(new ModelBoundEvent(model, topic, form)); + NotifySuccess(T("Admin.ContentManagement.Topics.Added")); return continueEditing ? RedirectToAction("Edit", new { id = topic.Id }) : RedirectToAction("List"); } @@ -351,16 +354,20 @@ public ActionResult Edit(TopicModel model, bool continueEditing, FormCollection public ActionResult DeleteConfirmed(int id) { if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageTopics)) + { return AccessDeniedView(); + } var topic = _topicService.GetTopicById(id); if (topic == null) - return RedirectToAction("List"); + { + return HttpNotFound(); + } if (topic.IsSystemTopic) { NotifyError(T("Admin.ContentManagement.Topics.CannotBeDeleted")); - return RedirectToAction("List"); + return RedirectToAction("Edit", new { id = topic.Id }); } _topicService.DeleteTopic(topic); @@ -370,7 +377,7 @@ public ActionResult DeleteConfirmed(int id) } // AJAX - public ActionResult AllTopics(string label, int selectedId, bool useTitles = false, bool includeWidgets = false) + public ActionResult AllTopics(string label, int selectedId, bool useTitles = false, bool includeWidgets = false, bool includeHomePage = false) { var query = from x in _topicService.GetAllTopics(showHidden: true).SourceQuery where (includeWidgets || !x.RenderAsWidget) @@ -399,6 +406,16 @@ public ActionResult AllTopics(string label, int selectedId, bool useTitles = fal topics.Insert(0, labelTopic); } + if (includeHomePage) + { + topics.Insert(0, new Topic + { + Id = -10, + Title = T("Admin.ContentManagement.Homepage"), + SystemName = T("Admin.ContentManagement.Homepage"), + }); + } + var list = from x in topics select new { diff --git a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminMenu.cs b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminMenu.cs new file mode 100644 index 0000000000..a39edb757e --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminMenu.cs @@ -0,0 +1,99 @@ +using System; +using SmartStore.Collections; +using SmartStore.Web.Framework.UI; + +namespace SmartStore.Admin.Infrastructure +{ + public partial class AdminMenu : MenuBase + { + public override string Name => "Admin"; + + public override bool ApplyPermissions => true; + + protected override string GetCacheKey() + { + var cacheKey = "{0}-{1}".FormatInvariant( + Services.WorkContext.WorkingLanguage.Id, + Services.WorkContext.CurrentCustomer.GetRolesIdent()); + + return cacheKey; + } + + protected override TreeNode Build() + { + var xmlSiteMap = new Telerik.Web.Mvc.XmlSiteMap(); + xmlSiteMap.LoadFrom("~/Administration/sitemap.config"); + + var rootNode = ConvertSitemapNodeToMenuItemNode(xmlSiteMap.RootNode); + return rootNode; + } + + protected virtual TreeNode ConvertSitemapNodeToMenuItemNode(Telerik.Web.Mvc.SiteMapNode node) + { + var item = new MenuItem(); + var root = new TreeNode(item); + + var id = node.Attributes.ContainsKey("id") + ? node.Attributes["id"] as string + : string.Empty; + + root.Id = id; + + if (node.RouteName.HasValue()) + { + item.RouteName = node.RouteName; + } + else if (node.ActionName.HasValue() && node.ControllerName.HasValue()) + { + item.ActionName = node.ActionName; + item.ControllerName = node.ControllerName; + } + else if (node.Url.HasValue()) + { + item.Url = node.Url; + } + item.RouteValues = node.RouteValues; + + item.Id = id; + item.Visible = node.Visible; + item.Text = node.Title; + item.Attributes.Merge(node.Attributes); + + if (node.Attributes.ContainsKey("permissionNames")) + { + item.PermissionNames = node.Attributes["permissionNames"] as string; + } + + if (node.Attributes.ContainsKey("resKey")) + { + item.ResKey = node.Attributes["resKey"] as string; + } + + if (node.Attributes.ContainsKey("iconClass")) + { + item.Icon = node.Attributes["iconClass"] as string; + } + + if (node.Attributes.ContainsKey("imageUrl")) + { + item.ImageUrl = node.Attributes["imageUrl"] as string; + } + + if (node.Attributes.ContainsKey("isGroupHeader")) + { + item.IsGroupHeader = Boolean.Parse(node.Attributes["isGroupHeader"] as string); + } + + // Iterate children recursively. + foreach (var childNode in node.ChildNodes) + { + var childTreeNode = ConvertSitemapNodeToMenuItemNode(childNode); + root.Append(childTreeNode); + } + + Services.EventPublisher.Publish(new MenuBuiltEvent(Name, root)); + + return root; + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminSiteMap.cs b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminSiteMap.cs deleted file mode 100644 index 7595a4d965..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminSiteMap.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using SmartStore.Collections; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Logging; -using SmartStore.Services.Catalog; -using SmartStore.Services.Media; -using SmartStore.Services.Search; -using SmartStore.Web.Framework.UI; - -namespace SmartStore.Admin.Infrastructure -{ - public class AdminSiteMap : SiteMapBase - { - private static object s_lock = new object(); - - private readonly ICategoryService _categoryService; - private readonly IPictureService _pictureService; - private readonly CatalogSettings _catalogSettings; - private readonly ICatalogSearchService _catalogSearchService; - - public AdminSiteMap( - ICategoryService categoryService, - IPictureService pictureService, - CatalogSettings catalogSettings, - ICatalogSearchService catalogSearchService) - { - _categoryService = categoryService; - _pictureService = pictureService; - _catalogSettings = catalogSettings; - _catalogSearchService = catalogSearchService; - - Logger = NullLogger.Instance; - } - - public ILogger Logger { get; set; } - - public override string Name - { - get { return "admin"; } - } - - public override bool ApplyPermissions - { - get { return true; } - } - - protected override string GetCacheKey() - { - string cacheKey = "{0}-{1}".FormatInvariant( - Services.WorkContext.WorkingLanguage.Id, - Services.WorkContext.CurrentCustomer.GetRolesIdent()); - - return cacheKey; - } - - protected override TreeNode Build() - { - var xmlSiteMap = new Telerik.Web.Mvc.XmlSiteMap(); - xmlSiteMap.LoadFrom("~/Administration/sitemap.config"); - - var rootNode = ConvertSitemapNodeToMenuItemNode(xmlSiteMap.RootNode); - - return rootNode; - } - - private TreeNode ConvertSitemapNodeToMenuItemNode(Telerik.Web.Mvc.SiteMapNode node) - { - var item = new MenuItem(); - var treeNode = new TreeNode(item); - - string id = null; - if (node.Attributes.ContainsKey("id")) - id = node.Attributes["id"] as string; - - treeNode.Id = id; - - if (node.RouteName.HasValue()) - { - item.RouteName = node.RouteName; - } - else if (node.ActionName.HasValue() && node.ControllerName.HasValue()) - { - item.ActionName = node.ActionName; - item.ControllerName = node.ControllerName; - } - else if (node.Url.HasValue()) - { - item.Url = node.Url; - } - item.RouteValues = node.RouteValues; - - item.Id = id; - item.Visible = node.Visible; - item.Text = node.Title; - item.Attributes.Merge(node.Attributes); - - if (node.Attributes.ContainsKey("permissionNames")) - item.PermissionNames = node.Attributes["permissionNames"] as string; - - if (node.Attributes.ContainsKey("resKey")) - item.ResKey = node.Attributes["resKey"] as string; - - if (node.Attributes.ContainsKey("iconClass")) - item.Icon = node.Attributes["iconClass"] as string; - - if (node.Attributes.ContainsKey("imageUrl")) - item.ImageUrl = node.Attributes["imageUrl"] as string; - - if (node.Attributes.ContainsKey("isGroupHeader")) - item.IsGroupHeader = Boolean.Parse(node.Attributes["isGroupHeader"] as string); - - // Iterate children recursively - foreach (var childNode in node.ChildNodes) - { - var childTreeNode = ConvertSitemapNodeToMenuItemNode(childNode); - treeNode.Append(childTreeNode); - } - - return treeNode; - } - } -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs index a6c12464b7..f8ff4c2c9e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs @@ -39,6 +39,10 @@ public CategoryModel() [AllowHtml] public string BottomDescription { get; set; } + [SmartResourceDisplayName("Admin.Catalog.Categories.Fields.ExternalLink")] + [AllowHtml, UIHint("Link")] + public string ExternalLink { get; set; } + [SmartResourceDisplayName("Admin.Catalog.Categories.Fields.BadgeText")] [AllowHtml] public string BadgeText { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Directory/QuantityUnitModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Directory/QuantityUnitModel.cs index f44da303ce..f87e50dae0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Directory/QuantityUnitModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Directory/QuantityUnitModel.cs @@ -16,7 +16,10 @@ public QuantityUnitModel() [SmartResourceDisplayName("Admin.Configuration.QuantityUnit.Fields.Name")] public string Name { get; set; } - [SmartResourceDisplayName("Common.Description")] + [SmartResourceDisplayName("Admin.Configuration.QuantityUnit.Fields.NamePlural")] + public string NamePlural { get; set; } + + [SmartResourceDisplayName("Common.Description")] [AllowHtml] public string Description { get; set; } @@ -36,7 +39,10 @@ public class QuantityUnitLocalizedModel : ILocalizedModelLocal [SmartResourceDisplayName("Admin.Configuration.QuantityUnit.Fields.Name")] public string Name { get; set; } - [SmartResourceDisplayName("Common.Description")] + [SmartResourceDisplayName("Admin.Configuration.QuantityUnit.Fields.NamePlural")] + public string NamePlural { get; set; } + + [SmartResourceDisplayName("Common.Description")] public string Description { get; set; } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuItemRecordModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuItemRecordModel.cs new file mode 100644 index 0000000000..9c033c24c7 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuItemRecordModel.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; +using FluentValidation; +using FluentValidation.Attributes; +using Newtonsoft.Json; +using SmartStore.Collections; +using SmartStore.Core.Localization; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Localization; +using SmartStore.Web.Framework.Modelling; +using SmartStore.Web.Framework.UI; + +namespace SmartStore.Admin.Models.Menus +{ + [Validator(typeof(MenuItemRecordValidator))] + public class MenuItemRecordModel : TabbableModel, IIcon, ILocalizedModel + { + public int MenuId { get; set; } + public string Model { get; set; } + public bool ProviderAppendsMultipleItems { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.ParentItem")] + public int? ParentItemId { get; set; } + public IList AllItems { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.LinkTarget")] + public string ProviderName { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Title")] + public string Title { get; set; } + public string TitlePlaceholder { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.ShortDescription")] + public string ShortDescription { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.PermissionNames")] + [UIHint("AccessPermissions")] + public string[] PermissionNames { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.Published")] + public bool Published { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.DisplayOrder")] + public int DisplayOrder { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.BeginGroup")] + public bool BeginGroup { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.ShowExpanded")] + public bool ShowExpanded { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.NoFollow")] + public bool NoFollow { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.NewWindow")] + public bool NewWindow { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.Icon")] + public string Icon { get; set; } + + public string Style { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.HtmlId")] + public string HtmlId { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.CssClass")] + public string CssClass { get; set; } + + public IList Locales { get; set; } + } + + + public class MenuItemRecordLocalizedModel : ILocalizedModelLocal + { + public int LanguageId { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Title")] + public string Title { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Item.ShortDescription")] + public string ShortDescription { get; set; } + } + + + public partial class MenuItemRecordValidator : AbstractValidator + { + public MenuItemRecordValidator(Localizer T) + { + RuleFor(x => x.ProviderName).NotEmpty(); + + RuleFor(x => x.Model) + .Must(x => + { + try + { + if (x.HasValue()) + { + var node = new TreeNode(new MenuItem()); + node.ApplyRouteData(x); + + var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); + var result = node.Value.GenerateUrl(urlHelper); + + return result.HasValue(); + } + } + catch { } + + return true; + }) + .When(x => x.ProviderName.IsCaseInsensitiveEqual("route")) + .WithMessage(T("Admin.ContentManagement.Menus.Item.InvalidRouteValues")); + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuRecordListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuRecordListModel.cs new file mode 100644 index 0000000000..cfb9520676 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuRecordListModel.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.Admin.Models.Menus +{ + public class MenuRecordListModel : ModelBase + { + [SmartResourceDisplayName("Admin.Common.Store.SearchFor")] + public int StoreId { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.SystemName")] + public string SystemName { get; set; } + + public IList AvailableStores { get; set; } + public int GridPageSize { get; set; } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuRecordModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuRecordModel.cs new file mode 100644 index 0000000000..d37a76bb3e --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuRecordModel.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; +using FluentValidation; +using FluentValidation.Attributes; +using SmartStore.Collections; +using SmartStore.Core.Localization; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Localization; +using SmartStore.Web.Framework.Modelling; +using SmartStore.Web.Framework.UI; + +namespace SmartStore.Admin.Models.Menus +{ + [Validator(typeof(MenuRecordValidator))] + public class MenuRecordModel : TabbableModel, ILocalizedModel, IStoreSelector, IAclSelector + { + [SmartResourceDisplayName("Admin.ContentManagement.Menus.SystemName")] + public string SystemName { get; set; } + + public bool IsSystemMenu { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Template")] + public string Template { get; set; } + public bool IsCustomTemplate { get; set; } + public IList AllTemplates { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.WidgetZone")] + [UIHint("WidgetZone")] + public string[] WidgetZone { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Title")] + public string Title { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Published")] + public bool Published { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.DisplayOrder")] + public int DisplayOrder { get; set; } + + // Store mapping. + [SmartResourceDisplayName("Admin.Common.Store.LimitedTo")] + public bool LimitedToStores { get; set; } + public IEnumerable AvailableStores { get; set; } + public int[] SelectedStoreIds { get; set; } + + // ACL. + public bool SubjectToAcl { get; set; } + public IEnumerable AvailableCustomerRoles { get; set; } + public int[] SelectedCustomerRoleIds { get; set; } + + public IList Locales { get; set; } + public IList AllProviders { get; set; } + public TreeNode ItemTree { get; set; } + } + + + public class MenuRecordLocalizedModel : ILocalizedModelLocal + { + public int LanguageId { get; set; } + + [SmartResourceDisplayName("Admin.ContentManagement.Menus.Title")] + public string Title { get; set; } + } + + public partial class MenuRecordValidator : AbstractValidator + { + public MenuRecordValidator(Localizer T) + { + RuleFor(x => x.SystemName).NotEmpty(); + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Messages/MessageTemplateModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Messages/MessageTemplateModel.cs index aef4792494..90cffa4419 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Messages/MessageTemplateModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Messages/MessageTemplateModel.cs @@ -12,7 +12,7 @@ namespace SmartStore.Admin.Models.Messages { [Validator(typeof(MessageTemplateValidator))] - public class MessageTemplateModel : EntityModelBase, ILocalizedModel, IStoreSelector + public class MessageTemplateModel : TabbableModel, ILocalizedModel, IStoreSelector { public MessageTemplateModel() { diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Messages/QueuedEmailModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Messages/QueuedEmailModel.cs index dff6e45f88..ce4275bf0a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Messages/QueuedEmailModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Messages/QueuedEmailModel.cs @@ -80,10 +80,10 @@ public partial class QueuedEmailValidator : AbstractValidator { public QueuedEmailValidator() { - RuleFor(x => x.Priority).NotEmpty().InclusiveBetween(0, 99999); + RuleFor(x => x.Priority).InclusiveBetween(0, 99999); RuleFor(x => x.From).NotEmpty(); RuleFor(x => x.To).NotEmpty(); - RuleFor(x => x.SentTries).NotEmpty().InclusiveBetween(0, 99999); + RuleFor(x => x.SentTries).InclusiveBetween(0, 99999); } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderListModel.cs index ce42e552ae..3ea76d69b4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderListModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderListModel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; @@ -15,6 +14,7 @@ public OrderListModel() AvailablePaymentStatuses = new List(); AvailableShippingStatuses = new List(); AvailableStores = new List(); + AvailablePaymentMethods = new List(); } [SmartResourceDisplayName("Admin.Orders.List.StartDate")] @@ -42,6 +42,9 @@ public OrderListModel() [SmartResourceDisplayName("Admin.Common.Store.SearchFor")] public int StoreId { get; set; } + [SmartResourceDisplayName("Order.PaymentMethod")] + public string PaymentMethods { get; set; } + [SmartResourceDisplayName("Admin.Orders.List.OrderGuid")] [AllowHtml] public string OrderGuid { get; set; } @@ -60,5 +63,6 @@ public OrderListModel() public IList AvailablePaymentStatuses { get; set; } public IList AvailableShippingStatuses { get; set; } public IList AvailableStores { get; set; } + public IList AvailablePaymentMethods { get; set; } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/GeneralCommonSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/GeneralCommonSettingsModel.cs index c8ea851499..0ac83811f0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/GeneralCommonSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/GeneralCommonSettingsModel.cs @@ -323,9 +323,6 @@ public partial class SocialSettingsModel [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.SocialSettings.FacebookLink")] public string FacebookLink { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.SocialSettings.GooglePlusLink")] - public string GooglePlusLink { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.SocialSettings.TwitterLink")] public string TwitterLink { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Scripts/_references.js b/src/Presentation/SmartStore.Web/Administration/Scripts/_references.js index a941beba51..6d51960f98 100644 Binary files a/src/Presentation/SmartStore.Web/Administration/Scripts/_references.js and b/src/Presentation/SmartStore.Web/Administration/Scripts/_references.js differ diff --git a/src/Presentation/SmartStore.Web/Administration/Scripts/admin.globalinit.js b/src/Presentation/SmartStore.Web/Administration/Scripts/admin.globalinit.js index bf57a8181b..71d0efd6c1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Scripts/admin.globalinit.js +++ b/src/Presentation/SmartStore.Web/Administration/Scripts/admin.globalinit.js @@ -136,6 +136,7 @@ if (sectionHeaderHasButtons === true) { var y = $(this).scrollTop(); sectionHeader.toggleClass("sticky", y >= navbarHeight); + $(document.body).toggleClass("sticky-header", y >= navbarHeight); } }).trigger('resize'); } diff --git a/src/Presentation/SmartStore.Web/Administration/Scripts/smartstore.discountrules.js b/src/Presentation/SmartStore.Web/Administration/Scripts/smartstore.discountrules.js index d2a28c3cc2..bece81d6c1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Scripts/smartstore.discountrules.js +++ b/src/Presentation/SmartStore.Web/Administration/Scripts/smartstore.discountrules.js @@ -1,5 +1,3 @@ -; - // Unobtrusive handler for inline discount rule partials (function ($, window, document, undefined) { diff --git a/src/Presentation/SmartStore.Web/Administration/Scripts/smartstore.iconchooser.js b/src/Presentation/SmartStore.Web/Administration/Scripts/smartstore.iconchooser.js new file mode 100644 index 0000000000..26e5388c0a --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Scripts/smartstore.iconchooser.js @@ -0,0 +1,110 @@ +$(function () { + $('.fa-icon-chooser').each(function (i, select) { + var map = {}; + $(select).data('icons', map); + + // Add icon data of selected icon (intial option) to the map very early + var selectedOption = $(select).find('option[data-icon][selected]'); + if (selectedOption.length) { + var icon = selectedOption.data('icon'); + map[icon.id] = icon; + } + + $(select).select2({ + escapeMarkup: function (markup) { return markup; }, // let our custom formatter work + minimumInputLength: 0, + allowClear: true, + templateResult: formatIcon, + templateSelection: formatIconSelection, + tags: true, + createTag: function (params) { + var term = $.trim(params.term); + + if (term === '') { + return null; + } + + return { + id: term, + text: term, + isCustom: true + }; + }, + ajax: { + url: function (params) { + return $(select).data('explorer-url'); + }, + global: false, + type: "POST", + dataType: 'json', + delay: 250, + data: function (params) { + return { + term: params.term, + selected: $(select).val(), + page: params.page + }; + }, + processResults: function (data, params) { + params.page = params.page || 1; + + return { + results: data.results, + pagination: data.pagination + }; + }, + cache: true + } + }); + + function formatIconSelection(icon, el) { + return formatIcon(icon, el, true); + } + + function formatIcon(icon, el, isSelection) { + if (icon.loading || !icon.id) + return icon.text; + + var html = ['']; + + var variants = []; + var iconClass = ''; + + if (icon.element && $(icon.element).data('icon')) { + // Option is pregenerated + icon = $(icon.element).data('icon'); + } + + map[icon.id] = icon; + + if (icon.isCustom) { + // Has been set in "createTag" function or server-side (if IsPro == true) + html.push(''); + } + else { + if (icon.isBrandIcon) { + variants.push("fab fa-" + icon.text); + } + else if (icon.hasRegularStyle) { + variants.push("fas fa-" + icon.text); + variants.push("far fa-" + icon.text); + } + else { + variants.push("fas fa-" + icon.text); + } + + var len = (isSelection ? 0 : 2) || variants.length; + + for (i = 0; i < len; i++) { + iconClass = (i < variants.length ? variants[i] + " " : "far ") + "fa-fw mr-2 fs-h6"; + html.push(''); + } + } + + html.push(icon.text); + html.push(''); + + return $(html.join('')); + } + }); +}); \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj index a00c53e1c6..44b280e563 100644 --- a/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj +++ b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj @@ -1,5 +1,6 @@  - + + Debug @@ -186,6 +187,7 @@ + @@ -230,7 +232,7 @@ - + @@ -270,6 +272,9 @@ + + + @@ -504,6 +509,7 @@ + @@ -582,6 +588,20 @@ + + + + + + + + + + + + + + @@ -600,7 +620,6 @@ - Designer diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Blog/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Blog/List.cshtml index 1301a3b84e..9375955cd7 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Blog/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Blog/List.cshtml @@ -8,7 +8,7 @@ }
          - + @T("Admin.ContentManagement.Blog.BlogPosts")
          diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml index 7a0d9bfd6b..a5953500e0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml @@ -32,6 +32,62 @@ @Html.ValidationMessageFor(model => model.Id) + + + @Html.SmartLabelFor(model => model.ExternalLink) + + + @Html.EditorFor(model => model.ExternalLink) + @Html.ValidationMessageFor(model => model.ExternalLink) + + + + + @Html.SmartLabelFor(model => model.ParentCategoryId) + + + @Html.DropDownListFor(model => model.ParentCategoryId, + Model.ParentCategoryId == null ? Enumerable.Empty() : new List { + new SelectListItem { Text = Model.ParentCategoryBreadcrumb, Value = Model.ParentCategoryId.ToString() } + }, + new + { + placeholder = "[" + T("Common.Unspecified") + "]", + data_select = true, + data_select_lazy = true, + data_select_init_text = Model.ParentCategoryBreadcrumb, + data_select_url = Url.Action("Allcategories", "Category", new { selectedId = Model.ParentCategoryId }) + }) + @Html.ValidationMessageFor(model => model.ParentCategoryId) + + + + + @Html.SmartLabelFor(model => model.DisplayOrder) + + + @Html.EditorFor(model => model.DisplayOrder) + @Html.ValidationMessageFor(model => model.DisplayOrder) + + + + + @Html.SmartLabelFor(model => model.Published) + + + @Html.EditorFor(x => x.Published) + @Html.ValidationMessageFor(model => model.Published) + + + + + @Html.SmartLabelFor(model => model.ShowOnHomePage) + + + @Html.EditorFor(x => x.ShowOnHomePage) + @Html.ValidationMessageFor(model => model.ShowOnHomePage) + + } @@ -175,51 +231,7 @@ - - @Html.SmartLabelFor(model => model.ParentCategoryId) - - - @Html.DropDownListFor(model => model.ParentCategoryId, - Model.ParentCategoryId == null ? Enumerable.Empty() : new List { - new SelectListItem { Text = Model.ParentCategoryBreadcrumb, Value = Model.ParentCategoryId.ToString() } - }, - new - { - placeholder = "[" + T("Common.Unspecified") + "]", - data_select = true, - data_select_lazy = true, - data_select_init_text = Model.ParentCategoryBreadcrumb, - data_select_url = Url.Action("Allcategories", "Category", new { selectedId = Model.ParentCategoryId }) - }) - @Html.ValidationMessageFor(model => model.ParentCategoryId) - - - - - @Html.SmartLabelFor(model => model.ShowOnHomePage) - - - @Html.EditorFor(x => x.ShowOnHomePage) - @Html.ValidationMessageFor(model => model.ShowOnHomePage) - - - - - @Html.SmartLabelFor(model => model.Published) - - - @Html.EditorFor(x => x.Published) - @Html.ValidationMessageFor(model => model.Published) - - - - - @Html.SmartLabelFor(model => model.DisplayOrder) - - - @Html.EditorFor(model => model.DisplayOrder) - @Html.ValidationMessageFor(model => model.DisplayOrder) - +
          @@ -257,6 +269,9 @@ @Html.ValidationMessageFor(model => model.PageSizeOptions) + +
          + @Html.SmartLabelFor(model => model.Alias) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml index accc2b0fff..ba662e30c9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml @@ -54,7 +54,7 @@ @Html.SmartLabelFor(model => model.Password) - @Html.EditorFor(model => model.Password) + @Html.EditorFor(model => model.Password, null, new { htmlAttributes = new { value = Model.Password } }) @Html.ValidationMessageFor(model => model.Password) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Home/About.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Home/About.cshtml index 56aa0998af..60802bf5ed 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Home/About.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Home/About.cshtml @@ -24,9 +24,9 @@

          Version @SmartStore.Core.SmartStoreVersion.CurrentFullVersion
          - Copyright © 1999-2018 SmartStore AG
          + Copyright © 1999-2019 SmartStore AG
          www.smartstore.com, - github.com/smartstoreag/SmartStoreNET + github.com/smartstore/SmartStoreNET

          @@ -98,15 +98,6 @@

          @MSPLLicense() -
        • -
          - DotNetZip -
          - - @MSPLLicense() -
        • Entity Framework diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml index 1d667bc4c1..3aab2a436c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml @@ -336,10 +336,10 @@ { foreach (var discount in Model.AvailableDiscounts) { -
          - - +
          + +
          } } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/Create.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/Create.cshtml new file mode 100644 index 0000000000..f8ee52bcf9 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/Create.cshtml @@ -0,0 +1,24 @@ +@using SmartStore.Admin.Models.Menus +@model MenuRecordModel +@{ + ViewBag.Title = T("Admin.ContentManagement.AddMenu"); +} +@using (Html.BeginForm()) +{ +
          +
          + @T("Admin.ContentManagement.AddMenu") @Html.ActionLink("(" + T("Admin.Common.BackToList") + ")", "List") +
          +
          + + +
          +
          + + Html.RenderPartial("_CreateOrUpdate", Model); +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/CreateItem.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/CreateItem.cshtml new file mode 100644 index 0000000000..8e14c9db4a --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/CreateItem.cshtml @@ -0,0 +1,24 @@ +@using SmartStore.Admin.Models.Menus +@model MenuItemRecordModel +@{ + ViewBag.Title = T("Admin.ContentManagement.AddMenuItem"); +} +@using (Html.BeginForm()) +{ +
          +
          + @T("Admin.ContentManagement.AddMenuItem") @Html.ActionLink("(" + T("Common.Back") + ")", "Edit", new { id = Model.MenuId }) +
          +
          + + +
          +
          + + Html.RenderPartial("_CreateOrUpdate.Item", Model); +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/Edit.cshtml new file mode 100644 index 0000000000..6fc34412ae --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/Edit.cshtml @@ -0,0 +1,29 @@ +@using SmartStore.Admin.Models.Menus +@model MenuRecordModel +@{ + ViewBag.Title = T("Admin.ContentManagement.EditMenu"); +} +@using (Html.BeginForm()) +{ +
          +
          + @T("Admin.ContentManagement.EditMenu") - @Model.Title @Html.ActionLink("(" + T("Admin.Common.BackToList") + ")", "List") +
          +
          + + + +
          +
          + + Html.RenderPartial("_CreateOrUpdate", Model); +} +@Html.DeleteConfirmation("delete-menu") \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditItem.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditItem.cshtml new file mode 100644 index 0000000000..9ee8504d4e --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditItem.cshtml @@ -0,0 +1,29 @@ +@using SmartStore.Admin.Models.Menus +@model MenuItemRecordModel +@{ + ViewBag.Title = T("Admin.ContentManagement.EditMenuItem"); +} +@using (Html.BeginForm()) +{ +
          +
          + @T("Admin.ContentManagement.EditMenuItem") - @Model.Title @Html.ActionLink("(" + T("Common.Back") + ")", "Edit", new { id = Model.MenuId }) +
          +
          + + + +
          +
          + + Html.RenderPartial("_CreateOrUpdate.Item", Model); +} +@Html.DeleteConfirmation("DeleteItem", "delete-item") \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditorTemplates/MenuItem.Catalog.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditorTemplates/MenuItem.Catalog.cshtml new file mode 100644 index 0000000000..c4b8b1c091 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditorTemplates/MenuItem.Catalog.cshtml @@ -0,0 +1,7 @@ +@model string + +@{ +
          + @T("Admin.ContentManagement.Menus.CatalogNote") +
          +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditorTemplates/MenuItem.Entity.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditorTemplates/MenuItem.Entity.cshtml new file mode 100644 index 0000000000..5ba17b6a93 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditorTemplates/MenuItem.Entity.cshtml @@ -0,0 +1,5 @@ +@model string + +@{ + Html.RenderPartial("~/Views/Shared/EditorTemplates/Link.cshtml"); +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditorTemplates/MenuItem.Route.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditorTemplates/MenuItem.Route.cshtml new file mode 100644 index 0000000000..5a8991a0d6 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/EditorTemplates/MenuItem.Route.cshtml @@ -0,0 +1,57 @@ +@using Newtonsoft.Json + +@model string + +@{ + var rnd = CommonHelper.GenerateRandomInteger(); + var id = ViewData.TemplateInfo.GetFullHtmlFieldId(string.Empty); + var routeNameFieldId = string.Concat("RouteName", rnd); + var routeValuesFieldId = string.Concat("RouteValues", rnd); + var routeName = string.Empty; + var routeValuesStr = string.Empty; + var rawValue = ViewData.Model != null ? Convert.ToString(ViewData.Model) : null; + + try + { + if (rawValue.HasValue()) + { + var routeValues = JsonConvert.DeserializeObject(rawValue); + object val; + if (routeValues.TryGetValue("routename", out val)) + { + routeName = val as string; + routeValues.Remove("routename"); + } + + routeValuesStr = JsonConvert.SerializeObject(routeValues).EmptyNull(); + if (routeValuesStr == "{}") + { + routeValuesStr = string.Empty; + } + } + } + catch { } +} + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/ItemList.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/ItemList.cshtml new file mode 100644 index 0000000000..3145879640 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/ItemList.cshtml @@ -0,0 +1,91 @@ +@model MenuRecordModel + +@using SmartStore.Collections +@using SmartStore.Admin.Models.Menus + +@if (Model.ItemTree.HasChildren) +{ +
          + @foreach (var item in Model.ItemTree.Children) + { + @ItemGroup(item, 0, 42) + } +
          +} +else +{ +
          + @T("Admin.ContentManagement.Menus.NoMenuItemsAvailable") +
          +} + +@helper ItemGroup(TreeNode node, int level, int padding) +{ + var item = node.Value; + var parentId = node.Parent != null ? node.Parent.Value.EntityId : 0; + var indentStyle = " style=\"padding-left: {0}px;\"".FormatInvariant(padding); + var rtl = WorkContext.WorkingLanguage.Rtl; + + + if (node.HasChildren) + { +
          + @foreach (var child in node.Children) + { + @ItemGroup(child, level + 1, padding + 40); + } +
          + } +} diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/List.cshtml new file mode 100644 index 0000000000..05cb423676 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/List.cshtml @@ -0,0 +1,88 @@ +@using SmartStore.Admin.Models.Menus +@model MenuRecordListModel +@{ + ViewBag.Title = T("Admin.ContentManagement.Menus").Text; +} + +
          +
          + + @T("Admin.ContentManagement.Menus") +
          + +
          + +
          + @if (Model.AvailableStores.Count > 1) + { +
          + @Html.SmartLabelFor(x => x.StoreId) + @Html.DropDownList("StoreId", Model.AvailableStores, T("Admin.Common.All")) +
          + } +
          + @Html.SmartLabelFor(x => x.SystemName) + @Html.TextBoxFor(x => x.SystemName, new { @class = "form-control" }) +
          + +
          + @Html.SmartLabel(string.Empty, string.Empty) + +
          +
          + +
          + @(Html.Telerik().Grid() + .Name("menus-grid") + .Columns(columns => + { + columns.Bound(x => x.SystemName) + .Width(280) + .ClientTemplate("<#= SystemName #>"); + columns.Bound(x => x.Title); + columns.Bound(x => x.Published) + .ClientTemplate(@Html.SymbolForBool("Published")) + .Centered(); + columns.Bound(x => x.LimitedToStores) + .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) + .Hidden(Model.AvailableStores.Count <= 1) + .Centered(); + }) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "Menu")) + .ClientEvents(events => events.OnDataBinding("onDataBinding").OnRowDataBound("onRowDataBound")) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .PreserveGridState() + .EnableCustomBinding(true)) +
          + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/_CreateOrUpdate.Item.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/_CreateOrUpdate.Item.cshtml new file mode 100644 index 0000000000..3c69e4d353 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/_CreateOrUpdate.Item.cshtml @@ -0,0 +1,183 @@ +@using SmartStore.Admin.Models.Menus +@model MenuItemRecordModel + +@Html.ValidationSummary(false) +@Html.HiddenFor(model => model.Id) +@Html.HiddenFor(model => model.MenuId) +@Html.HiddenFor(model => model.ProviderName) + +@Html.SmartStore().TabStrip().Name("menu-item-edit").Style(TabsStyle.Material).Items(x => +{ + x.Add().Text(T("Admin.Common.General").Text).Content(TabGeneral()).Selected(true); + + EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "menu-item-edit", this.Html, this.Model)); +}) + +@helper TabGeneral() +{ + +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/_CreateOrUpdate.cshtml new file mode 100644 index 0000000000..6491340a76 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/_CreateOrUpdate.cshtml @@ -0,0 +1,179 @@ +@using SmartStore.Admin.Models.Menus +@model MenuRecordModel + +@Html.HiddenFor(model => model.Id) +@Html.HiddenFor(model => model.IsSystemMenu) +@Html.ValidationSummary(false) + +@Html.SmartStore().TabStrip().Name("menu-edit").Style(TabsStyle.Material).Items(x => +{ + x.Add().Text(T("Admin.Common.General").Text).Content(TabGeneral()).Selected(true); + x.Add().Text(T("Admin.Catalog.Categories.Acl").Text).Content(TabAcl()); + x.Add().Text(T("Admin.Common.Stores").Text).Content(TabStores()); + + EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "menu-edit", this.Html, this.Model)); +}) + +@helper TabGeneral() +{ +
          +
          +
          + @Html.SmartLabelFor(model => model.SystemName) +
          +
          + @Html.TextBoxFor(model => model.SystemName, Model.IsSystemMenu ? new { @readonly = "readonly" } : null ) + @Html.ValidationMessageFor(model => model.SystemName) +
          +
          +
          +
          + @Html.SmartLabelFor(model => model.Published) +
          +
          + @Html.CheckBoxFor(model => model.Published, Model.IsSystemMenu ? new { @readonly = "readonly" } : null ) + @Html.ValidationMessageFor(model => model.Published) +
          +
          +
          +
          + @Html.SmartLabelFor(model => model.Template) +
          +
          + + @Html.ValidationMessageFor(model => model.Template) +
          +
          +
          +
          + @Html.SmartLabelFor(model => model.WidgetZone) +
          +
          + @Html.EditorFor(model => model.WidgetZone) + @Html.ValidationMessageFor(model => model.WidgetZone) +
          +
          +
          +
          + @Html.SmartLabelFor(model => model.DisplayOrder) +
          +
          + @Html.EditorFor(model => model.DisplayOrder) + @Html.ValidationMessageFor(model => model.DisplayOrder) +
          +
          +
          + + @(Html.LocalizedEditor("menu-info-localized", + @
          +
          +
          + @Html.SmartLabelFor(model => model.Locales[item].Title) +
          +
          + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + + @Html.EditorFor(model => model.Locales[item].Title) + @Html.ValidationMessageFor(model => model.Locales[item].Title) +
          +
          +
          + , + @
          +
          +
          + @Html.SmartLabelFor(model => model.Title) +
          +
          + @Html.EditorFor(model => model.Title) + @Html.ValidationMessageFor(model => model.Title) +
          +
          +
          + )) + + if (Model.Id > 0) + { +

          @T("Admin.ContentManagement.MenuLinks")

          + + + } +} + +@helper TabStores() +{ + @Html.Partial("StoreSelector", Model) +} + +@helper TabAcl() +{ + @Html.Partial("AclSelector", Model) +} + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/Edit.cshtml index 94d40249ad..1b4d9ae4c9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/Edit.cshtml @@ -7,7 +7,7 @@ {
          - + @T("Admin.ContentManagement.MessageTemplates.EditMessageTemplateDetails") - @Model.Name @Html.ActionLink("(" + T("Admin.ContentManagement.MessageTemplates.BackToList") + ")", "List")
          diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/AddProductToOrderDetails.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/AddProductToOrderDetails.cshtml index 06364f7900..21b5b1ab87 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/AddProductToOrderDetails.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/AddProductToOrderDetails.cshtml @@ -6,7 +6,7 @@ } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml index 100b11a8d0..667d9cc779 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml @@ -74,6 +74,10 @@ @Html.SmartLabelFor(model => model.OrderNumber) @Html.EditorFor(model => Model.OrderNumber)
          +
          + @Html.SmartLabelFor(model => model.PaymentMethods) + @Html.DropDownList("PaymentMethods", Model.AvailablePaymentMethods, null, new { multiple = "multiple", @class = "form-control" }) +
          @if (Model.AvailableStores.Count > 1) {
          @@ -85,7 +89,6 @@ {
          } -
          @Html.SmartLabelFor(model => model.OrderStatusIds) @Html.DropDownList("OrderStatusIds", Model.AvailableOrderStatuses, null, new { multiple = "multiple", @class = "form-control" }) @@ -252,7 +255,8 @@ ShippingStatusIds: jQuery.makeArray($('#@Html.FieldIdFor(model => model.ShippingStatusIds)').val()).join(), StoreId: $('#@Html.FieldIdFor(model => model.StoreId)').val(), OrderGuid: $('#@Html.FieldIdFor(model => model.OrderGuid)').val(), - OrderNumber: $('#@Html.FieldIdFor(model => model.OrderNumber)').val() + OrderNumber: $('#@Html.FieldIdFor(model => model.OrderNumber)').val(), + PaymentMethods: jQuery.makeArray($('#@Html.FieldIdFor(model => model.PaymentMethods)').val()).join(), }; e.data = searchModel; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml index 48ee154e8d..35af7ea85a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml @@ -36,7 +36,7 @@ @Html.EditorFor(model => model.TrackingNumber) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.BillingAndShipment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.BillingAndShipment.cshtml index 9b53e5d2cb..2e47d7e3d5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.BillingAndShipment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.BillingAndShipment.cshtml @@ -10,7 +10,7 @@
          @@ -25,7 +25,7 @@
          diff --git a/src/Presentation/SmartStore.Web/Views/Customer/Login.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/Login.cshtml index b3eb12e9e5..f3b08a336a 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/Login.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/Login.cshtml @@ -14,7 +14,7 @@ if (!String.IsNullOrEmpty(this.Context.Request.QueryString["returnUrl"])) { var webHelper = EngineContext.Current.Resolve(); - registerUrl = webHelper.ModifyQueryString(registerUrl, "returnurl=" + HttpUtility.UrlEncode(this.Context.Request.QueryString["returnUrl"]), null); + registerUrl = webHelper.ModifyQueryString(registerUrl, "returnUrl=" + HttpUtility.UrlEncode(this.Context.Request.QueryString["returnUrl"]), null); } } diff --git a/src/Presentation/SmartStore.Web/Views/Customer/Partials/MyAccountMenu.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/Partials/MyAccountMenu.cshtml deleted file mode 100644 index 93182bb528..0000000000 --- a/src/Presentation/SmartStore.Web/Views/Customer/Partials/MyAccountMenu.cshtml +++ /dev/null @@ -1,36 +0,0 @@ -@model MyAccountMenuModel - -@using SmartStore.Web.Models.Customer; - -@{ - var selItem = Model.SelectedItemToken != null ? Model.SelectedItemToken.ToLowerInvariant() : ""; -} - -@{ Html.RenderWidget("myaccount_menu_before"); } - -
          -
          - @T("Account.Navigation") -
          -
          - @{ Html.RenderWidget("myaccount_menu_firstitem_before"); } - - @foreach (var item in Model.Root.Children.Where(x => x.Value.Visible).Select(x => x.Value)) - { - - - @item.Text - @if (item.BadgeText.HasValue()) - { - @Html.Raw(item.BadgeText) - } - - } - - @{ Html.RenderWidget("myaccount_menu_lastitem_after"); } -
          -
          - -@{ Html.RenderWidget("myaccount_menu_after"); } - -@{ Html.RenderAction("PollBlock", "Poll", new { systemKeyword = "MyAccountMenu" }); } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Customer/Partials/_ExternalAuthentication.AssociateMessage.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/Partials/_ExternalAuthentication.AssociateMessage.cshtml index 114a61018f..b13753a592 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/Partials/_ExternalAuthentication.AssociateMessage.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/Partials/_ExternalAuthentication.AssociateMessage.cshtml @@ -8,7 +8,7 @@ {
          @T("Account.AssociatedExternalAuth.YourAccountWillBeLinkedTo") @openAuthParameters.ExternalIdentifier - + @T("Common.Remove")
          diff --git a/src/Presentation/SmartStore.Web/Views/Customer/Register.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/Register.cshtml index 71dc25cf24..0d93aa0f10 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/Register.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/Register.cshtml @@ -224,7 +224,7 @@
          @Html.EditorFor(model => model.Newsletter, new { @class = "form-check-input" })
          diff --git a/src/Presentation/SmartStore.Web/Views/Install/Index.cshtml b/src/Presentation/SmartStore.Web/Views/Install/Index.cshtml index 99678ef59c..f62b146d5f 100644 --- a/src/Presentation/SmartStore.Web/Views/Install/Index.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Install/Index.cshtml @@ -29,7 +29,7 @@ @* scripts (head) *@ - +