diff --git a/README.md b/README.md index d3402dc825..a7a83aee3a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping 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/net](http://www.smartstore.com/net) +* **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) * **Documentation:** [SmartStore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home) diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets index 4fb755fcf5..f184eaa7eb 100644 --- a/SmartStoreNET.Tasks.Targets +++ b/SmartStoreNET.Tasks.Targets @@ -44,7 +44,7 @@ x86 $(BUILD_NUMBER) - 2.5.0 + 2.6.0 $(StageFolder) .$(Version) diff --git a/build.bat b/build.bat index 2cee9dc2b5..b58175809a 100644 --- a/build.bat +++ b/build.bat @@ -5,7 +5,11 @@ FOR %%b in ( "%VS120COMNTOOLS%..\..\VC\vcvarsall.bat" "%ProgramFiles(x86)%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" - "%ProgramFiles%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" + "%ProgramFiles%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" + + "%VS110COMNTOOLS%..\..\VC\vcvarsall.bat" + "%ProgramFiles(x86)%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" + "%ProgramFiles%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" ) do ( if exist %%b ( call %%b x86 diff --git a/changelog.md b/changelog.md index 8aba6eb6f6..0dac88a9fd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,21 +1,69 @@ # Release Notes -## SmartStore.NET 2.5.1 +## SmartStore.NET 2.6 + +### Highlights +* Major improvements in Importer & Exporter: better field mapping, higher performance, bug fixes etc. +* 'PayPal PLUS' payment plugin +* 'paydirekt' payment plugin +* 'Viveum' payment plugin +* 'BeezUP' export provider +* (Dev) Publishing SmartStore.Web from within Visual Studio now deploys the project correctly. No need to execute ClickToBuild.cmd anymore. Just publish directly to any target, including Azure. ### New Features +* #961 Fix "Open Redirection Vulnerability" * #571 Option to display another checkbox on confirm page to let the customer accept that his email address can be handed over to a third party * #870 Implement address import for customers (both billing & shipping address) * #886 Add setting to hide manufacturer images on product detail page and to hide default image for manufacturers +* Import localized SEO names for product and categories +* #477 Implement option to specify the number of exported and imported pictures +* #859 Make checkout attributes suitable for multi-stores +* Product details: Select attribute and gift card values by query string parameters +* #950 make contact form comply with current German law ### Improvements +* Major improvements in Importer: better field mapping, higher performance, bug fixes etc. +* (Dev) Publishing SmartStore.Web from within Visual Studio now deploys the project correctly. No need to execute ClickToBuild.cmd anymore. Just publish directly to any target, including Azure. +* Localization & SEO: language switcher now takes language specific SEO slugs into account when building links +* Smarter import of plugin resource files with graceful fallbacks (de-DE > de > de-* > en-US > en > en-* > *) +* (Perf) Faster language resource file import * Exports the product detail link including the attribute query string when exporting attribute combinations +* #918 Compare products: Display base price information +* Export email attachments needs to be stored in database because the temp file may not exist anymore when sending the email +* #913 Use HTML5 Input types (tel, email) +* Added paging to frontend order list +* Added paging to backend checkout attribute list +* #977 Show PAngV base/delivery amount also +* Updated LiveEngage LiveChat plugin ### Bugfixes +* TaskScheduler could fail polling when primary store url is an external IP address * Fixed ajax cache issue when saving payment or shipping restrictions. Internet Explorer showed the old data state (before storage). * "The provider failed at the Execute method: Member 'CurrentValues' cannot be called for the entity of type 'Product'" when exporting product attribute combinations * Bundles without selected attributes could throw an exception on product detail page -* GMC feed did not export the product type (category path) +* GMC feed did not export the product type and Billiger did not export shop_cat (category path) * The error message of a payment provider when capturing a payment was not displayed +* Adding new shipping method threw an exception +* Attribute Values: Assigning IsPreselected to more than one value causes an error +* BizImporter: fixed redirection bug when default file extension in .biz wasn't .html +* Fixed: Export deployment emails were always send manually +* Manually notifying gift card recipient threw an exception +* Loading shipping by weight grid failed under SQL CE +* #949 Import: ProcessSlugs does not process explicitly specified "SeName", if product name did not change in an update scenario +* Customer import: Creates customer role duplicates for inserted customers +* GMC feed does not generate the sale price if the sale price is set for a future date +* Mobile devices: Fixed "Uncaught Error: Syntax error, unrecognized expression: :nth-child" +* Download nor sample download were removable when editing product +* Copied product must not share sample download of source product. Could produce "The DELETE statement conflicted with the REFERENCE constraint 'FK_dbo.Product_dbo.Download_SampleDownloadId'". +* #921 Specification attribute options with single quotation marks are causing a Javascript error +* #971 Product is added to cart automatically if it has a non-required file upload attribute +* #973 Bundle item upload is nowhere linked +* Base price in product list ignored PriceDisplayType (catalog settings) and possibly displayed the wrong base price info +* Private messages: Fixes "No route in the route table matches the supplied values" +* Payone: Hash string incorrect for frontend API payments where the order has more than 9 products +* Export mail notification: Download link not working if SSL is enabled +* Discount rule has spent amount including sub total option can cause wrong discount calculation if the cart contains a product several times +* #986 File uploads possible through /content/filemanager/index.html ## SmartStore.NET 2.5 diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll index 51e391ce23..8acdf39c24 100644 Binary files a/lib/SmartStore.Licensing/SmartStore.Licensing.dll and b/lib/SmartStore.Licensing/SmartStore.Licensing.dll differ diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs index 67335b18c1..f74f0edf78 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("2.5.0.0")] +[assembly: AssemblyVersion("2.6.0.0")] -[assembly: AssemblyFileVersion("2.5.0.0")] -[assembly: AssemblyInformationalVersion("2.5.0.0")] +[assembly: AssemblyFileVersion("2.6.0.0")] +[assembly: AssemblyInformationalVersion("2.6.0.0")] diff --git a/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs b/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs index 66b12e67f8..789c363da0 100644 --- a/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs +++ b/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs @@ -99,7 +99,7 @@ public virtual void Collect(IEnumerable keys) } /// - /// Collect single key for combinde loading + /// Collect single key for combined loading /// /// Data key public virtual void Collect(int key) diff --git a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs index e1c4a7569a..ed034c1730 100644 --- a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs @@ -10,22 +10,6 @@ namespace SmartStore.Core.Data public static class RepositoryExtensions { - - public static IEnumerable LoadAll(this IRepository rs) where T : BaseEntity - { - return rs.Table.AsEnumerable(); - } - - public static IEnumerable Where(this IRepository rs, Func predicate) where T : BaseEntity - { - return rs.Table.Where(predicate); - } - - public static T GetSingle(this IRepository rs, Func predicate) where T : BaseEntity - { - return rs.Table.SingleOrDefault(predicate); - } - public static T GetFirst(this IRepository rs, Func predicate) where T : BaseEntity { return rs.Table.FirstOrDefault(predicate); diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs index 50fd39fe5a..999f66bad3 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs @@ -191,6 +191,7 @@ public string Sku /// Gets or sets the manufacturer part number /// [DataMember] + [Index] public string ManufacturerPartNumber { [DebuggerStepThrough] @@ -208,6 +209,7 @@ public string ManufacturerPartNumber /// Gets or sets the Global Trade Item Number (GTIN). These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books). /// [DataMember] + [Index] public string Gtin { [DebuggerStepThrough] diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs index d0dbf678e5..e6d7f56f56 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs @@ -24,6 +24,7 @@ public CustomerSettings() NewsletterEnabled = true; OnlineCustomerMinutes = 20; StoreLastVisitedPage = true; + DisplayPrivacyAgreementOnContactUs = false; } /// @@ -151,6 +152,11 @@ public CustomerSettings() /// public bool StoreLastVisitedPage { get; set; } + /// + /// Gets or sets a value indicating whether to display a checkbox to the customer where he can agree to privacy terms + /// + public bool DisplayPrivacyAgreementOnContactUs { get; set; } + #region Form fields /// diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs index 3b8190d6a4..b9e323c670 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs @@ -21,14 +21,9 @@ public class ExportDeployment : BaseEntity, ICloneable public bool Enabled { get; set; } /// - /// Whether to deploy in a folder that can be reached from the internet + /// XML with information about the last deployment result /// - public bool IsPublic { get; set; } - - /// - /// Whether to create a zip archive with the content of the export - /// - public bool CreateZip { get; set; } + public string ResultInfo { get; set; } /// /// The deployment type identifier @@ -84,6 +79,11 @@ public ExportHttpTransmissionType HttpTransmissionType /// public string FileSystemPath { get; set; } + /// + /// Path of a subfolder + /// + public string SubFolder { get; set; } + /// /// Multiple email addresses can be separated by commas /// @@ -117,14 +117,13 @@ public ExportDeployment Clone() { Name = this.Name, Enabled = this.Enabled, - IsPublic = this.IsPublic, - CreateZip = this.CreateZip, DeploymentTypeId = this.DeploymentTypeId, Username = this.Username, Password = this.Password, Url = this.Url, HttpTransmissionTypeId = this.HttpTransmissionTypeId, FileSystemPath = this.FileSystemPath, + SubFolder = this.SubFolder, EmailAddresses = this.EmailAddresses, EmailSubject = this.EmailSubject, EmailAccountId = this.EmailAccountId, diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs index 49e6991353..6ea68ec73c 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs @@ -23,7 +23,8 @@ public enum ExportDeploymentType FileSystem = 0, Email, Http, - Ftp + Ftp, + PublicFolder } /// @@ -125,7 +126,12 @@ public enum ExportFeatures /// /// Whether to not automatically send a completion email /// - CanOmitCompletionMail = 1 << 12 + CanOmitCompletionMail = 1 << 12, + + /// + /// Whether to provide additional data of attribute combinations + /// + UsesAttributeCombination = 1 << 13 } /// diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs index b49d8d76d1..3161d1ea94 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs @@ -23,7 +23,7 @@ public ExportProfile() public string Name { get; set; } /// - /// The name of the folder (file system) + /// The root path of the export folder /// public string FolderName { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs index b642e3d50d..1139496b4a 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs @@ -85,6 +85,11 @@ public ImportEntityType EntityType /// public string FileTypeConfiguration { get; set; } + /// + /// XML with extra data + /// + public string ExtraData { get; set; } + /// /// Mapping of import columns /// diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs index cee3019aa8..b3072c0912 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs @@ -18,7 +18,7 @@ public MediaSettings() CartThumbPictureSize = 80; CartThumbBundleItemPictureSize = 32; MiniCartThumbPictureSize = 32; - AutoCompleteSearchThumbPictureSize = 20; + VariantValueThumbPictureSize = 20; MaximumImageSize = 1280; DefaultPictureZoomEnabled = true; PictureZoomType = "window"; @@ -38,7 +38,7 @@ public MediaSettings() public int CartThumbPictureSize { get; set; } public int CartThumbBundleItemPictureSize { get; set; } public int MiniCartThumbPictureSize { get; set; } - public int AutoCompleteSearchThumbPictureSize { get; set; } + public int VariantValueThumbPictureSize { get; set; } public bool DefaultPictureZoomEnabled { get; set; } public string PictureZoomType { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs index a317879baa..63cbf58ea8 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Orders { - /// - /// Represents a checkout attribute - /// - public partial class CheckoutAttribute : BaseEntity, ILocalizedEntity - { + /// + /// Represents a checkout attribute + /// + public partial class CheckoutAttribute : BaseEntity, ILocalizedEntity, IStoreMappingSupported + { private ICollection _checkoutAttributeValues; public CheckoutAttribute() @@ -60,11 +61,16 @@ public CheckoutAttribute() /// Gets or sets whether the checkout attribute is active /// public bool IsActive { get; set; } - - /// - /// Gets the attribute control type - /// - public AttributeControlType AttributeControlType + + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + public bool LimitedToStores { get; set; } + + /// + /// Gets the attribute control type + /// + public AttributeControlType AttributeControlType { get { @@ -75,6 +81,7 @@ public AttributeControlType AttributeControlType this.AttributeControlTypeId = (int)value; } } + /// /// Gets the checkout attribute values /// diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs b/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs index d59871a751..9db5f14733 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs @@ -451,7 +451,7 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat public int? RewardPointsRemaining { get; set; } /// - /// Gets or sets a value indicating whether a new payment notification (IPN) arrived + /// Gets or sets a value indicating whether a new payment notification arrived (IPN, webhook, callback etc.) /// [DataMember] public bool HasNewPaymentNotification { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs b/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs index 996271de79..05e9a49fd4 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Orders @@ -16,6 +15,7 @@ public OrderSettings() ReturnRequestReasons = "Received Wrong Product,Wrong Product Ordered,There Was A Problem With The Product"; NumberOfDaysReturnRequestAvailable = 365; MinimumOrderPlacementInterval = 30; + OrderListPageSize = 10; } /// @@ -87,5 +87,10 @@ public OrderSettings() /// Gets or sets a value indicating whether to display all orders of all stores to a customer /// public bool DisplayOrdersOfAllStores { get; set; } + + /// + /// Page size of the order list + /// + public int OrderListPageSize { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Email/SmtpContext.cs b/src/Libraries/SmartStore.Core/Email/SmtpContext.cs index 796e642109..054487610f 100644 --- a/src/Libraries/SmartStore.Core/Email/SmtpContext.cs +++ b/src/Libraries/SmartStore.Core/Email/SmtpContext.cs @@ -73,6 +73,8 @@ public SmtpClient ToSmtpClient() smtpClient.UseDefaultCredentials = this.UseDefaultCredentials; smtpClient.EnableSsl = this.EnableSsl; + smtpClient.Timeout = 10000; + if (this.UseDefaultCredentials) { smtpClient.Credentials = CredentialCache.DefaultNetworkCredentials; diff --git a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs index c83270e71f..033c5d4230 100644 --- a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs @@ -24,7 +24,7 @@ public static void Dump(this Exception exception) public static string ToAllMessages(this Exception exception) { var sb = new StringBuilder(); - + while (exception != null) { if (!sb.ToString().EmptyNull().Contains(exception.Message)) diff --git a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs index 5a172fc17f..9131705215 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs @@ -490,16 +490,19 @@ public static string[] SplitSafe(this string value, string separator) /// true: success, false: failure [DebuggerStepThrough] [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] - public static bool SplitToPair(this string value, out string strLeft, out string strRight, string delimiter) { + public static bool SplitToPair(this string value, out string strLeft, out string strRight, string delimiter) + { int idx = -1; - if (value.IsEmpty() || delimiter.IsEmpty() || (idx = value.IndexOf(delimiter)) == -1) + if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(delimiter) || (idx = value.IndexOf(delimiter)) == -1) { strLeft = value; strRight = ""; return false; } + strLeft = value.Substring(0, idx); strRight = value.Substring(idx + delimiter.Length); + return true; } diff --git a/src/Libraries/SmartStore.Core/IWebHelper.cs b/src/Libraries/SmartStore.Core/IWebHelper.cs index 03b4f128c8..2d089bd64d 100644 --- a/src/Libraries/SmartStore.Core/IWebHelper.cs +++ b/src/Libraries/SmartStore.Core/IWebHelper.cs @@ -13,11 +13,21 @@ public partial interface IWebHelper /// URL referrer string GetUrlReferrer(); - /// - /// Get context IP address - /// - /// URL referrer - string GetCurrentIpAddress(); + /// + /// Gets a unique client identifier + /// + /// A unique identifier + /// + /// The client identifier is a hashed combination of client ip address and user agent. + /// This method returns null if IP or user agent (or both) cannot be determined. + /// + string GetClientIdent(); + + /// + /// Get context IP address + /// + /// URL referrer + string GetCurrentIpAddress(); /// /// Gets this page name diff --git a/src/Libraries/SmartStore.Core/IWorkContext.cs b/src/Libraries/SmartStore.Core/IWorkContext.cs index d3a54cf702..e137ac98bd 100644 --- a/src/Libraries/SmartStore.Core/IWorkContext.cs +++ b/src/Libraries/SmartStore.Core/IWorkContext.cs @@ -59,10 +59,5 @@ public interface IWorkContext /// Gets or sets a value indicating whether we're in admin area /// bool IsAdmin { get; set; } - - ///// - ///// Gets a value indicating whether we're in the public shop - ///// - //bool IsPublic { get; } - } + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs index 7b1bbc9292..f39bbeedeb 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs @@ -95,7 +95,7 @@ public object ResolveUnregistered(Type type, ILifetimeScope scope = null) } } - throw new SmartException("No contructor was found that had all the dependencies satisfied."); + throw new SmartException("No constructor for {0} was found that had all the dependencies satisfied.".FormatInvariant(type.Name.NaIfEmpty())); } private bool TryResolveAll(Type[] types, out object[] instances, ILifetimeScope scope = null) diff --git a/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs b/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs index e0baba6444..fb8a10be35 100644 --- a/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs +++ b/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs @@ -47,7 +47,6 @@ public static void ErrorsAll(this ILogger logger, Exception exception, Customer while (exception != null) { FilteredLog(logger, LogLevel.Error, exception.Message, exception.StackTrace, exception, customer); - exception = exception.InnerException; } } @@ -62,7 +61,7 @@ private static void FilteredLog(ILogger logger, LogLevel level, string message, { if (exception != null && fullMessage.IsEmpty()) { - fullMessage = exception.StackTrace; + fullMessage = "{0}\n{1}\n{2}".FormatCurrent(exception.Message, new String('-', 20), exception.StackTrace); } logger.InsertLog(level, message, fullMessage, customer); diff --git a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs index 3b8f7a4657..593a39ddb0 100644 --- a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs @@ -30,6 +30,8 @@ public TraceLogger(string fileName) console.Filter = new EventTypeFilter(SourceLevels.All); console.Name = "console"; + _traceSource.Listeners.Add(console); + var textListener = new TextWriterTraceListener(fileName); textListener.Filter = new EventTypeFilter(SourceLevels.All); textListener.TraceOutputOptions = TraceOptions.DateTime; @@ -41,17 +43,19 @@ public TraceLogger(string fileName) _streamWriter = new StreamWriter(fileName, append, Encoding.UTF8); textListener.Writer = _streamWriter; + + _traceSource.Listeners.Add(textListener); + } + catch (IOException) + { + // file is locked by another process } - catch (IOException) { } - _traceSource.Listeners.Add(console); - _traceSource.Listeners.Add(textListener); - - // Allow the trace source to send messages to - // listeners for all event types. Currently only + // Allow the trace source to send messages to + // listeners for all event types. Currently only // error messages or higher go to the listeners. - // Messages must get past the source switch to - // get to the listeners, regardless of the settings + // Messages must get past the source switch to + // get to the listeners, regardless of the settings // for the listeners. _traceSource.Switch.Level = SourceLevels.All; } diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index 7aba1fc589..a14afb2ba4 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -85,9 +85,9 @@ ..\..\packages\Microsoft.Web.Xdt.1.0.0\lib\net40\Microsoft.Web.XmlTransform.dll - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True False diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs index df3c302220..38750e3069 100644 --- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs +++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs @@ -23,9 +23,10 @@ public static class SmartStoreVersion new Version("2.5") }; - private const string HELP_BASEURL = "http://docs.smartstore.com/display/SMNET25/"; + //private const string HELP_BASEURL = "http://docs.smartstore.com/display/SMNET25/"; + private const string HELP_BASEURL = "http://docs.smartstore.com/display/"; - static SmartStoreVersion() + static SmartStoreVersion() { s_breakingChangesHistory.Reverse(); @@ -67,9 +68,18 @@ public static Version Version } } - public static string GenerateHelpUrl(string path) + public static string GenerateHelpUrl(string languageCode, string path) { - return HELP_BASEURL + path.Trim().TrimStart('/', '\\'); + return String.Concat( + HELP_BASEURL, + GetUserGuideSpaceKey(languageCode), + "/", + path.EmptyNull().Trim().TrimStart('/', '\\')); + } + + public static string GetUserGuideSpaceKey(string languageCode) + { + return "SMNET25"; } /// diff --git a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs index 09f40c69d9..d3fcef7874 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs @@ -31,14 +31,24 @@ public FileDownloadManager(HttpRequestBase httpRequest) /// The URL to download the file from (either a fully qualified URL or an app relative/absolute path) /// Specifies whether the FormsAuthenticationCookie should be sent /// Timeout in milliseconds - public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false, int? timeout = null) + /// Specifiers whether the file is located on the local server + public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false, int? timeout = null, bool isLocal = false) { Guard.ArgumentNotEmpty(() => url); url = WebHelper.GetAbsoluteUrl(url, _httpRequest); - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = "SmartStore.NET"; + HttpWebRequest req; + + if (isLocal) + { + req = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(url)); + } + else + { + req = WebRequest.CreateHttp(url); + req.UserAgent = "SmartStore.NET"; + } if (timeout.HasValue) { diff --git a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs index 95672ae9a5..3fbb4f83f8 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs @@ -31,6 +31,28 @@ public static string TempDir(string subDirectory = null) return path; } + /// + /// Ensures that path is a valid root path + /// + /// Relative path + /// Valid root path + public static string ValidateRootPath(string path) + { + if (path.HasValue()) + { + path = path.Replace('\\', '/'); + + if (!path.StartsWith("~/")) + { + if (path.StartsWith("~")) + path = path.Substring(1); + + path = (path.StartsWith("/") ? "~" : "~/") + path; + } + } + return path; + } + /// /// Safe way to cleanup the temp directory. Should be called via scheduled task. /// @@ -114,8 +136,10 @@ public static bool Copy(string sourcePath, string destinationPath, bool overwrit /// Source directory /// Target directory /// Whether to override existing files - public static void CopyDirectory(DirectoryInfo source, DirectoryInfo target, bool overwrite = true) + public static bool CopyDirectory(DirectoryInfo source, DirectoryInfo target, bool overwrite = true) { + var result = true; + foreach (FileInfo fi in source.GetFiles()) { try @@ -124,6 +148,7 @@ public static void CopyDirectory(DirectoryInfo source, DirectoryInfo target, boo } catch (Exception exc) { + result = false; exc.Dump(); } } @@ -137,9 +162,12 @@ public static void CopyDirectory(DirectoryInfo source, DirectoryInfo target, boo } catch (Exception exc) { + result = false; exc.Dump(); } } + + return result; } /// diff --git a/src/Libraries/SmartStore.Core/WebHelper.cs b/src/Libraries/SmartStore.Core/WebHelper.cs index b5e9d594bc..edef7863b7 100644 --- a/src/Libraries/SmartStore.Core/WebHelper.cs +++ b/src/Libraries/SmartStore.Core/WebHelper.cs @@ -2,6 +2,7 @@ using System.Configuration; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Web; @@ -11,22 +12,29 @@ using SmartStore.Core.Domain.Stores; using SmartStore.Core.Infrastructure; using SmartStore.Utilities; +using System.Net; +using System.Text; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Net.Sockets; namespace SmartStore.Core { - public partial class WebHelper : IWebHelper { + private static object s_lock = new object(); private static bool? s_optimizedCompilationsEnabled; private static AspNetHostingPermissionLevel? s_trustLevel; private static readonly Regex s_staticExts = new Regex(@"(.*?)\.(css|js|png|jpg|jpeg|gif|bmp|html|htm|xml|pdf|doc|xls|rar|zip|ico|eot|svg|ttf|woff|otf|axd|ashx|less)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex s_htmlPathPattern = new Regex(@"(?<=(?:href|src)=(?:""|'))(?!https?://)(?[^(?:""|')]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); private static readonly Regex s_cssPathPattern = new Regex(@"url\('(?.+)'\)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); + private static ConcurrentDictionary s_safeLocalHostNames = new ConcurrentDictionary(); private readonly HttpContextBase _httpContext; private bool? _isCurrentConnectionSecured; private string _storeHost; private string _storeHostSsl; + private string _ipAddress; private bool? _appPathPossiblyAppended; private bool? _appPathPossiblyAppendedSsl; @@ -49,17 +57,71 @@ public virtual string GetUrlReferrer() return referrerUrl; } + public virtual string GetClientIdent() + { + var ipAddress = this.GetCurrentIpAddress(); + var userAgent = _httpContext.Request != null ? _httpContext.Request.UserAgent : string.Empty; + + if (ipAddress.HasValue() && userAgent.HasValue()) + { + return (ipAddress + userAgent).GetHashCode().ToString(); + } + + return null; + } + public virtual string GetCurrentIpAddress() { + if (_ipAddress != null) + { + return _ipAddress; + } + + if (_httpContext == null && _httpContext.Request == null) + { + return string.Empty; + } + + var vars = _httpContext.Request.ServerVariables; + + var keysToCheck = new string[] + { + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_FORWARDED", + "HTTP_X_CLUSTER_CLIENT_IP", + "HTTP_FORWARDED_FOR", + "HTTP_FORWARDED", + "REMOTE_ADDR", + "HTTP_CF_CONNECTING_IP" + }; + string result = null; - if (_httpContext != null && _httpContext.Request != null) - result = _httpContext.Request.UserHostAddress; + foreach (var key in keysToCheck) + { + var ipString = vars[key]; + if (ipString.HasValue()) + { + 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; + } + } + } if (result == "::1") + { result = "127.0.0.1"; + } - return result.EmptyNull(); + return (_ipAddress = result.EmptyNull()); } public virtual string GetThisPageUrl(bool includeQueryString) @@ -191,12 +253,12 @@ private string GetStoreHost(bool useSsl, out bool appPathPossiblyAppended) if (_currentStore != null) { - var securityMode = _currentStore.GetSecurityMode(useSsl); + var securityMode = _currentStore.GetSecurityMode(); if (httpHost.IsEmpty()) { - //HTTP_HOST variable is not available. - //It's possible only when HttpContext is not available (for example, running in a schedule task) + // HTTP_HOST variable is not available. + // It's possible only when HttpContext is not available (for example, running in a schedule task) result = _currentStore.Url.EnsureEndsWith("/"); appPathPossiblyAppended = true; @@ -598,5 +660,177 @@ public static string GetAbsoluteUrl(string url, HttpRequestBase request) url = string.Format("{0}://{1}{2}", request.Url.Scheme, request.Url.Authority, url); return url; } - } + + public static string GetPublicIPAddress() + { + string result = string.Empty; + + try + { + using (var client = new WebClient()) + { + client.Headers["User-Agent"] = "Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0) (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"; + try + { + byte[] arr = client.DownloadData("http://checkip.amazonaws.com/"); + string response = Encoding.UTF8.GetString(arr); + result = response.Trim(); + } + catch { } + } + } + catch { } + + var checkers = new string[] + { + "https://ipinfo.io/ip", + "https://api.ipify.org", + "https://icanhazip.com", + "https://wtfismyip.com/text", + "http://bot.whatismyipaddress.com/" + }; + + if (string.IsNullOrEmpty(result)) + { + using (var client = new WebClient()) + { + foreach (var checker in checkers) + { + try + { + result = client.DownloadString(checker).Replace("\n", ""); + if (!string.IsNullOrEmpty(result)) + { + break; + } + } + catch { } + } + } + } + + if (string.IsNullOrEmpty(result)) + { + try + { + var url = "http://checkip.dyndns.org"; + var req = WebRequest.Create(url); + using (var resp = req.GetResponse()) + { + using (var sr = new StreamReader(resp.GetResponseStream())) + { + var response = sr.ReadToEnd().Trim(); + var a = response.Split(':'); + var a2 = a[1].Substring(1); + var a3 = a2.Split('<'); + result = a3[0]; + } + } + } + catch { } + } + + return result; + } + + public static HttpWebRequest CreateHttpRequestForSafeLocalCall(Uri requestUri) + { + Guard.ArgumentNotNull(() => requestUri); + + var safeHostName = GetSafeLocalHostName(requestUri); + + var uri = requestUri; + + if (!requestUri.Host.Equals(safeHostName, StringComparison.OrdinalIgnoreCase)) + { + var url = String.Format("{0}://{1}{2}", + requestUri.Scheme, + requestUri.IsDefaultPort ? safeHostName : safeHostName + ":" + requestUri.Port, + requestUri.PathAndQuery); + uri = new Uri(url); + } + + var request = WebRequest.CreateHttp(uri); + request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; + request.ServicePoint.Expect100Continue = false; + request.UserAgent = "SmartStore.NET {0}".FormatInvariant(SmartStoreVersion.CurrentFullVersion); + + return request; + } + + private static string GetSafeLocalHostName(Uri requestUri) + { + return s_safeLocalHostNames.GetOrAdd(requestUri.Port, (port) => + { + // first try original host + if (TestHost(requestUri, requestUri.Host, 5000)) + { + return requestUri.Host; + } + + // try loopback + var hostName = Dns.GetHostName(); + var hosts = new List { "localhost", hostName, "127.0.0.1" }; + foreach (var host in hosts) + { + if (TestHost(requestUri, host, 500)) + { + return host; + } + } + + // try local IP addresses + hosts.Clear(); + var ipAddresses = Dns.GetHostAddresses(hostName).Where(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.ToString()); + hosts.AddRange(ipAddresses); + + foreach (var host in hosts) + { + if (TestHost(requestUri, host, 500)) + { + return host; + } + } + + // None of the hosts are callable. WTF? + return requestUri.Host; + }); + } + + private static bool TestHost(Uri originalUri, string host, int timeout) + { + var url = String.Format("{0}://{1}/taskscheduler/noop", + originalUri.Scheme, + originalUri.IsDefaultPort ? host : host + ":" + originalUri.Port); + var uri = new Uri(url); + + var request = WebRequest.CreateHttp(uri); + request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; + request.ServicePoint.Expect100Continue = false; + request.UserAgent = "SmartStore.NET"; + request.Timeout = timeout; + + HttpWebResponse response = null; + + try + { + response = (HttpWebResponse)request.GetResponse(); + if (response.StatusCode == HttpStatusCode.OK) + { + return true; + } + } + catch + { + // try the next host + } + finally + { + if (response != null) + response.Dispose(); + } + + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/packages.config b/src/Libraries/SmartStore.Core/packages.config index 5765e490f2..61985f795a 100644 --- a/src/Libraries/SmartStore.Core/packages.config +++ b/src/Libraries/SmartStore.Core/packages.config @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/EfRepository.cs b/src/Libraries/SmartStore.Data/EfRepository.cs index 860551f723..5afd5e5d41 100644 --- a/src/Libraries/SmartStore.Data/EfRepository.cs +++ b/src/Libraries/SmartStore.Data/EfRepository.cs @@ -161,7 +161,7 @@ public virtual void UpdateRange(IEnumerable entities) private void SetEntityStateToModifiedIfApplicable(T entity) { var entry = InternalContext.Entry(entity); - if (entry.State == System.Data.Entity.EntityState.Detached || (this.AutoCommitEnabledInternal && !InternalContext.Configuration.AutoDetectChangesEnabled)) + if (entry.State < System.Data.Entity.EntityState.Added || (this.AutoCommitEnabledInternal && !InternalContext.Configuration.AutoDetectChangesEnabled)) { entry.State = System.Data.Entity.EntityState.Modified; } diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs index 0a57740d6e..2c49aa7d7a 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs @@ -11,10 +11,12 @@ public ExportDeploymentMap() this.HasKey(x => x.Id); this.Property(x => x.Name).IsRequired().HasMaxLength(100); + this.Property(x => x.ResultInfo).IsMaxLength(); this.Property(x => x.Username).HasMaxLength(400); this.Property(x => x.Password).HasMaxLength(400); this.Property(x => x.Url).HasMaxLength(4000); this.Property(x => x.FileSystemPath).HasMaxLength(400); + this.Property(x => x.SubFolder).HasMaxLength(400); this.Property(x => x.EmailAddresses).HasMaxLength(4000); this.Property(x => x.EmailSubject).HasMaxLength(400); diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs index 6e5e6758fe..adacc3c4c6 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs @@ -11,7 +11,7 @@ public ExportProfileMap() this.HasKey(x => x.Id); this.Property(x => x.Name).IsRequired().HasMaxLength(100); - this.Property(x => x.FolderName).IsRequired().HasMaxLength(100); + this.Property(x => x.FolderName).IsRequired().HasMaxLength(400); this.Property(x => x.SystemName).HasMaxLength(400); this.Property(x => x.ProviderSystemName).IsRequired().HasMaxLength(4000); this.Property(x => x.Filtering).IsMaxLength(); diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs index eb06973c22..02515fd268 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs @@ -14,6 +14,7 @@ public ImportProfileMap() this.Property(x => x.FolderName).IsRequired().HasMaxLength(100); this.Property(x => x.KeyFieldNames).HasMaxLength(1000); this.Property(x => x.FileTypeConfiguration).IsMaxLength(); + this.Property(x => x.ExtraData).IsMaxLength(); this.Property(x => x.ColumnMapping).IsMaxLength(); this.Property(x => x.ResultInfo).IsMaxLength(); diff --git a/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs index fdad717e6c..5203e70451 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs @@ -465,9 +465,9 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailAddresses", "Email addresses to", - "E-Mail-Addressen an", + "E-Mail Adressen an", "Specifies the email addresses where to send the data.", - "Legt die E-Mail Addressen fest, an die die Daten verschickt werden soll."); + "Legt die E-Mail Adressen fest, an die die Daten verschickt werden soll."); builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailSubject", "Email subject", diff --git a/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs index 8313baa6a1..09f2dc6314 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs @@ -81,9 +81,9 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmailAddresses", "Email addresses to", - "E-Mail-Addressen an", + "E-Mail Adressen an", "Specifies the email addresses where to send the notification message.", - "Legt die E-Mail Addressen fest, an die die Benachrichtigung geschickt werden soll."); + "Legt die E-Mail Adressen fest, an die die Benachrichtigung geschickt werden soll."); builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmail.Subject", "Export of profile \"{0}\" has been finished", diff --git a/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs index 3eda0b6cee..9b5970f35a 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs @@ -1,7 +1,9 @@ namespace SmartStore.Data.Migrations { using System.Data.Entity.Migrations; - using Setup; + using SmartStore.Data.Setup; + using SmartStore.Core.Domain.Media; + using System.Linq; public partial class ThirdPartyEmailHandOver : DbMigration, ILocaleResourcesProvider, IDataSeeder { @@ -23,6 +25,17 @@ public bool RollbackOnFailure public void Seed(SmartObjectContext context) { context.MigrateLocaleResources(MigrateLocaleResources); + + // Some users have disabled the "TransientMediaClearTask" due to a bug. + // When the task is enabled again, it would delete media files, which are marked as transient + // but are permanent actually. To avoid this, we have to set IsTransient to false. + var transientPictures = context.Set().Where(x => x.IsTransient == true).ToList(); + transientPictures.Each(x => x.IsTransient = false); + + var transientDownloads = context.Set().Where(x => x.IsTransient == true).ToList(); + transientDownloads.Each(x => x.IsTransient = false); + + context.SaveChanges(); } public void MigrateLocaleResources(LocaleResourcesBuilder builder) @@ -35,25 +48,25 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutThirdPartyEmailHandOver.Activated", "Show activated", "Aktiviert anzeigen"); builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOver", - "Third party email hand over", - "E-Mail Weitergabe an Dritte", - "Specifies if customers can accept to hand over their email address to third party when ordering and if the checkbox is enabled by default.", - "Legt fest, ob Kunden bei einer Bestellung der Weitergabe Ihrer E-Mail Adresse an Dritte zustimmen knnen und ob die Checkbox standardmig aktiviert ist."); + "Consent for email transfer to third parties", + "Zustimmung zur E-Mail Weitergabe an Dritte", + "Specifies whether customers can agree to a transferring of their email address to third parties when ordering, and whether the checkbox is enabled by default during checkout.", + "Legt fest, ob Kunden bei einer Bestellung der Weitergabe ihrer E-Mail Adresse an Dritte zustimmen knnen und ob die Checkbox dafr standardmig aktiviert ist."); builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOverLabel", - "Text for email hand over", + "Text for email transfer consent", "Text fr E-Mail Weitergabe", - "Specifies the text to accept to hand over the email address to third party. Please choose a certain reason for the hand over, e.g. 'I agree with the transmission and storage of my email address for the trusted shops buyer protection.'", + "Specifies the text to be displayed to the customer. Please choose a specific reason, e.g. 'I agree to the transfer and storage of my email address for TrustedShops buyer protection.'", "Legt den Text fr die Zustimmung zur Weitergabe der E-Mail Adresse an Dritte fest. Whlen Sie bitte einen konkreten Grund fr die Weitergabe, z.B. 'Mit der bermittlung und Speicherung meiner E-Mail-Adresse zur Abwicklung des Kuferschutzes durch Trusted Shops bin ich einverstanden.'"); builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOverLabel.Default", - "I agree with the transmission and storage of my email address by third parties.", + "I agree to the transfer and storage of my email address by third parties.", "Mit der bermittlung und Speicherung meiner E-Mail-Adresse durch dritte Parteien bin ich einverstanden."); builder.AddOrUpdate("Admin.Orders.Fields.AcceptThirdPartyEmailHandOver", - "Accepts hand over of email", + "Accepts transfer of email", "Akzeptiert Weitergabe der E-Mail", - "Indicates whether the customer has accepted to hand over his email address to third party.", + "Indicates whether the customer has agreed to a transfer of his email address to third parties.", "Gibt an, ob der Kunde bei der Bestellung einer Weitergabe seiner E-Mail Adresse an Dritte zugestimmt hat oder nicht."); builder.AddOrUpdate("Admin.OrderNotice.OrderCaptureError", @@ -76,17 +89,17 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Legt fest, ob das Standardbild bei Herstellern ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn dem Hersteller kein Bild zugeordnet ist."); builder.AddOrUpdate("Admin.DataExchange.Export.Partition.Note", - "You can partition the data to be exported with the following settings. This includes
  • Skipping the first n records
  • The maximum number of records to be exported
  • The number of records per export file
  • Export data for each shop in a separate file
By default, all data of a store will be exported into one file.", + "With the following settings you can partition the data to be exported. This includes
  • Skipping the first n records
  • The maximum number of records to be exported
  • The number of records per export file
  • Export data for each shop in a separate file
By default, all data of a store will be exported into one file.", "Mit den folgenden Einstellungen lassen sich die zu exportierenden Daten aufteilen. Dazu zhlt
  • Das berspringen der ersten n Datenstze
  • Die maximale Anzahl zu exportierender Datenstze
  • Die Anzahl der Datenstze pro Exportdatei
  • Daten von jedem Shop in eine separate Datei exportieren
Standardmig werden alle Daten eines Shops in eine Datei exportiert."); builder.AddOrUpdate("Admin.DataExchange.Export.Partition.Validate", - "Partitioning settings must be greater than or equal to 0.", + "Partitioning setting values must be greater than or equal to 0.", "Einstellungen zur Aufteilung mssen grer oder gleich 0 sein."); builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IsActiveSubscriber", - "Only active subscriber", + "Only active subscribers", "Nur aktive Abonnenten", - "Filter by active or inactive newsletter subscriber.", + "Filter by active or inactive newsletter subscribers.", "Nach aktiven bzw. inaktiven Newsletter Abonnenten filtern."); builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IsActiveCustomer", @@ -120,26 +133,26 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Nach dem Datum der letzten Shop Aktivitt filtern."); builder.AddOrUpdate("Admin.DataExchange.Export.Filter.LastActivityTo", - "Last activity to", + "Last active until", "Zuletzt aktiv bis", "Filter by date of last store activity.", - "Nach dem Datum der letzten Shop Aktivitt filtern."); + "Nach dem Datum der letzten Shop-Aktivitt filtern."); builder.AddOrUpdate("Admin.DataExchange.Export.Filter.HasSpentAtLeastAmount", - "Has spent x amount", + "Has spent amount x", "Hat Betrag x ausgegeben", "Filter by spent amount.", "Nach dem insgesamt ausgegebenen Betrag filtern."); builder.AddOrUpdate("Admin.DataExchange.Export.Filter.HasPlacedAtLeastOrders", - "Has x placed orders", + "Has placed x orders", "Hat x Bestellungen", "Filter by number of placed orders.", "Nach der Anzahl der gettigten Bestellungen filtern."); builder.AddOrUpdate("Admin.DataExchange.Export.SystemProfileNote", - "The following list contains system profiles intended to export data in the SmartStore.NET own format. There are additional export buttons available for these imports. You can find them over the object lists, such as the product or order list.", - "Die folgende Liste enthlt Systemprofile, ber die Daten im SmartStore.NET eigenen Format exportiert werden knnen. Fr diese Importe stehen zustzliche Export-Buttons zur Verfgung. Sie finden diese ber den entsprechenden Listen, wie z.B. der Produkt- oder Auftragsliste."); + "The following list contains system profiles, which are provided by plugins such as the Data Export Plugin. You can customize system profiles as desired, but cannot create new ones. These profiles also add additional action buttons. You will find these above data lists, such as the product or order list.", + "Die folgende Liste enthlt Systemprofile, die von Plugins wie bspw. dem Datenexporte Plugin bereitgestellt werden. Sie knnen Systemprofile nach Belieben anpassen, aber keine Neuen erstellen. Fr diese Profile stehen auerdem zustzliche Aktions-Buttons zur Verfgung. Sie finden diese ber den entsprechenden Listen, wie z.B. der Produkt- oder Auftragsliste."); builder.AddOrUpdate("Admin.DataExchange.AddNewProfile", "New profile", @@ -150,6 +163,73 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Whlen Sie bitte das zu importierende Objekt und laden Sie eine Importdatei hoch."); builder.Delete("Admin.DataExchange.Import.ProfileEntitySelectNote"); + + builder.AddOrUpdate("Admin.Configuration.Restriction.SaveBeforeEdit", + "You need to save before you can specify restrictions.", + "Sie mssen zunchst speichern, bevor Sie Einschrnkungen festlegen knnen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerNumberMethod", + "Customer numbers", + "Kundennummern", + "Specifies whether to assign customer numbers and whether they should be created automatically.", + "Legt fest, ob Kundennummern vergeben werden und ob diese automatisch vergeben werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerNumberVisibility", + "Customer number presentation", + "Darstellung der Kundennummer", + "Specifies the presentation and handling of the customer number to the customer.", + "Legt die Darstellung und Handhabung der Kundennummer gegenber dem Kunden fest."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberMethod.Disabled", "Disabled", "Deaktiviert"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberMethod.Enabled", "Enabled", "Aktiviert"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberMethod.AutomaticallySet", "Automatically assigned", "Automatisch vergeben"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.None", "Do not display", "Nicht anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.Display", "Display", "Anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.EditableIfEmpty", "Editable if empty", "Editierbar falls leer"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.Editable", "Always editable", "Immer editierbar"); + + builder.AddOrUpdate("Admin.Common.FileInUse", + "The file is in use and cannot be opened.", + "Die Datei ist in Benutzung und kann daher nicht geffnet werden."); + + + builder.AddOrUpdate("Admin.DataExchange.Export.UserProfilesTitle", + "User profiles", + "Benutzerprofile"); + builder.AddOrUpdate("Admin.DataExchange.Export.SystemProfilesTitle", + "System profiles", + "Systemprofile"); + + builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmailAddresses", + "Email addresses to", + "E-Mail Adressen an", + "Specifies the email addresses where to send the notification message.", + "Legt die E-Mail Adressen fest, an die die Benachrichtigung geschickt werden soll."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailAddresses", + "Email addresses to", + "E-Mail Adressen an", + "Specifies the email addresses where to send the data.", + "Legt die E-Mail Adressen fest, an die die Daten verschickt werden soll."); + + builder.AddOrUpdate("Admin.Common.FileNotFound", "File not found", "Datei nicht gefunden"); + + builder.AddOrUpdate("Admin.Common.EnterEmailAdress", + "Please enter an email address.", + "Bitte geben Sie eine E-Mail-Adresse ein."); + + builder.AddOrUpdate("Admin.Configuration.EmailAccounts.TestingEmail", + "Testing email functionality.", + "Test der E-Mail-Funktion."); + + builder.AddOrUpdate("Admin.Common.EmailSuccessfullySent", + "The email has been successfully sent.", + "Die E-Mail wurde erfolgreich versendet."); + + builder.AddOrUpdate("Admin.Common.SkipAndTakeGreaterThanOrEqualZero", + "Values for skip and limit must be greater than or equal to 0.", + "Werte fr berspringen und Begrenzen mssen grer oder gleich 0 sein."); } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.Designer.cs new file mode 100644 index 0000000000..90e9ba6504 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.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.1.3-40302")] + public sealed partial class GtinMpnIndex : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(GtinMpnIndex)); + + string IMigrationMetadata.Id + { + get { return "201605020640016_GtinMpnIndex"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.cs b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.cs new file mode 100644 index 0000000000..98f1936ecc --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.cs @@ -0,0 +1,19 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class GtinMpnIndex : DbMigration + { + public override void Up() + { + CreateIndex("dbo.Product", "ManufacturerPartNumber"); + CreateIndex("dbo.Product", "Gtin"); + } + + public override void Down() + { + DropIndex("dbo.Product", new[] { "Gtin" }); + DropIndex("dbo.Product", new[] { "ManufacturerPartNumber" }); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.resx b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.resx new file mode 100644 index 0000000000..15bcc90554 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.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/201605061916117_SwapColumnMappingValues.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.Designer.cs new file mode 100644 index 0000000000..1fa094db33 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.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.1.3-40302")] + public sealed partial class SwapColumnMappingValues : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(SwapColumnMappingValues)); + + string IMigrationMetadata.Id + { + get { return "201605061916117_SwapColumnMappingValues"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.cs b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.cs new file mode 100644 index 0000000000..f16431d45b --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.cs @@ -0,0 +1,105 @@ +namespace SmartStore.Data.Migrations +{ + using System.Collections.Generic; + using System.Data.Entity.Migrations; + using System.Linq; + using Core.Domain; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using Setup; + + public partial class SwapColumnMappingValues : 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); + + var importProfiles = context.Set().Where(x => x.ColumnMapping != null).ToList(); + + foreach (var profile in importProfiles) + { + var dic = new Dictionary>(); + var storeMapping = true; + + try + { + var json = JObject.Parse(profile.ColumnMapping); + + foreach (var kvp in json) + { + dynamic value = kvp.Value; + + var mappedName = (string)value.MappedName; + var property = (string)value.Property; + var defaultValue = (string)value.Default; + + if (mappedName.HasValue()) + { + // break migration because data is already migrated + storeMapping = false; + break; + } + else if (property.HasValue()) + { + if (!kvp.Key.IsCaseInsensitiveEqual(property) || defaultValue.HasValue()) + { + // swap value + dic.Add(property, new Dictionary + { + { "MappedName", kvp.Key }, + { "Default", defaultValue } + }); + } + else + { + // ignore because persisting not required anymore + } + } + else + { + // explicitly ignored property + dic.Add(property, new Dictionary + { + { "MappedName", property }, + { "Default", "[IGNOREPROPERTY]" } + }); + } + } + } + catch + { + storeMapping = true; + dic.Clear(); + } + + if (storeMapping) + { + if (dic.Any()) + profile.ColumnMapping = JsonConvert.SerializeObject(dic); + else + profile.ColumnMapping = null; + } + } + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.Delete("Admin.DataExchange.ColumnMapping.Validate.MultipleMappedIgnored"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.resx b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.resx new file mode 100644 index 0000000000..15bcc90554 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.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/201605111140288_ImportExtraData.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.Designer.cs new file mode 100644 index 0000000000..6cac518be6 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.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.1.3-40302")] + public sealed partial class ImportExtraData : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ImportExtraData)); + + string IMigrationMetadata.Id + { + get { return "201605111140288_ImportExtraData"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.cs b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.cs new file mode 100644 index 0000000000..191ace97ba --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.cs @@ -0,0 +1,72 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using Setup; + + public partial class ImportExtraData : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ImportProfile", "ExtraData", c => c.String()); + } + + public override void Down() + { + DropColumn("dbo.ImportProfile", "ExtraData"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + context.MigrateSettings(x => + { + x.Add("MediaSettings.VariantValueThumbPictureSize", "20"); + x.Delete("MediaSettings.AutoCompleteSearchThumbPictureSize"); + }); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.DataExchange.Import.NumberOfPictures", + "Number of pictures", + "Anzahl der Bilder", + "Specifies the number of images per object to be imported.", + "Legt die Anzahl der zu importierenden Bilder pro Objekt fest."); + + builder.Update("Admin.Configuration.Settings.Catalog.DefaultPageSizeOptions") + .Value("en", "Number of displayed products per page"); + + builder.AddOrUpdate("Admin.Validation.ValueGreaterZero", + "The value must be greater than 0.", + "Der Wert muss grer 0 sein."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.OrderListPageSize", + "Number of displayed orders per page", + "Anzahl der Auftrge pro Seite", + "Specifies the number of displayed orders per page.", + "Legt die Anzahl der dargestellten Auftrge pro Seite fest."); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.RunningError", + "Error while running scheduled task \"{0}\"", + "Fehler beim Ausfhren der Aufgabe \"{0}\""); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.Cancellation", + "The scheduled task \"{0}\" has been canceled", + "Die geplante Aufgabe \"{0}\" wurde abgebrochen"); + + builder.AddOrUpdate("Admin.Common.HttpStatus", + "HTTP status {0} ({1}).", + "HTTP-Status {0} ({1})."); + + builder.AddOrUpdate("Admin.System.Warnings.SitemapReachable.MethodNotAllowed", + "The reachability of the sitemap could not be validated.", + "Die Erreichbarkeit der Sitemap konnte nicht berprft werden."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.resx b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.resx new file mode 100644 index 0000000000..738509bbb4 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.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/201605191216116_CheckoutAttributeMultiStore.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.Designer.cs new file mode 100644 index 0000000000..d4625babee --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.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.1.3-40302")] + public sealed partial class CheckoutAttributeMultiStore : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CheckoutAttributeMultiStore)); + + string IMigrationMetadata.Id + { + get { return "201605191216116_CheckoutAttributeMultiStore"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.cs b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.cs new file mode 100644 index 0000000000..1b6ba5a31f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.cs @@ -0,0 +1,17 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class CheckoutAttributeMultiStore : DbMigration + { + public override void Up() + { + AddColumn("dbo.CheckoutAttribute", "LimitedToStores", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropColumn("dbo.CheckoutAttribute", "LimitedToStores"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.resx b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.resx new file mode 100644 index 0000000000..d743254b0e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.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/201605201911421_ExportRevision.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.Designer.cs new file mode 100644 index 0000000000..e3a2d0014f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.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.1.3-40302")] + public sealed partial class ExportRevision : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ExportRevision)); + + string IMigrationMetadata.Id + { + get { return "201605201911421_ExportRevision"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs new file mode 100644 index 0000000000..d168932260 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs @@ -0,0 +1,204 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Linq; + using Core.Domain; + using Core.Domain.DataExchange; + using Setup; + using Utilities; + + public partial class ExportRevision : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ExportDeployment", "ResultInfo", c => c.String()); + AddColumn("dbo.ExportDeployment", "SubFolder", c => c.String(maxLength: 400)); + AlterColumn("dbo.ExportProfile", "FolderName", c => c.String(nullable: false, maxLength: 400)); + DropColumn("dbo.ExportDeployment", "CreateZip"); + } + + public override void Down() + { + AddColumn("dbo.ExportDeployment", "CreateZip", c => c.Boolean(nullable: false)); + AlterColumn("dbo.ExportProfile", "FolderName", c => c.String(nullable: false, maxLength: 100)); + DropColumn("dbo.ExportDeployment", "SubFolder"); + DropColumn("dbo.ExportDeployment", "ResultInfo"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + // migrate folder name to folder path + var rootPath = "~/App_Data/ExportProfiles/"; + var exportProfiles = context.Set().ToList(); + + foreach (var profile in exportProfiles) + { + if (!profile.FolderName.EmptyNull().StartsWith(rootPath)) + { + profile.FolderName = rootPath + profile.FolderName; + } + } + + context.SaveChanges(); + + // migrate public file system deployment to new public deployment + if (context.ColumnExists("ExportDeployment", "IsPublic")) + { + var fileSystemDeploymentTypeId = (int)ExportDeploymentType.FileSystem; + var publicFolderDeploymentTypeId = (int)ExportDeploymentType.PublicFolder; + + context.ExecuteSqlCommand("Update [ExportDeployment] Set DeploymentTypeId = {0} Where DeploymentTypeId = {1} And IsPublic = 1", + true, null, publicFolderDeploymentTypeId, fileSystemDeploymentTypeId); + + context.ColumnDelete("ExportDeployment", "IsPublic"); + } + + var oldFileManagerPath = CommonHelper.MapPath("~/Content/filemanager"); + FileSystemHelper.ClearDirectory(oldFileManagerPath, true); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.DataExchange.Export.FolderName", + "Folder path", + "Ordnerpfad", + "Specifies the relative path of the folder where to export the data.", + "Legt den relativen Pfad des Ordners fest, in den die Daten exportiert werden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.FileNamePattern.Validate", + "Please enter a valid pattern for file names. Example for file names: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%", + "Bitte ein gltiges Muster fr Dateinamen eingeben. Beispiel: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%"); + + builder.AddOrUpdate("Admin.DataExchange.Export.FolderName.Validate", + "Please enter a valid, relative folder path for the export data.", + "Bitte einen gltigen, relativen Ordnerpfad fr die zu exportierenden Daten eingeben."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.Http", "HTTP POST", "HTTP POST"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.PublicFolder", "Public folder", "ffentlicher Ordner"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.SubFolder", + "Name of subfolder", + "Name des Unterordners", + "Specifies the name of a subfolder where to publish the data.", + "Legt den Namen eines Unterordners fest, in den die Daten verffentlicht werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.ZipUsageNote", + "If there are a large number of export files, it is recommended to use the option Create ZIP archive. This saves time and avoids problems, such as a full email mailbox.", + "Bei einer groen Anzahl an Exportdateien wird empfohlen die Option ZIP-Archiv erstellen zu benutzen. Das spart Zeit und vermeidet Probleme, wie z.B. ein volles E-Mail Postfach."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Cleanup", + "Clean up after successful deployment", + "Nach erfolgreicher Verffentlichung aufrumen", + "Specifies whether to delete unneeded files after all deployments succeeded.", + "Legt fest, ob nicht mehr bentigte Dateien gelscht werden sollen, nachdem alle Verffentlichungen erfolgreich waren."); + + builder.AddOrUpdate("Admin.Common.FtpStatus", + "FTP status {0} ({1}).", + "FTP-Status {0} ({1})."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.CopyFileFailed", + "At least one file could not be copied.", + "Mindestens eine Datei konnte nicht kopiert werden."); + + builder.AddOrUpdate("Admin.Common.LastRun", "Last run", "Letzte Ausfhrung"); + builder.AddOrUpdate("Admin.Common.SuccessfulOn", "Successful on", "Erfolgreich am"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Name", + "Name of profile", + "Name des Profils", + "Specifies the name of the publishing profile.", + "Legt den Namen des Verffentlichungsprofils fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.ProfilesTitle", + "Publishing profiles", + "Verffentlichungsprofile"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.NoProfiles", + "There are no publishing profiles.", + "Es liegen keine Verffentlichungsprofile vor."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Note", + "Click New profile to add one or multiple publishing profiles to specify how to further proceed with the export files.", + "Legen Sie ber Neues Profil ein oder mehrere Verffentlichungsprofile an, um festzulegen wie mit den Exportdateien weiter zu verfahren ist."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.PublishingTarget", + "Publishing target", + "Verffentlichungsziel"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.DeploymentType", + "Publishing type", + "Art der Verffentlichung", + "Specifies the type of publishing.", + "Legt die Art Verffentlichung fest."); + + builder.AddOrUpdate("Common.Publishing", + "Publishing", + "Verffentlichung"); + + builder.AddOrUpdate("Common.NoFilesAvailable", + "There are no files available.", + "Es sind keine Dateien vorhanden."); + + builder.AddOrUpdate("Common.CopyToClipboard", "Copy to clipboard", "In die Zwischenablage kopieren"); + builder.AddOrUpdate("Common.CopyToClipboard.Succeeded", "Copied!", "Kopiert!"); + builder.AddOrUpdate("Common.CopyToClipboard.Failded", "Failed to copy.", "Kopieren ist fehlgeschlagen."); + + builder.AddOrUpdate("Products.NoBundledItems", + "No bundle items available", + "Keine Bundle-Bestandteile vorhanden"); + + builder.AddOrUpdate("Common.NoFileUploaded", + "There was no file uploaded.", + "Es wurde keine Datei hochgeladen."); + + builder.AddOrUpdate("Products.BasePriceInfo", + "Content: {0} {1} ({2} / {3} {1})", + "Inhalt: {0} {1} ({2} / {3} {1})"); + + builder.AddOrUpdate("Products.BasePriceInfo.LanguageInsensitive", + "{0} {1} ({2} / {3} {1})", + "{0} {1} ({2} / {3} {1})"); + + builder.AddOrUpdate("PrivateMessages.Disabled", + "Private messages are disabled.", + "Private Nachrichten sind deaktiviert."); + + builder.AddOrUpdate("Common.MethodNotSupportedForGuests", + "This function is not available for guests.", + "Diese Funktion steht fr Gste nicht zur Verfgung."); + + builder.AddOrUpdate("ContactUs.PrivacyAgreement", + "Privacy consent", + "Einwilligungserklrung Datenschutz"); + + builder.AddOrUpdate("ContactUs.PrivacyAgreement.MustBeAccepted", + "Please agree to the storage of your data.", + "Bitte stimmen Sie der Speicherung Ihrer Daten zu."); + + builder.AddOrUpdate("ContactUs.PrivacyAgreement.DetailText", + "Yes I've read the privacy terms and agree that my data given by me can be stored electronically. My data will thereby only be used to process my inquiry.", + "Ja, ich habe die Datenschutzerklrung zur Kenntnis genommen und bin damit einverstanden, dass die von mir angegebenen Daten elektronisch erhoben und gespeichert werden. Meine Daten werden dabei nur zur Bearbeitung meiner Anfrage genutzt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.DisplayPrivacyAgreementOnContactUs", + "Get privacy consent for contact requests", + "Einwilligungserklrung im Kontaktformular fordern", + "Specifies whether a checkbox will be displayed on the contact page which requests the user to agree on storage of his data.", + "Bestimmt ob im Kontaktformular eine Checkbox angezeigt wird, die den Benutzer auffordert der Speicherung seiner Daten zuzustimmen."); + + + builder.Delete( + "Admin.DataExchange.Export.FolderAndFileName.Validate", + "Admin.DataExchange.Export.Deployment.IsPublic", + "Admin.DataExchange.Export.Deployment.CreateZip", + "Admin.Common.TemporaryFiles", + "Admin.Common.NoTempFilesFound"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.resx b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.resx new file mode 100644 index 0000000000..a653f94065 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.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 + + + H4sIAAAAAAAEAOy923LcuLIo+D4R8w8OP50zsY/ddq+OWLuje05IsmQrti9akmzvvV4UFAuSuMwiq3nRpSfmy+ZhPun8wgHAGwgk7iBZpa4Xu0QkEkAikUgkMhP/6//7/3/7n4/r9MU9Ksokz35/+ebVTy9foCzOV0l2+/vLurr5H39/+T//7//z//jteLV+fPGtg/uZwOGaWfn7y7uq2vz6+nUZ36F1VL5aJ3GRl/lN9SrO16+jVf767U8//fvrN29eI4ziJcb14sVv53VWJWtE/8B/HuVZjDZVHaWf8hVKy/Y7LrmgWF98jtao3EQx+v3lxToqqosqL9Crd1EVvXxxkCYR7sYFSm9evoiyLK+iCnfy168luqiKPLu92OAPUXr5tEEY7iZKS9R2/tcB3HQcP70l43g9VOxQxXVZ5WtLhG9+bgnzmq/uRN6XPeEw6Y4xiasnMmpKvt9fXuabJH75gm/p16O0IFAj0h5R+mKwJHtF65XNf//2ggP6t54p3r765dVPr376txdHdVrVBfo9Q3VVROm/vTirr9Mk/g/0dJn/QNnvWZ2mbE9xX3HZ6AP+dFbkG1RUT+fopu3/6erli9fjeq/5in01pk4zuNOs+vntyxefcePRdYp6RmAIQUf1HmWoiCq0OouqChUZwYEoKYXWubYunsoKrcnvrk3Mf3gdvXzxKXr8iLLb6u73l/jnyxcnySNadV/afnzNErzscKWqqJGuqdMsTusVOs0uEtxktOkaPMzzFEUZMEwNvvIsKsuHvFjhggrFePi+KDuEk9PiMqnS6Sl+mK+eJm/kE6oizNGEbOUsjb1DZVwkm0bgzNDePHP1MVnjZbG6zOmCLn05+RxlK1QclN+T1S2qfLE1WP6ZZ9PToWnqexFt8AZbYSEm9N2k/sVd/jCaN7+RH2LmRkUA+VIkeUGlslq+GwiPy+g28Fz89nrYfdV7cvR4hDeb27x4ctmZo8dXDIb95ixvS7Mt/+2nn4wm2ZK93iXlJo2evhCWt2FUY/65QFVFx2LNO1gk3CS3dUGhX7V49hzkzEFvp+Ggb1Fah9gpLJultNJT14lnP+ZxlCZ/olXXpgP3tjga5hUQ7tlY3lYzHXZTC6hYUXZbR7eWLALgIVOHMH3eF3m9mV9A9+0v1fRc6/tzdJ/cUgaSzOTLF+copQDlXbJp7CniyroawE+KfH2ep9CC7qGuLvK6iMn4ci3oZVRQ9dpNpvTd8hQlLZ69BJG3pdkI30y0XNqZaamu3ImnaB/X/qNGFyg/wjhUrQc4uJ2k0e3pGg/2JEmRhty/mI1Wc8StUt/zWOBDN11L5Z3/OdFVBVfKTCq6m8k4RyWVcaVcgHKQchkqA+xl40iMSqE7oeuunXGogyhoHM69hNXLOl/tqqP1MkeXrvWFjjCnJVldZ2l9m2SCENFVvczrGBI+4bQqf6EA6lZaEeIkFM5QsU5KsjjPUUyN+tYC4QLFNbHXveJx7QWBvK1Al0m2h3+Ti6y3v/wyRduDNXTylqWL94jyNirIsoK3dZ6Hr7gqwxJWQwprWAPutYhZVC4Gw7Z6+YpFtF+9zqt3ohV0UiB0gVl1Q9vzU54vo8fjR7TeeN96YUStIk7wcFOgrnoQV8m99+XTadlItYb5/XAFlY+mQomXDBMLJv7EYSjH3LSLHK9/e4FEqpX0370QkrcV6iyxpCrS+kRMfmEezOhA7sy/ZB/w+jijKr0ftoM0zR/e16is8LnkW155I/SwiQj3RHjdvcOM+bWKO0zkz8tkra17nK0ca/ramtyObUTSgMe0UYGo0o1KIQ1OKfZx7YOsfMDiTNqppvyqEaPjbjFFokjnyr1leIPMS5I3KPbyXCGjMJV2U5Z/rtfXqPhyQyRY6TeAKYy6zfLxW2LQ2oeWoE2fMLmoQUeh9HFQV+xiHHdWAgbKBhmst5xgEXtJCxZRMJnx4jAqUdsBQt1O0e186LSrsyGT9Ro12AKmmn2IbXWc4mWCCGJ+2O8S8rY6Gr2vk77V5rfttWeJ29XdQIa4gjzGs5xO3oqBV3rYhk7yYh1Vvht2h+0iSqvJu36wWifZUb5eMx7D00UylMFsTAc3N0ma4OXiS+0wFqd3KEUB4ig6w9VBHOc14MI9he0qDB99jMrqdHOwWuEjmiqcwdRhRCPxCkQk5ZcMPE9aO5uU1cf8NslcD6i4PuUiLKqlKJwVgpakkruJTvRfMWCDGiCWCrs/AGKrtx4maYqnuZ97VTd5WKCvYxB5hzk4217zmp6q2y3M1aDRiP3mYQQtWwoIqdh+N1Y9aqlBGIaQE1t/M6Xq8fEjUWii9KCu7ohKE1Mg1SlHVQOcBqMKwpyY1bKdIKwG1OuzvKzgsfXF4EDEUqHXAIhTF5tYT3kfabm8k+NiuJccjG036Zkf7iEtAjs3LhH6xRXbdukc4TMGZpE/qIkW7NoIBOwiDCF0VQJm3+WHqFid5UlWlR8SjITcuIP9FuAkvZfDAWNQANuOpLvqNNprBGBA/HEwcgHIA9qKwIu7nNY/wofYU6yUwX3noUDyS4EE2sshvcw6PUEd4pDW6zx71SLYn+nlbZ0kRVkFMt3q1ddZGtId+sO0gllsE2XTh28fkQNaIcbWaK/QKoS/3SdZLB5dNS0yAbCTDatdmm/maujt5A39M9kQXSlKNb78gW6V7/IMNZcfk7d1Ej3O1JLfeVt+kmnWELgVdhttDzNsgFyRsGnz5dZ7NbtIlZ3jIMUujgCkHR1D+RndO3JZ783vkgLFRD171eLYb8/ytjQb5kRhUdQ3pGwNH0EcTcpgzoMP+UdEiHhazhE0dXlXIGTa4M8BGsSCFhVJzDXm6EdTX/8LL7XL/FvkbeLdhtCpuRx2zvEpGM8Bxt1x7SdU3eUSE9IY5mqo3Ii3ZGwQ00MDpyttFevzLbsRSIY12iuA3REEEDsPQnkdC8f0cIkaaRFwU7ffhxRiSb0PTZXpZMZMUuHk0ulthml9dEdWwiRSiREsc8gjXpu0EGFu63us7HoomCNM++Utb0tmZAjrAjiVtnp9XaD7JNKIiTA3wtugBzmdcx33c2HxK3d9L/+t/oY0gBtXi2u/5hVrviWV76I38cYwDQbXea0cbDaY9fwXX1C/jq+b1SRGq/5KJrTHgOzySepa4LSsD9P8tnf4sl7SpHb5isGxHa64bWcu0eP0Xmxk8MSiHM7pt8MIshRD66sBcGAnqFxgJRDIm42arnjwEEGw3wvkbYWK5wuVitey2UB5lF0Mmy2ze6cm6na2Ft8R65bpqIznVXikl9Ht9Emkl4n/M8yTbWrq0De2bXmyw4wscMquiW4EmX0KtqI47Ya88VO5ZQYJH+2xQiGkQiG4XY8h/DL6FPmqjqtzfBpHDy7nuKiKcI9ejfBsh+LXdmlbNkh1Kw3hZlFSz6OKudVzI8oHlG5u6vS/UHmJOSUNguxz7oJLHi7XTD8cK8dy61UPyUTJQQBifBwIZR3cyWJpaZHhw/mxwmIsq3PFrWfJiKQ1xABQs2p+saAd/X2lz/68oJWGpDn/KLQCt0kzNdMEviGk7LekTDD0abZK7pNVHaXpk68esswF2MVdjjXhGfXEE9y/OdubNSyw41q03qQBIvrC5mPp8IS7h9wfaMJErtJjfheXRHeoYKf9Rnu6qNfBjvqBMHboqBLFDdq7j+GQ9h5OB/G25Zq++FFPv+iirL6JYqKCFHgfrbReumGafV8lqgUeppHT8n1yUx1FhfdlT4cnhLZCYqSSAn2p7jDFm+0kwFNgFOeg/GjimoMItZroxiQQEetGB6sV1wfvMZ2W7/KHLM0j/3vyFo/vzH3N0maBdwi9x/ipc4v/ciPgdMxA1KI5ftwkzRtK76InHqcZChooTlGEYPsPUXkRYa0JhZrVMTbLABXcG5JB5OC2QIhVHF07M0I2i9XktDwnmauLAA7RPaKjpzhFTad8ZRyL8QwVSe69+nqcdO+niD3Xyil1KT/OSJUA+TBCJrjF8jQhKy9KO4yNM2Bvv0Zxsia2qbMC/2ofSf77yxcXJNU63j8duh8s38lpeVx6k5N5PtCXcbCKQ+4ls3u8NDG6xufQ++RW5fGPf9RRa+vwEtnNcY1iPLiPElw3SRmsnu5hYE+dN6wkCzjyj/lDM+o2JYm382BeJTdP1CBwkhddHw8RPn35IT6M4h/0fVDyDrh3Gh9yGiQYTxta4hNIf+j11ijooR/PUrKu12EmqcEYPYbDSAmAVi2yBM2grOK1QDp4WD8d1lU1GBc81haB/p6Ud2lSVmGQtos/RZh5sVwf2W+cLz+xdk7RJbG3fWmEJPgO9CVdTdtAezA5otewE7VxscF4otRyIOY4e78Gcnnt4KHA4mr9HBwxdeas46xCRRmEv1qxNcKMJmaKVrDN2iY+fFwmqFmT3gIf76GorA4qLDqv6wod5evrJGuv9QIyIe4zlnk01RtxoU0Tf435O0pu76ZbiuNzTHD035PVhNg/TEubfqfxlSc9Ij9hEu7GIlxwRZg0iVvkJi4OMLlHxROpbGk96dRArIKJF606TbpE7e4T5Njd4/uEorIuEOmSQp0M8ipl3+bBmnWH9BWxPVryY4zajLR1tkoRvdHSWKDCmNeb9s5QQfI1hTJ8jJASaoTGySaaskYsT33ZbIn9Fgk6yXSlVy30ZT7YygfHGDmU4AyjALV1EWSlgcrD52oMKPjzjMplDkpjIEf/pGZW5QnKBLCrlg+k3lUyUJkTkhTelvjdyaQxeGl8rDqzmMJNjAeRDUCAc+w4e4mo7DsLqOg/BCYbAwjrOI6zhOJRDqGFUfSeg5B1nAdz7HN/CR7OrVDpc+fbX3oCvWnTl/bHFmX/4SqK8agryManqeU43ibcQSFQR3CiPGWKpeKUhXGUpt+iIsFKHXSOVHZfUU8xQQa1ZLNkUtVxqnjUVgO3GK3tEJ3HxerpSh4cA4pMyJZLuXAEZJ3IjLsbVnSWBxW7O4aQdpgDs+0ya8MButsXQ5whFAq8IEJ4+TR3qqJTMoxWqe1/7f2a5W1pvHwnetO8m5kQnhpfS3JWivFwA/ikdh0TMQY3anVN2Z7KrW9+lwgtbL2ryqO83uQZm9jN2TIlYJos40k3M9Q/kq4yXx4dMBHi2fqB6I7xl7nmKCYevNkKqrP8ACdsS0pg280JMDFMYZHQj0EwXhhbJFpULefL4yz7NuEawDggQPlQQGi/dDX9Y/LOEUYdiv1WLG9LsxVPFHBD4l8Ctawz4M8XK3GYV5hPZw3rSZNohteZ2mUUKqBnHzAT6JU6EtEXygGwtbRZ3lSRSKqL5E/k1/Yo9Ke8zPGOheKKx+3skdPi+TJ2HpssfRY9kJ5H2a3SZysMDwSOjQvvv7DFkUPbdgse7n5/m+/TbyKsaH1L0MOnSZ5vCH356HBk0V9AAucbYytsI6bhZHX9TVUHxCSp48rE5HQ8gF+Gkq4H1rr7J7RKoldt/b3irhBfDYkOkywqBpf19i+ThaTzM1zTJC7KLWySWHyUnyQpytSng58DBT9+Rg++cvu0vCyirExCREhNk+LS5eZWFCEwhPbmlpM3buJkhNM/58hevJimHpnqWDGfbuS0lTqvBsk1mmzRWK7gsD4Xur4qUiNZX6kxdsEgt2sMvv0qVq4bSq4A648n+3mdIqMHyQMlq9ygyW+t2peYAmXp74NSw6Ab3n/ABwjm+WZLi82Appfw02caZRqNngjz9A/9zNZwNxt6K1Dollt7heU8HT9iQYV1v2jGM/Lg/zDdDZL0dKy8b3Lbe0ZeuB7vm7CI9ruNvC3NRmAYV2B9RZ/mxQf0+C1K6/lbbxXTjznZc6aOqXDVgm3PWYPPtv9Ra8C1XznytgKdtkYe9r7IgiVuiEMGAMl9pRZOOam9qJk5w2SSYhZk3XQ974aSFbq8q9fXGfPqtyuyNnHo9tzgPMerF/lVSMcUDY8YRgZxta7YTUIRJSSvpo8YUtS1vTfhwpqmDYWSGFa0oVMBrUFMW0YDMO96ELOQbqpD6h0c6r0aIm9rIJp3/p6O6MEQUf3e+xG1EqsdjXuJw66hkS6NLFxGlmrXrYEc9lnJAbw1OUz7dSpvK5CGH8p37LQ8wVpPPeQ1XVAfk7/z13OoQdjwACyPG+5/SNaeCDjBPu8S6Kzrb8gdno089pcNLLa9fJhcPrDk/kvIiDG3Gobojyup4/RHf0jWobzCBPLDJ9mASf+DyJJAQmQvPbY5JGRWJ32GE/ZRDdsV1bCPQ9jBOIS9a/4zsg+rHOPGm7voFgeVC4oCCBTitAFnp/E/d8B49zqESvgEOYHAhG8kaJAU4401LED+vCYSqx12iGCsSVxe9QeC0HmkJIcEw+xTpgNT8YnDaNXoTEmgwmJJFyUqL8mpHqqz4FSh3ctNW4Hn/eZDGA/d7ZFg4RLkzbDU7VLpGUkNP7kIkkndrgTPQB+H6oIMdMExgfALLvb2Au+vIJpU3AtLpgWWHC+LfJatz0ntMrr1P5ZhJPul5by0QtlxdXt1sMyukn0UzP7qw5uK7Kn+PKtAvudleVtzPPM7y3u7C70lHPSdG9tXY9TYLB+J0bgpW74Jo/GH285HLA7KMrnN8Crq7krmeBxukZdQTkv67KH/3UsYc+Rw0vzPdRpAa9XIvHCPSFJb55e6+nJDkVK91/6WxvthDFVecs2bGaZVZYY14/oTOFlMl//dYbCuJlbTPOmqtjUp1E2rugxbm3jdWEscD8IjrJRFtNf95G1pzjETBXYaeVeEDOvc7hhSaG9us7nZ7SO+p7HwR7D92pO3FUhxatEEu9Agb7fhT8xD7JNpYadlm5HA24uE2ZeyqshTgm0rEyPZazQuD7sYbuLOCgvPcy7jAe2upnVsR6i+1DAYKo+YhkxZPdBDa1xJJa+WBsr6pk/5qJGEsPAF3Ez2u4jBLjJPnvKFrpImc8P11ZTajCih1SWKds/t8rYkdPP3V9uhRaTxgiU5ey7+qKMClee315MPqrHhrv6FV8IaTZhArnmtfYaG/OKYJzsSUtEQQq39mGQ/mAx1iySysdW2tkfPMtQ7zZQ1px1peHjQMQF4h2C/zyjWbUuj93XSt9r8to0jKPsHHr8W3sl9AFSTbSRdW1NlQScnddS8GDj5WAyynodp6PgRj6kMr69C29Szya9uLPr6N0k9NO8ex178ydsKpB3AyVltw7rcE/KGueC19O9wz6LBDFTzou8YFHrWl4UQlBYJWMjrWcvXh+U9VJkobU/xzQvyH1C6uanTDJWl/wleQBlMrLwgHixtV+gTNO1UtfvmS5MF3PTOdwV+j8p2gOHcGkYdVKn3AoGvuKqCQq+pIVPhddW8GBDv1eVRvnbM0U9qv2JQbAePtZ3Rxa6HMms0jaHH6a+lCKWdErVJub3DCDI6M7FXA+DA1lC5wMQgkDfLOuYp7vl1n5tYY4uJsts6uvW/sw20Bu0Dc4sQy9Gy2RMquuZuNVjihGUecG8ScDQSwjuJxMFmU+T3aNXiOwLcbG3DKPMqPNLAqTOCJpjY57RxG5l0j+1kqXSPpVviADXeYEeF4O46hrA9KzH7M+w14KQF8Lf+SlXBSQsgBq0ii9KDuroje1oTsHaOYsy3LqenLgPQKxXivcqgEEItBX1VhuM1k2x+UnsomeVmdDeJMhwnbJPt1duMLX8hvEy5b56mDuIYH1PnaRD/eZ+sUDHls2RayxgoOFWC5GqoOUhSowrCFmBWy+vEdZIX9fosL11MBLRu+apHsReh8rYu800ShzJLh3C3nf8wc3p2sFoV1AI6sX/ILmRlU8qXfkmBwkQsFSQHAGKrPVIUlG01XWQBoU4O5YpuMkD+8qztjJdAozj2Ek3eFqXS1kg0MlshPJUu6ut/oVglHf82TYDS52YhlH7d/5bgE5inASMqK9ITb5+vFk+oKe7wNXLXzhD1PDaERjxKd4RxMSxrORinPUHXwRYI7B39qepaA+C/BXhK/73gl7dFCfS+yOvNxHnsTPMmb42HPyjVKV97yuQwmwORoEHOIc9xi/hr5YQe1rBcml+xQJw0Z8pgac4C+EvzthNeIp3i2Mt1hZB59tL4Wa7xwPePapEB3yNZiQv+/kgqT5zERTu31pKiSaHT/LcXEvK2KIG0Sc4CXTiQtryjIYI6BHu/6J2kKSZWawj1NlbgJbhRoDMg7wVmizpYR8JgO4ueyG1yUGSfUHWXr6a8SZJwzFFdFCiLn45wzRkabRo7j6pBAVZ7j//deS1cRo/thhrC8PYt0udODChWLurrCovE9DSL00uCNpSrvaqx48d5GrskjeG5iYk701wjHDU6z0hbqTPPCNvGZh0ZbshiKds3NhKOeBdJiIoQpScITU1TectTE1je8tTUbvGr7j8DSrjJmbRb69O2QsXKZE1gxbvOVmhlmxDWupmHqFid5UlWld9RgTDb+fv0Ht2h+EdeDzHecx6lhcZnSaHaqR6hfNgPbm6SNAnwWmR/PthMTgPqVE0ONRj9UYGw/DrCvDXWnZxZCmMiGKafSNLlWdRwgTYTvkJa/kAr2ZRMOsKj+/u3szR0/LhJCmpI+JRnQ5rvmdr8LxRNT092fTXJXd+h6yTA+9Y9qoOY7psf8nQ1A3+IDc/EmEzDh1H2Y5YDMNfmLCKGbfP0aM7maLjKkCdjjiZPr6MZlIt2N6UKYB/COvW6r/GBoEj+pJKGpqiIYvJzUA1mb3qWJSNr/ByVTDLgCSX8hhjS5yW42OhMo72or3sdfd4hn9VFfBeVaE4D/lmUuIYOdhaQ5pg+/by0zZHzORY4m7q5LZndahzogevtvZ5kT8LniFy9MY/1Gl1bfIhIFqLWivM5r/oXzXyJRkJbNtXlXYL7F+HPNFrpQ5Stvtw7nKyk96jjKyDwPpWu0SsecLhThcoFNwwQyNbhT+mU2LQA+SOOSyRdc/ZC7AxPX8voFn1ISvL6AthBCPCqvSEeOiuHEu6qFaC2T3m+T27oKVE7CAjw6muJVt+T6k4YjB5aGJRBFdvB0VokEFTB3zROVOg/VyR0li936hmWGpI82n2xpGdDEdwzpty2Z+dohdAarVgJedyo90BHWSgtU2iBhcHoa1g/XYv7Lo9E7kpFso9LxAcvx8UuvdoYSWMBkpd2HIBE6PFQfmk5QUHo8IRNg6YEBebeG0beVn/H6mk+bqSarw3aS+nT7rbmO+xQQ7PJdh+FtaKGttUXWpkyhYJg1PGx5HJa6bC24OrlBmHbL3N5Wx29fFfoaAcNgax9EmCiW8uJBEpHTXNtd6ihUXS7j8K6VEPbChRO0ZpDaTcaEKzTeQkcfyGzFyzytjqjHCMVoByAZtIphBfexH4Qp2XXWfoObBTA1NUhPMrrzUwW83NMjA3JXz2LSbBvbZ7sORcoI+fYOUbWNDXPsD7hwxZNwTVxO+R5uY47qG1yaWPuRAYo+y3ZyOwkbOCmo4ElqXI8V7I64ogkoNJdWQbvtSUzPfQKb9knSDU5jGMieYeeBNHzzxZ9NYfXfLOkojnSp/YP7hua3B14jtHMMpKp3Zc720ajI05NsnFrU9NuER/aeX1nu7d1AuS4PS07ZMHU+I94gWTDu0WWRyAirZvH2sw5RBOoV2erFGE1K5reS6IR8Ec0CV8o/pYqSgdlmcfE5XnVKSvwrUdALUmm+em0Kn/7rsVFI3hdA1xEGiukiodKBtxn4kMlQqGia2chHirpLy499UqCYq9XytsKog028+Qtkew3YxrnmQ/OHlt5ptVIArOLfXC5ARf/TosNuFj3SmkN4NuvQQXfBYrjbwgeAsdhlEbZ8NaX+13QxKbbuQxoE0kGpSMb5O0CubWp4ASxoQQOf+MU2CHIZDQBr5s6Nx4HWdh51PQ+RHv5J28riA5yWUTxD0zxmbzIaQRw2NMd5Rnk6pv+DqXJPSqeHKvPrvsYO9fxi17ie2fr8id3BGUhrgYZIHZwBCB1AhxDeeWRYlGGEEp7o7t+RVI6BfGkc3lIL4y9XLsYQq4D2XqFV4vjeQWzY3aO/qiR0xsRrYFghGa/DhTrIES2smCLINRxKczl0zmKyjw7yYuGm+Y3g7T8i6jZO8gFgV8H1JFZZlPLP883nbdHFd3ctJEXU1+srNZJpo8b/ttPQV4bGcm2MEnrtih+z/VEzdBEcpaGIIBzJwjmedjMqXJ2hDchP38KHtN+Z1Ot/wA721lUtPqN5Q1hc5/nUJGd4hAelsEskGHcQpbJ34SXJCowDxG3iMkslmGUjl0QxQNzS04XYyl1xcKzZwwpGHDSkMMGjTEWGoL2EimQvt/OFlnV/a7QCnDNK4PRdznIpa+Qv819ExRQ7XdBeVsax2rTB4ptLbboscKf1pvpM5wQB+g/6qQI8GY5saER+JbhQ+E9LS+jx+NHxFDDFRVGdITZ4DYvnoJtxEd5VhV5GkLXCPfewWlJPb2QL8Eme51AEEI0NA62OsOwV4BIHMS1aR3BNm1c0ctcDbcSUKZTfHvBLm9LoNjEj2NNtFNQlfxg9S/MN6z1JLhy3tznzdDQaYkx4WWP4gBeqh4C1VxyzS+zeJXTWtg5Xi3EdUH4us3l5B3WLkG4l1rytniSPedEFvxYJYZLkIeuxMqsKdOsDmDcNKwYdJ2FW2D7laVg4qc4Rc3W7LkaCKIzVCS5d/4J6klD8Xm6Ll5UeNql7i8LnS0C5U48zZIqidJtlmRsF42k2NW4hlx0jQC18moMbWtBk+7/c4tlMfmZpTx3EsuU6fGnj/mtg0TGtW6JfxGDZS+N5W0xZNqmS5xwua23QzBxZAaXMgNzJcAPq1cBJsglFWzQ2wi2IegiAipX9jbMhbZAxhDihMDvRYq8rSYrNO7RQ16oElS/mcZQozEPTfRa73FGgC11LGM+9tsK91ug2vSe335E9yj1f+szL6pw0UCWrZ9g8Nly+Wz6ZK/e68xRn9B5w9yir8X0bhuYyVFRoGKOtmb0t9BwWkGczrLY138kzypyhohK1WtJf3PiIjclRqa8KJWWcMoKfembfQfDQeQ3b5G/ElDt5b+8LZZO3pmNQh2A6Az6m7U2SbylphrlShT4F1yXcihhlSpAvdbsWUGzvPQbr+uCHePZr1bFag3hLUt4KNRKHXI/eCuQ9fW/UKx0zP9lMi+o+TVW4hAVBXBaas3bh0/Ng10BEfYZK31xTiRDWTYG5ehYrFyN4QdBqgATJKkK1takxKZN0feehZb2fQDS9ZyB9LMxdU/lOkj/vm45/NwLf3lb7bHT2/0vzIWY4+2c3EiseGulZ48r4J0VoVA0rQoQfoFC8R1a1Sm6jMofDmxPqpWvWCR7ppe3pTFo/jKNQfMAs4vKvmParG5jzLPjxw3hSHXc6JswIYnNLYC0lb9vkXEYVH03X7LjovBXcj5jje+8drH+fIxoLGdROdY9zlaurdZxjPnEtV2WbNMx2Gn5IVnhhe07QfjPW7IqzhCW40JaUrO6enNwoEGf5w+ttO6HnWQR8Vg4yjPiGkCshZ8oOtrWeM3BHXAQqM37pGjlqsjFKVb4c6enPS6IBwRG96pHst/R5G01pPfV4hosy3iDd0cHol35juMU67NpQDWS7Zvksr7l0asxKHtdD0EAF/YgmJdq+bXwWYX5q77+fgE+5wV4kda387caLNorym5rvDXbzYD5G1WEMZLYJ4SVOGDl2Sse035RTb2o8IjeF3m9mZ+5ccvzNzp6qG8+27PDLYLx6ru8Q2v0LSoSgsrFOkLql69GaPbrTt4WJVQAzp0l6tBvNbwNc16bkvspbhfdjcY9N//tuX1yPrT1F1I6OE2l45VpKMsZOZ5rPMLCrOAPealMGRfI4vIxv83PkpisgO3JhfChWqeH+YpRgaZzhmucx7qEw59R9ZAXPyaf3bMiwYLpiS7ho9au5Z+viuI8fozv8KkAkZeqnFErkvJIG4ET9ZARXilrMRl7dMBi6h5tDfusQ+LM6EfGgUuGNIJSj2UM6peIqO+W9V76LilQTCKzXnVI9juqvC3t/do0BsRmYjTv8v4ySbJU88fw/u66nXzMCQJ/spqYYU/yYo0nnbYwbXu+WXp4oUUWWHm3aPqN5bzMbcdIZdtxtsIzG1zDsjvefIpo4jnPU06LZS+a5W09Azv1hEftwyj+cZrhFuIfnmEDR1EVpfntKwnGPYsGnWDwxj5Eztpg0dcLuPxLWA90/NfBCjq6tkLIJKeyxs7EXKcaUONxnIXIfPopyuqbiNoUiku0xgqF281RK0sgdHtBIm9rmf3pW4IecFfVIXiTtOyqs5ofoNsEpAF4mUe15+M9H8/Gx61wD8DGHKY9F++5eDYupooSedmgVYKcmXiMaM/D8rb6U8WbQKeTt8vcNJnv+EVellgHT/25jEe157Nt5TPzfbRJTvcJVXe5i3dpW798NUK0ZwzFhLKEatJC6ffVSbY3kh3nHWJMZP49cOLBf9SoRqtjzE/pQVVF8Z1j3tk2tKN8BSLc86S8LYZg3vH6uCd4EshV14ipiBGImxEB1FY0qpXCQF4lJwkQzaDZ0/F+yQf9mHh7eGnfE+Vw+5SsEWlzhpblIf2JJGgDXOhXDfhgUJRDCbZEBaitNZRBZdH3US3dEJivhiNha3jZRUf9DCKp9/JZIemKJC+8H6cj7DR/zDRpVetNEiSMOZ9/cJf5LEM7R5v0Kcj4jNqZZUxHR5M3cRjHk7ehz08USA8hTqTTu5CG9HS5wMLtski8nw/AaJxyMjabXxzntf9TJbgTK3J5F6VpPwsBkpv0OzOc4CSwjsCnrjfXKkwHxJJcN6KrMTA4kBGMSscZA3qpNuNuues2LJ69cqNZpqrjxS+TOHy2hlfdqcqwcX0AxPwjPMsLPkeGra9iiWcyDIGsz/ZlqUmVPlHLTXTNRZlai3mReO/QTYRXNd5V6VrAteyQWlzrrzdRcuvi5tbLqw7HXlbJ29JIi6kCc7U65kQNB9I5l3S39vJON3cTaxaRhztCvww5VPvV6LwaJ7KI4lNlo9o1SQPR9An/n9cZM1S+kJDHu8AxLF6nRekZ8Y3DVcRQ+61X7Z+1tY1l5Wf0UH5EZKl7Ouj3IhPGuJec8rZginkn+l/oKBdGnoS1fU0Y4vIJRSVm2uZRbK9I2BGm/XpRrBe1pjHRq10LPxp2Tkg3dUzsdI5vLXO/w8s3K912GGGl9Mj2i2W/WJ7TYjldb/Kiwq3eJE75uUb194tj2xbHSZ5izlmobcwRpM0wEcthMIVJoPQj2fh15DL6gfwwNNkEvmT+50y8SE4SlK7IXzMka+q44ijPbpLbuhh7C05lezh+xMKGdc+bMBlTWq+zPinCxK2doxLL09PsRuUjEqap9oUKjJy8UREuG9P4+Qzg6ni0xVyNwYfLYzmUcH2sAPV7COQpi92zYRDu7LI/vWJQ7TdVxbIOkhKj4Q91xtc302Q+MsrGMdH+TNPGPVYLae2U5pO8d8oPky7EU8tnI5paXZ+m3jNoY2QDFzZz3dkni90sV+ZeMY9EWL5DmzR/cozP4FHsJZq8rXZX8hVpyyzqQPr1jErNwJQhzhcGbjOBUm/qvWTCNKTNEhskTWxVbS6LKCvXCX35KsRUQDjHMUlUKMFgDgfdxgoVKFxdeyvcHOsnb8n4ujsQI9D2DB5+DTi4MNfIZD0m9+gTk7/Sw3vMxQdNlZzpRhZGxW/MVz3wcK6SwQinKimgn1Puo599clR/r3jI29pi++REWT6I2Ca/WlJNL7cNY76DpObO7xNM2EXDzE/LdlPsFq+n208g462fGUvkIcw7c5wR8Z9kW5zDWNpxT2OincVqOqPK/+XmpkSe/vHUbcwPxWFUxXcXyZ+e9wBneJE3j4xsj1MdeXqSviltoz8GC2L7Z7I5KOK7EI5BpNbw+JS/LjYoR3DYl5c+xgd5aRW3YAb6kY4lNdDLoSSqZAADfb1mVUlNForT8iSNbst+Vn0jwGQtBdNC8erAUjp9InsZs1zH0/oJra9R0QmdNL9++YI+rPT7y58EFhjBEvUIq6+IZOZHfaU36krNAbiFBRxFmwlRTZLiWO49QaMrFnlLC07QRUJE5xkNGzObpk+4I8kGd5ck/icDHFV+YzIFB2WZxwklYbe/0QcLGkGNt2a6PK+6dxq5/h9nqxfNslXWGhb5YIOBKuB9gY4IExMv6d9f/l/C8E0b7AUi02A3BK6RN+Mx4Ua+ZO8Q2cNeHFCdi0RalXG0EsUmpuhq/KVdNCTsC2tQJWaPJKvEA2KSxXjeUpuhcEgMz5mkk31zfAneH1BGjog2c2jSD/ZVT7E/fbMcMXW0++01w6wGPIz1q1XXN0MGBqtIuZeFtmZduKnd41vlOOZiWuW87QTHYl2+3YWAh6RLKdeqq0Gcy9ewYVxNawDzjt/VVrZkQ6w8TfUregQFkiIn+7TF8EcId2qpgl2fYXWCc7AbCxL3/CArH1BxRflExRQMnIzPGhBbbmMRA/wGMfB28BrQ8Zm4DZgLk5YJ/GK8doHP08Tdq8kPeoV1L6yDxRVaHRGTDM2mI+MSfVWII8e1bLjSoD1oG6CFOiXJhmL41IeorRIfUK869NJOQ9AgXVhAK7KALZhTYvlVqxzBDGtXOUcm7bdVFlvEra1Ty4wcHMSGLYgNA/JYzVnvp1evRMOOEwtJ+jAD80houktsMxY9umker5awLDTGDTCSUkqGZyewPzMyFUhrk/ZHFRdjsP5pr+EyRsYBIijEWsNDZOa8BWAGGMuMaV3Gfpik5Nq3a0DbzTF8cCpw6M1JIa4uB2q0b4YPL8rp+stXUNGjhXUhi9CMwqCwfQqUbhQzSCzdfBnth8zTiYvIq8M0vyX3GHoDjwAJ8WUHZMOQIuKdMvZIuz8DC0rnZCeMPqT3R/maXpn3jKPiEh5YxoEtnC0TCugBPpQx+HbwoWwEM7GibH5Mmu/qLGeCbJ4BOkvo26FX7f9ySyQIDhokR5BWRkm4Dcg2CSNfnivVQ5jDSqmcJyNjZVNlaxizfWTMlGn4N32nYEzuHWCxje1nzPEQFmDM8TwZMebwfPcyZ932eVqtrOQBwRNNC2N1lOHxmkvGcIYTWSfmOIFI6LoLUu1dUjaZ5A82eFJIYsh2NKr7F1UliKk6eBumUrYBnZHNGNeLNO1KtyFMV2U6svQtmIv9ADTpfpyjP+qkQI2Lr7bTUC0bygTdr0z7B9AVgNNPopNYMyLdDDLOiEQm/ejqL63HXWG+Se5R8URzB2g0rBGwQodzUN7GqCFeY/s52fap6s18WhhIZyOuYuotzVmHdbZK0WmF1gdVVSTXdYWaqKmroUTHcCY4FHwore7AoEZdke89zJi39fBhM8L51oINCxhZdvpa27NA2qEYnqVl9YwWghfnc+3t4AFbN5Yl+BqeRXNeXvrgLQ7InpHnY2EX5g2pWsi7sgjv7aChp22/Nzf0R14NEwgVFNzmYv+RNmNxTN8aQSkdxXxcKp0vky50dbaGSw1lIg8/MY/u8FYuG8MCDLqDQpS8VnMTUaNrobWYQ8AQY7JwNlwJ4l/Ceq7qyAx8paLzLljRW6YeDWPEExqxJK2okIKuPKdtEuA/VVtbIxe1I5pPQGrn06QrbL2t4mzDHR2qMxM/7/DurhrHQgy8g7t82/7FBsXJTdKkIenNaKYMrK6tYGW4ogNTa3qwg+xtNqL5GN1sjneB5eGRfKHv3V1JOFLGfg64wMhCBRqrQEOH7kAhPEbLcvml4jHcGRaOB2+Y9A7GsKUbiZLB3WS7mryLbTnKbhmvNXjpL7/i/Me++I5lwjfu66/Bs/Qq7Lbly+hWkR1GhA3sscFilqtguDhg8pcG57eoSKKs6qflKF9fJxkFtPJnMcWjIJwChQNNjTu0tIOMbUfnkwu2c2rSs21yq1GNz/BAZ4BiKzh+h893FsPajqWxgyc9g1H9o45ohtivWeK1Klg8W7E0Rh0C1sdo4EtuBlBHt4PjoTk16Rlbb9t433UHcBD7ARj6OQn47ZHqz0iUd0PRW+pMESzD4EpznDDGXWP5JaxvtvNtsQi2xtjGD46m9r6ScaslayqRWawSiifAUlH3R75sdKt2a1eP0YCXW0lG/GGxqngUSy8uq6OBqf7vYkXbKk1+YXX9WejkVxcRecvhXf6QpXm00vLWGDwwd3HIIbNh18/JeQvuy4zcBVPa6DZgVHMxDrtMUIFHTXKksrnVZUwAg0Mc1kPa8JgEvWUmeMpnS+/R6qHMwKLqqTLpAFtvCxhUZ4gQICdgy100L0h7PysT7q4J4RzdJ+jB1A42hlbsvQ2gww7MtbBLrKgcwXzbNjxHO8eSH1C6uanTjOR8HTOVEQdJq2uZlqnpzL/y1uUMDS+ZLWNr7cDm5nPtPFswflNxMfb/jB5KGmOqTeYqQEJM3QHZMLGIeKeSuUq7PwNXSufEpO3Fk7mS3nf5P3vGUXEJDyzjQIdkriB6gA9lDL4dfCgbwUysKJsfk+a7Osun4Td72A0GD56IfqEH3I4fK1RkUXpQV3eEvI2LJfeonJQ2RrUhUqkq2pDPrAM7lb3eakgzrHerObaxjSwmAE7yol7THNRaBhdBIW7uoWxYF0BtxadBrMHyTszAWXLi7g4bXeabJDbkozGslJEomDUnccgXYiW4F3PxEkzg3WGmK/rv+yKvN2pOYgClbGTNQSxSgH2Yvm3dninr/1yMB8yHSdNDrW0QYg3XGAiZZshTiK8Gs4z5tpTvgK7PK/BG82HMd1ugfjH8oteSmAGHV8EY5DLuA/l6S1hQMoZZdThxfkyapxUWY8Uvxcr8SToIGGJFCmfDhiBi85foAulvql7MwEgq6po0P665MEdpzwNjsIBctJs2D7jvs3HdTp4Yupz1X8voFn1IcG+Kpz4RvpTxlLVUrx6wFVzehoAbVDxjsH1cajSUGZjWaA5N+rH4swfgSBrJZ8VPzTKei3ub1gDWBWX2lvLtaBBLMe1o3kw6QSssu7nT+y41j3Jw0u3d9gKdx7s7LCjp+Vw7vDgXu8RsOvc5AXIChttFlzlp72dlux10lXuf3FRHUbG6OsNdvotKtPqeVHcDB8nYRVMPYsuuig1X6pqRiUWI+8MFVhj2agbeM5wGI04EMSzOmCMlomchHb+AtVRM6ao1qhsE2FO2CpaXoUZDmZGnlXNo0o+uznbx8Fd2idkx8qjqbNw8bnV3FFHzwSzF1OB8mnRmVHFZtfVzXiGTM9IAJ1VZCYi1ysrg3R3WlPR8LmVVnIvtPyOdowe8fM5yjKDs1o/W9q6qBLEhAG/DkMrmdspKbzKSGbjVZP52woIPDcRMEdDWNBZ74PnHpiG3BSM2a5NU/C7ZUPdzNZHGYGAq8BbCKu33GOvubC9wx2dYr/A8bP/m0vW7OTN3rKLjixG0iulsbXFwA1CWawlXbw8LgkOYkRPBOTJpv0ewrHsB6cbG2GOFgw7obMBjNvdZCWd2U/ZkLt1ZRmNTntpsgefKOarqIjtHf9TIJDICBofVAQbSTnMGm9gxnVk1hlm0ZdU87YSePHTaUO7JKgQP2gspAK00k5w2exQVFfNmtfJuRlEH1lLG4Haairwp+f2h/EnpcJuFQc9mUUO0U2HSi6HWgioyNxLttiGtMTkT7ub+oR3GEvy6k7uIMAqdW4WswuScuou+FrpBLMGmO+h5cZan6be8wqNoA6zJh4OsfFA91yuvA6Yj4sCt0hApmoK4dej81jGswVBm4FmDuTNi277Wckr6HYp/5DWfs1j4LFfaDRGASjxY10qlN20d0h6EMW4dt9sObwbWt51vIyWDr7ygNSWuC0yK27PoiVoZT7OE4NXd6yhqwbaVcQU784qqMfOLjSAnM6POzGIuMZgBo34w9baGC7srPIFtTHlEhsCEN53uzg2bB7hVtzSWF8q2o1uA/XXzbdIlvu5iq4FM6z2e+4/57RXzm/CMdAEo6kA8z4DY8LmqFcimyHV+6zjbYDwzMLPB3Jn0gqu6FeyrtbNBwBMx7G4a1lQjmJk3d9KcZsSFOu6z5DpXbtuKVw0WYrSdZTCaT+Sivi7jImkeDDfLsgZWkaaMYaGtU8fATS2Uek3ZmRkYTU/8nWA7PN77qEKfUEmc8q9Oinyt5TtFHTghPAtulwZe3tD8bGfQmzlMqHrim/SCrbctzHeZ27LeUGNSxmOaWZztxL7Mz3Qi2U36MNRa7kxxc5Ok+Au60vnUCJDgaaIDsjpLCJhnz30l7cIcJwEZYY3Opgs7DR7EKZcJmgxKcSqFwOFzaWp/PSlBb5lMffmjgnocs5xOVfNko8eResu5fFR5QZ7PStZR8XT8GN9F2S06x0vtqC5wE/GT3PdDVxN0AiGVrDw/tK2ArNv2fRpRaNynGdjQeBZM+qJAsx0MSv+w48xRlfAsOUa/MC+CnZmbCUGCW3DfqP5ibHcYxT9OM9yX+IedgUVXEWJBSR0bptQ2u1OWZ9PRzMDbpvO5E5Yb2WB0fp6aejPz9C46fxqOZUGG3kFX0H/UqEar43WUpAdVFcV39N79JFEcr+RVICYGoW1YWNGc7dPmS3OwfigzMK9++owMmcmCJy5mCFfNQGJ1PmJZBQ3DOrLpuAmASUd93jo5qxvJvDwKzpdJF9h628CpzGJjWcxO7LGEmU/Ysq0C7KxYMVvFzeoRLSZ6gTk16QtTbTH2Pl1v8qLCfbuh20d8h1Z1ii6j8oeUr+VVIIYeQdswsqIZKJkF2/NprAz6Ds3AgHrim3SirZdkt6TmYsx3/GjNfPIq8POhjsynaGYZ5tN3aAbm0xN/55gPN5TmjatyxyZqnhAryBlvgLXnPaAdSA9VMfjyW7duKLPxrHzWDI/4N/Odlo5xneoJ16lwDVR0q2YdFdWX63+huCJF6BEzQkxNaVGW5RXF8uvXEh2lBeGP8veXVVGLzExQX6CKfVWrfPmi+c7wVfuImcCjXPXo8Siq0G1eJAjE0pc/aXHhXyS8EULTFmlRfMzjKE3+RKt2BuFO8VD6rn2Msts6uoWxtWVmnUMXVUFjOEvKfNLucXBa5GeoWCdlmXTvLUOIeRgt0vGz5yLC8d24rod5moK9wt+NKjdhqzIUXfSw4ZBUw9EiaX0pQJr07ie6jpAzrWTVNGUGK6ZNKvMJVXc5OOVjCD1CLEUQXhX3WLSCPRsBGBObiqusUtG8BdGiPEzzW/KIIISrK9NzU2OwBVmps5xrUHSP1EA4hnepdPRRiU5juXmWxFVdgDjaIlOCqDCNIIzJQ7JJJQVaS2YfANOjRmlyj4qny2QN9pQtNx34kCBHMXY27ZAt2j7M+CRJK4kQ09QxbVTJVGMYA95q4D9FWX0T0alX9Z0F06LW4bRC1nbgYoPi5CaJqS7U00/RY7iCXk6C1b7QGyRQbCrgHRszb8aUeJcRqHsNpaaIvkVFEmVDaP1Rvr5OskhGHH0tbcP/qCP65WuWgHKGLXcdhUXXTZswwW2NlO8MTZZg0fk2IYZOCrd3dKAE7u/vdKeHBBX4GAerGn2hFs1n9FDKZHdXpkVy/IhlbBalB3V1R85szSKSK9MqeG1j/VvIEGbmgWoTNNITHPsWtR5RJMNg1gv6Nr20F7RUi4hmQIBwtOkkDHUO5o0MeBOEn3rUYAde4ICxw4+oGGJXITSjn2whMO9amaAhbzxI0TQvbWjQiLndYXqBOeANDj4ynXJIam2IREaxcfpt7WiZ5KnwMEe5bbV9G2cog/vHZ5LTHTf4nDfgsUNMUWSLVrrjyJIvaWkLpZqQcZMklYhlGya49XaCIXQbNBWwEfXmqKhxT42uyfWgtUeBvTLpjRDBKZX8Y6c3rQbDxkrBOss4Lk1HtS5iBqTXEIKkW43MPQe4Eke3P9qZTOVaBRPMokHztVCg6Qv1Gw/KEFaylCKBh9HrdXf4KE/1ymvYcDgCMLBK5bBRovVZ11qhqBO1xMrS+6WbdOJTRCWutC9tud6KBTsfgkYtmQOpxfH6Eq03qWQVQHCmhqsnFWIexvgYqsDJgRhIdQK2Utj7xhD6gRd5WeKKqQIlD6MfeLOdyK24IwCDwzDgoQKfikFPI3P0GqT6Mxfj3gWesUY+dlqeXG+i5BZcRF2ZftU0G4tywYxBjE6nH1GFT4u6pQ5DGvQ5KvHq/Y6S2zuQjCMAU3TvEswNpaSnPIwW6chpBcLIOQnphPFTFqtk8VBscO4f3w7DZ33+Lt8IqWK43K09h465FlZfH14Nl49MHcU94lCBv8Vmc+co6vWuDv2QpRebwkW5aROdlwPbxHABy/spjIllSkj2+ldPRRhaMz6wkpR+/K21jnow9olJx98gX3E3xCL5NDXkg1RXhMgIXIEriKjBDxCSG6s/MfM0VbLeGEAxFBYOpExz9a6ixgjF1Fw0ZK5uLv/hobMg6p4zkLLh984DGiKwqAAygJR0IMH4Xv4Ky0AsC2OsiDJuASJVDGrJR6evDNFOcDFQ0M+gBWhZDSP2JyvrnnDVu1EAlAQBFUOD4EF6cR4UKnKBOCemUJc5REEbHkQ+Ag4SogfjKqMgBY9oJiJw3ixyUowB9eMYT603WcboAOKouc6BQn1YL9NRkTwAlHwwIjBEGMY/S0EYABdAFSmRfQhymKQp87CYiiocqMFwxjUC0IdDOBORWmeuIUuAgkoCrH5UfBUVnQbXMwNyCYgVWmAIenUOa0otUASSD0SAhUjDuNApaCKimlgrJA0e5WvqoT148sH0EODU4+DBvRkGRArQR0pqF6155IF31XviAcozDKlQfMEKoCrNOwqq1GkYK6RVS9AFoFJngtVTCcr9oBwPl/QhEJW4pA4i1iCCurXNq7hIgFHITw4UXF+MW6JKFPOopmWXzhXk6mCzSRO0uszZfopEUcLLR6WqBhGL8S5W0EqJFdq6pFMQhHIDf5rQrYe2GV9XKSTNepzTLrm+Ydg1WkEysILBCKF6AQgHogVoB48zlIC/Grt5S8X7GE4rhkfgCtFuJNPHyCASjV3ZQ9FG6kLOvv4rJ5lRde3gTbAoCKxynddT3qhx+XofhQOEn5W2E3rdRFrFggDjmkYUtyQx18K0YlRs3YqMLgScknSzEq3XrYbwDynNRFjtsIQqCooZaoNSzFNrN3zDeiYTQM1HpWcwR3LNw12sB5HqQAHCyQcDgUMk4uKIFOQBMc5yHB21PA5skjKUvI6WAaRVFSxmTkZtIwBJldgD0VW/RkFwu4Hq16ofIWfdEOB4MQNCaipqh6yuryCuNBZOT2ZNm9MSXBUAeCUL2wOuLR3QKC4g7bGBV57qYEjVDahDB6DbLzOmCL5O1FGdtotHic2Vu9UUnnGZKTtiPKcSpvIwI3QexTT2VW5FYMH0534G2tuGwOKSy6im/6GoogjFNbW7GKPQUsAUk4LS6oBk/SQYd2E+G4+qS/r926S2F1X0O/mUczKrEqXqyDju3Gk6Rii8qMJiWmhiRl0AZoeL459qihxWiPuycFgLTnReluu71pXqq3Fd58ErFdUpqK1UTHmYCelPI2mvJIUWc6HGY00mJTqLOepCiu0nSt0D+aRpucRD6TTcFixlv6mAN9M7FxLVVxfRepOiIYWInDocpH5I4wreFOLQQbrfkCTFmz59OpTx+3kAfSSQ8gHBFSD6sAlbFBSSIJwhpmJoWbHFi0AmQ1Fs49ZkmXWrPkf3CXow0Hk4QO0KGMN7O9XBWGck0QeUbm7qNCOesqMCLc3kNQ2HK0UQlqryZhRrU9aMA7m7bExKh1cRSD46ARaiF5MfSkEoEdXEDq+kwc6JdEhTBdNDgFOPgwf35iIQKUAfKal9XO61QYYSSIWXIVghgL/97EGEqjRkSv97s4ryIRvVhyiqSbSmoLJZkxP77PfZ2ZTUBaDk4xKBIbqxOeMURAKQzUERmmdOTxIOTDOMMbSUKF0GPB1VOHRzkOWKTX8noQkLoxkBAyqhRqQnA4sEoMEonV9I5mizCSo5o4ExmsdmLGF4osEFE0OgqJfQYJMyqsQGA2ey1pmxhBAdDDoZh8AEdqANzShoEFYHwskHA4FDtOmSTiroAqKaOJSuaVMlTzkIXfdVUtSYBnPJTihD59WQVl3uLA9XkA9LWU/lLM+nATXwnIfbUHjOT0bJNp+qIRkbaMvxNfwyIQGbBgDqwczsugTpQUdKLx5Es34GSOkq1J9TeUzzkEBhPRKBTDqvsBpZE2IeW1GXh/fqDPf4LirR6ntS3TF5dUXS6KrIB6epCZGNySGsoJoOsYydQp3qofTIV0OKYzkN4Qr6gYL1VPSzkEzqNgBSSucoFCW/shNqTM5xLcvxjipPSdhxQ5PLPZLlWiP6GRCNnBogpRKvTbmtk3gMpilJAKTlVqqjSnj5kFTVIEpJsooriKZsYWI9Fmpbu0L1lewG67hsLPG6zpVj8ixqiZaSkIOQj2sMKEuABcauqvBMuTDZtPRXQ7J7ORHGgPoxjOBVJNGraDBKyN1XRmXno3OXjExrUeABdWdgDt7rPM3jmtioMHqDQCPNQUiVgIAqwFJh/FKCUuqASCcW2kPn9SwkhTW4KzJgJMvbp5nZiX+AQhNergJXiRBpLVg2CQ9nKOWTHPlc4eFCH1TrUg5sMUjV6vSk31xrVGhYYZ+QwlqMS2Gt8KTYTH4ueZp+yyuabplel7JPpgLOLQpwhauJvJa/G4sCtyRPqyTlq8uOAL5bcwU8lQPsEKZ1FfLdEAVIZNnjPKr9xLQ9aJEDbwwF0FvGD+9cnWYJeVVbcYRSVVApHIp6sDIjPBuk1GdU6Kc9zYPPIl2JTxrpiSmtaz5wGQoTEpue+g1bBKiunVSXlLPD40xXwkNNIs1V4PJBK2qBSWhHz1MpiKnCC+mYwltVQcmn0o9AOLOBqbQiJ0rNpQvpaGJICx0NtGOfc8zC22J61yMYWuMNAVaSulhwD7zoXC1g5BNTbvyC2tVJka9VpFOBK7Q1eS047IJ7903pzyxHPS/pLnMLwjHAxmMb6gQmGoN4YpL1b+xdKYwoIpBCwPKwoLhmnv1TCWsB18QGk/51P20MjwRStfFAFeC9LDVxtpUgnMGl+YK8pIfPxck6Kp6OH+O7KLtF53iahrf6gEO+tpLiTK6rCz/0kOtS++rxgtQcXisMS0r6hzENx9CGgxxVCkG1McKpySV5a1GpW2jryEeqqwomb5e+HKkgqbahqfPcS9pXGO10VewHqzDhBSXqPAY98O3Gq5ME3k4U0PIByitBNJS9OqmgoKKBiYNpmZavxo9MKmk3hjUa2KiKhm7G1BojBWjFPaw5EbeNnvo0ZTq2ki1rsCSYkgXZdsDQd/mUORB39PTl1fgtaZGqCmj5MOWVIDryj3Uq6KdADN3yj9/RDhD8ZkM5BbR8gPJKcGybMeUUiGej3PCi6dVZ9xSpjG4ArG5wYhU5zUYPsGrJBmCGpKByNhRU++11g4XcTEVJhoq+7LfXZCbWUfvht9cYJEabqo7ST/kKpWVX0L1OO9Rsv7y42EQxuWT5HxcvXzyu06z8/eVdVW1+ff26pKjLV+skLvIyv6lexfn6dbTKX7/96ad/f/3mzet1g+N1PDoi/8b1tm8JK8/4uM+VkqfUVugkKcrqXVRF11GJ5+NotRbALrACXn25/heKK3pF98hteb/1pO4abHMwNJFB4hwSaGIQ7sDJ7/acQpqiuv4r0qdXYODTQMMTPCwy+XSEiJlxST1c8yKO0qg4ax9M7STvCo88T+t1NvzN85689sVTWaE1+T3Gwn43x3aaxWm9QlijTHDtaMP1TCi1wFyeRWX5gI/yuKBC5EFGDjkEYI6/qzxGOnw1x3SZVClHzPaTOY7DfPU0RtF8McfwCVXRf6Cnh8ZEwmIal9hhfIeYF715pKNCO7wAzZjP5rg+JmvMWqvLvDuisxiFQnO85yhboeKg/J6sqHxm0fJl5libGv/MM27o7HdbbN+LaNP6IkBIR8W2uC/u8gdgpoRCW7yHObkh5hc0X2axloskL7A05dZy/9VyLV9Gt8Bypl9FTL+95sQ7v4O8FrYQbkPnNySz7Sp6VL7hZLFr9ZgEg5nJ3qWqPc0OJu5dtrvWu6TcpNFT64jBYhqXbM1s4w/EichvolskDpMsrbmtE0xdf8Yo2k8WihIhAj+Q/uPWsMbHHHc++ROt2u77igMen4tQMMAxDec0feBxDF8tFIs2ZRKPi/1ugY0QBGElrM2pMcLIlTlglSC0xwWsm1HB9nB9n9HKi9clqbpMWFxadVtlYtfjozpt3rqB2LovNMf7NUv+qNEFyskBfYyVKzLHeZJGt6dr3B9iVheHDhRbqPZVyunz5MPyR46z+jpNyjteK2Y+P2MFp5EyF1VBXaVLam8LsI9xGF23Mi2aadZ82D2o6724nMYl9hiBXYMrsjH7EN+os7S+TTiLw7jEBuNlXsfCumI+b80qOEPFOinLZEgs57MCeGwO3K9Hsa27XVgz5/D0IItr+Lo1HKRLJWnOPSoPLgPOUVffVq45KRDqwhE5lWNUYmFQih6PH9F6wxnnmM9WuNrtu3HA5xCOysyxUudvDlv3zUbKNiurcQ0cC1q2ZOoVvJTkztPUU1pjDC4SGqy2C/pIKBnfXm1ATNIXLaOFEwv5l+wDFoNnNGn0qINcmcV6TdP84T0NQ7/Mv+UVv3TF4jnODXIrGmZzzODoaxXzpjS2xMbGswLxsd/nO80tKG+6SFFfqQNHyBrKHlnlaSQQaZHH0H2bU/J8rtfXqPhy861JfjRCNS56xmd2IRLalxHZSGlHdlSjmI4pm2UAseZQEn7iNDTGR1z8+8vNf5Pp9u3M/XcP/b67W56J1l2zPBb2u4XSuunje0ZdGj7bKMAHm02R34t2huG7xTgLhPey1ZdM2ObGJRZm2s1KgnFcMjuX8sx5mOa37aMNDnyprD0RTzbNXRJXs/FUsQUWvkB4CCSfNd8r9vvis6R8L8ZEWKvrTySpm0YFMT18ntfrqxm9yDjsdwtsUSWYLbpv5ljax3b+C+HjQxVxVyVCoTXez7kcbV+2XdzNPD/ky+hKVJPyfNO+hPOHQgtPrqhsR8N5cTHfF59H5gEgh6lT1p52LxGFy7hkud2peyuJHyf7feuOKGFM4R5q8tz68fs6kWjITYmF3liS14v4A/Pw1cJw04RcjWw2zaclvLa7Oid5sY5ElUAotcd8EaUVjLUpsTD5rdZJ1kmisbVvVGJ1KQpfTIwKLHrYJSXgCTkqmPtS4h1KkRA30H+0v9zo4y2h+42+cKlLyo8RPhvAJ1quaMlzKOnKx/w2yUAjrlhqh7nLZCRFLgBszV41pOzw2askuUgMtippzWl2KhrKJbI+89lu5kVUw9d59ygsjjdRxvsKdB9t8GBxUgjerMxnq2uYCuFv90kWA07NXKFFH4WoiyPLiIuW7d7w+1r31RrTWxDTWxtM/0w2xNASpaJLI1dkoRXc5RlqLgc4pYAtsFg/0SOEjfk8h5RfSq+na8DX1b1dSS5qvazmNLJSlG22co3eCpftY3DAhfFQZIsTdpfhyyzsag/5R1RVqDgtAY9isdQC812BkAo3UG51JYiKJAYx82UWcrumkcyX+beIUzvHJbvmZvzMruM7Rv+Eqrvc021zjMslWkuDYFtllDTW2DHOODx3nt5mJLHXHUnawN8Zjou2hzNZ3c6TMVlULnyprj+VQTWcEh1gG76+LtB9k7yD2zBHJbsmzhdi7u5G1I+vOyyOt81w1Wm4OawH5nYkrqCqYmvCLAEtciiywNm6XLR1j0SrHQxhIQvySt+IFMjG5/uWI0rz5fl4YO4Tl4SMIty5o/xZl10ywH29+/X8zLfxZCSSu/iuyOa+qcAjo9HjNOIedHGRwJi38i0pk+sUnWar5D5Z1VGacnIfBJg1HOCO5liTrHux1MLWVqepFLFQuOS9XsdEaI31NfE2DiheOpyhqyNXPmGI/Qaj4S2qO3VReNTBB9SueAh7JavxMbuo17CGxRQ7qVcS9DCEfe+pyxlMHxDCaQzyRqRADjbCg5i7uhqXLK+cXPyouQ6SDxbrI8rqmygmGSoKvKNV0AWIDMa8lfcVH3refLHxFxiehh+7CgzfLfrT1oGUBr7Mxvf0jzop0JfqDhW9DsZ5oUIQ1i0M6gaMf1RusX5rLLfwwo+JonGwWnHY+LWshbaZ3S6pNz+7w3cLo0tbh59Z9ruFt1aWNsuTyTs+8tsCym3W32MX3iTBD0PYU+P4cZMU1Bj2LnoqYcrwMPatUGcQigFaW3IoC+0mKi8irGwhmGWAYgtpOqopXOoLpVa9Jg5+B7cFQqJuKpbaeRP2FUWfVKDYZl32T+DxC5MpsJFfbaWjpzhFH1F2W93xEgyCcG3hDBVJLsyjDMahFapgUDSCJIYgrPzj7pLNcRbh858gFEdFNjjlyRj4MisfkYSs5CjtajdXNoLPiARqOQ/L0/K4FGhLP9kYE/tknTybcUVWOhkxPGf3eMXiys2lI49dCmRjxszjH/+oI2q64e2YoyLrCw9a/+A+StLoOkkF9HIot5bgQcAQFvOQZArsYqnFaSB/aMbeuk4Klw9AudUpKbl5ovaOk7zo+neI8NlUOCnJAS0uLKL4B00xTDLfCyF2fKHleVv+JI9w8DZ/vUfeJjWF4KlN1vUanncYwraF6FHXAg9hSTe0aqsm/NYEFFutOrLvHNZPh3VVCW4FQqk15u9JeZcmZaVAz4NYUKaRNSnC7H6Gz2GiZQyGsLgswKchWjWJ+WijUYmN/VFAZY3jS7oC0Axfra2hR+SGFrKDNgUWe9AGxUmUAr0bl7hh7K/jLpM1cFmnhHRrsb2w07bHw1lwWGtePM4qVJQQo0EAVrseETwjLAhiHyWg1QnYsD0VoNVJ7DJBzTrkJCNXZLWfo3J46v4oX18nGT3fAuPQAluNBcvE5nWvg80mTfizAghgjv87Sm7v+EcQ2m8W1AHOefYnu+/JikfSfrKgFzCeD9bj6fcItXhRgDm0pRIsUqAlPfuCulEFijrbphwgirEm96h4IpMo2M+4MnOsnR75NUuEm2y+zEbzL1G7nQGGCrHUAfMnFJV1gUjfJNhHEA4tHKxFNyOh0AEv+aHEzQJY4K+zVYro5aVo7RMKbfGeoYKEnMNGIgmIYxuEBuomegjnUeSNqQnLX+VIWLCtcePpN24vP54Oi4Mjj7zqNJ485F9Pf/e2x+DdA1dmZWHHnBJjEglOGlyRfU9liKFye+yQ+OHLno/bY3sDWR7l9SbPxJghqNzK7R3E6oatmwV69U+Vfxm/jiEsPQ6aOzasLUAeB2zh1ki9QE9weby/tXuPbxF/ORHL8NVG+QwbMHSYV/iwLsUKFNuYOZNIMG3STzbmumauZd59UPneTU6Nq3GMld2KiaUWmBPqhCSgHD7b9PIWXSR/8pbV/qujW2B5mV/gc2lcwfh1sPb9/wJdhwiFlubm8ygTwvFGBYu7uE5s+NodB8BtNLOENybthuHmJqrT6luCHj4JWqJQuDUKVys9PaNFGiQu0SKymtMoW21zh0kW8W/PcEU2NyZrJD4fMny1EDsoJ++vZYIuNyqw8XD5jLg7//aTlddNEWVlIjitjQrmWHjLhlOFWScjXO7BVXOvmoBZfkMqcTsXXt3pJK0poonCDWHeYxB6WPqUWKZhra5pmfnDDRszkPM6RbKsgAbgVm4AsMVtVGBxdm7yB0nSLYilNjp564IJowaKbYxwZYVPClSbZV8sE51w5XAurZ1JQyIgCKcWoifCIF1CGlkrHJRLS90EgGc7BZjbLIGvcoMAFobdx6qIiIDi7LrD5+0RycydqqcsZjC5CGFl9W21PeK6efEBPQIvlnJF1tt681AsuK93Rc9YVWjFVXNTSa4pg2ieAzp35VOFY/v1z/bqF8YoFNp7WMC+FS5KDKyrbM8to7/wmDC/QJJWqOid7Lg9VCy1MD0mK3R5V6+vMyGvLVdkjrNN9DDG1n9cyBj4rI142yLUex5sWDKwjOewhxD5WpTT7ABDB2CRLX8nQ3pt0w1EyK/PFjjgo+qOFGlfamOBOytQc1MkxgKOiraNzwNd5o+xudzpazFsv94iu1B1u0o9LU+wyK2HeH2er4Ti569qs3kugjAsi9CdadVYtp9x2f4DUayjsj0DOzFwOM71ZNkleNX/GBLa64mlgsyzSAaz9y5S49r7AO2eD9De92V/bB6XuyloNID2Jomp4zJjvQmgqsGo3ZU2U3zbr77BI2mkinBzqIG1FKiNiUGIEeLLbJ0ROx8KiT/iqPgZK42qyfJ81kCB2eWVAyt006wouA9m/L9AFNRz4MQpeDAY9+3Q4WbXuKGVvzQpf4CdHeNx38bBykvP9LLz8i0qkigD81cEmS8Ffvd5tEI60Q7imanXP4fuPLl+Q+Qrem4ZQ3Y118BBWSa3GVr11hU+3g4otzpy70ymi9MSej97+LrM2WzQlf5zzfkicEUWcmqCrJD0xPalrr7cUBRU44Cy0okgW7P7sazjt8+xmBx2NHX1pXWTuUzs2+UiKJcabdwXLzb6z1vD3xJFaRKVLpwet4sGtbaS1IgAlZtjJ2l/8CchRzL73YaBu7T+PAcP3x22q6M8q4o8hdK0yGCe/4E37JoLsNhmX2UhMjost1Vtl+CmfmaTSO/GnT+YCJegm1SO832QiGAIbNe4mQZhXPxRRwUqz2+vgQgNttDymH+w+lddVuJ7FkKhxaGdHq5liMXSedwo51MZ6XKANsZRgc3de/ZD+oahUDhluMNS8V39ozk+srB/TcZe7smrTiPjuvbe1wlvVBmVWPgLlP2rN18Lzs7Al9n3U0DpiQ/K7cCX2cjPrEJN1kBedjIFVoEpQH4Hl+QOx4+4/VLQUpjPNrJxnyhCLLeUNEzucB9R06NxkDWKutt/MAYDkR2Cj1Vh5+6h5uHi+2xuSBZiZbz0S/+Y0w6LAyPLq07Dxx+j7LYmDyrxGhLz3cIIIzq7Wju60uhHICDSarOpU25vbb4s42K57fljG8fc5oVo8GHfvsgCZ/sSbVv3SLyigyEsjojDc7fyRqRAyzvxhndvfd6O8QvtCEStLLIoPairO9xo6w91jmJKS59dQoXZYeewQzfNbtJpGTLtwzKZylqING8/2R0WCFVOV4QmNwlvroDK7bG3Rg9dIwCYeVtfyMRe5j8QtwzZ75bYDmKss5cynKNSK834PlmhQpZ7CirfmtV+khf1+iwvPa+6ezQO61hRd5pFe5lvkphH0X9cavGLb3xYP+J7drBa4U2Z2wuZz/tYFNulQdkiwNqgeFwXh6TyNKuDtsij6D8utjoICSBr+ajA4ojSZHfmTifdRwulvJOeYy28/2pxH5DgkzB3E9B8sjnclhVpWDzcDt/tsclmEiq3x06zoIF4m5K9zLKTWZG/sHKVU7OKqPdFXm9AOdWX7IIzgUKwtHsPL1m6z0sIKbI0QfVpVLAXWIY8sw8hnkhtoyIggNpG8biKQ0nlaWTi9kmw583dU1lPF1o1rRu+z4JpptN+rUjqTbNMaGNQPNSowBKf6IDBfF7uWjTMyavNP97aEISkfEKpzcVXk9ZaghootpyXiyqqagEvV2TfXxitWGphQmxyicOIhUJrvE2Ocql9UgZkz3FHdVGgLH4CHk8EIWxaaOqdY6HMY2ZL7Pt8GT22+xFkXpBD2Xj6gRGYzGdbvq6vq7yK0tMsTnHHIPbmIRxbOH7UtdBD2LdwSer3zz+oxgJDeraoHBsMadtiKxIUY+MhHFtQjIWHcGwB1xXXHgzhKJ+wnE+IjhmlJwiBJDMAD9E2SEwD8BBtg2Q2ALewpDZVOM10+GrJHzDXuXAanA2dK7LtHVnGUP+a7+bYztFNna3QCoq95stssD5ExeosT7Kq/I4KhOeWd8CRgFjsc3co/pHXQ3CD9JynhvRoUQxaloDY794ydy6o3MKZ5+YmSRMgmeOowEEj30g08o21+xI5F2Chiw++WBwcYRaBVAw1pEX/cS3AMbr/aodJVEGHr5aYgDG7jfBTVP5AKzU1ZTB2fT66v38r9rj5aofp+HGTFNTp5VOe8QkwQABX/P+FIoDKfLkbB79LChRX79B1wvvLyYBsTFB9tYOY7iAf8hQwR8mgfFqCOEgO5dTSYZT9EI9aIIAzfnGxggBu+E+P5KhJmRPW9jUzKea+3An76XXEm0f5Qvt9geokrb8mvEOMISxWWo11xiL5ky5TGp0RxVBCRxWcf2sik6oh/Vs8R6WQjUEHayMdNyQNjoKeMIRPC9CI5FBWnhC9lqcYkALMxsm8iO+iEkmtsCCAzbkqgf3ERwX2Fr/m5AXb+7oye6zkwIWX9Kaumj1aaaUzrmRz4RMmE/IuXMuwx6hztI6STMg2KwExb+NDRCLp2sP657zqk0mO21GAWci9OEab6vIuwT2O8GfqJ/whylZf7oVDgBp0a66gukP+1xKf1z4kZeX/ngyA0uVRGTM001xYhX02l8pV8ELEcntyXvILMdf75Iae2QIyF4DShbnM0EzDXF3bPBb2u4XULtHqe1LdgUwmFNrhBV4dZT7/BRg3DK968OdsgcatCshwC/AYnBzKnvuhez++zGJnBizE9pbh07LrAU08GQGZTAAA+7Hjw/AGOp9B5TbaVpxsSBoAUY/lihxwAlFSfJmFLo4yctIQ1W3muy02oIOjAgurJCpL4fGC/qMNNw1kpwonlOZNAHjGUrWXGAFcnhwD3xV1J3R9Ig1K3JWGovn1ybOA2SXC5W8gmVdpwgbwNlosdcAM3jWLpTaUlPXXta/yfrr2EbzkdrjK7s5AzfYKDloC4toGSAYJiIXKoL2a9b2SnSJ/dJdqCchCwBXZbFRdVanaAwDYeOfGKBsSWYlp0oRii75j+fkdyCLPfrdwu6SvHZNdhnO4ZL5by9cjEmULSdimYLu2Z6yIeJ7QezSu2zNcd8rt2X8rpX0eBwLk1mKVOjrm3eUT6L3PFj9jTZG1CoexG4kYXcxGRlim4dSw/uXNEDip1H6zxXKISzIh5da4yM7uBJ3m2e/zn+V2bgWRm6smnZLPqumwuDzsJa263ZL8sojiH3hg0EUpX2aBlbg+QnrKqMDyNhPB1658mdXdJH0WBkQrFP4FVo+/sYLF5LGK5jRZ9G0Cd+/ddwcDCLw2re3HO5MZ+RxVdZGRhxyQb6KfESontUVZfyI2ChRYF5qBwipS4exe5ygq8+wkL5rZ4m3tXKENXjrtiJ7deROHUOiMV+49ogS0nzc4haRYasOp0c1Nc/DlmHX4bmH1Wa2TDHQrHJfYUJpZvnC4ogRkN92JFtv0c/rnERa+ITb+MTanzV+HYrsl91lUtKqLGF3Lltja5yCM4xIbZWegMXQlDZUvdmjf8rcNu1FhFkYFefhCSMQNQyyx2+7lHRAO5iXwBHQOEs8AxzQij/zL2W8jOy+EbX4ZjloEousUtaICxi2Hsun3ZfR4/IgEMowKrC4tj/Dauc2LJyEH4bjIQfRt6Tt5ctqGeiR3R9PlCNIhwCtsMM4QomvWl9eE1gWNAwKYUz7uXzTzkwqLWbHiuiDBoG3oRKjbOAir242cIaZp1h3fvHguF8v3gQPWXBeW2wKw2Wxy/SlO0UeU3QqR5myBJb4zVCS54IbDFVnebdHafHoTtsDKHBf4IZtwmlOo6L3TLKmSKAUXOF/2jNc5nQBc8DG/9VviDCKH1a2sPc3CZpoEzx9i8VJWJzhC3t6KvcvMSTAHY1CavcWLSWEMExmDaZRz+6wTJ47HRXOeJo4zYqjg+tN/3BoW8pZrbvJsRjmGm/qI7lEqOPUy362s8UUF+myNS8wxkhf5QISjAouNewM/97Jxee4l7O0AHonw2G//0eZIc4OKAhUCrlHBblrapVxSkJvTLOYuFpjPNvslfbn4Q1TyKjtbsDUiiiZNZ/OMBEjgzqJzzeOuxjHRXse0KcZriaVLLfVQrwUFe5Nr11S7s4JGsbTy34/jx7gc2F2HYLsv+U+KfC3jbr7MhjNlOMclVms7yNtTAV6MK89RBNzrRZY3ba3Z4fCpSbElhGzxxU64+whlKXoG4hlLjD4HqecxsEPjcgCU153IRgE/EeD0NEAowxlkyJOb8ZbyJIvv0KpO0WVU/vD0ImMwuXiQKatPwzX+h/sDzOXCC+H0k42AybPjxw1hVDHlMldmIfyFdLy2qXhtDReK7XvzJTsuCl7wjwosZg1vYue1KI7Z7xZWgYh6oRaVgG9cYofxOFuB+Lrvlv2r6cvDcA+ZMss+ijPCfLbZgj8kqxX/YvLw1cpr8Jaw+hkqYuDinSu0xwtaV4RCC/tD/vANFeKqZb9vjaQ/iNMQj7T3aJzswtK60wj4pm0ex/DVFpO4YbDf7c/Y53kqzUzfldksxNNVKtwcNt+2hg2/FkHYsEfjwIaKun8tNrxIay7HafNlEUc/yUMN6gcalkq6hzJUJHEgd2Qem0sSPi2Kbefs/0BPzXuYI0zDVytMAhKb+kAqSes0krYmq4X4+PIOrdG3qEiIUu/HxCNUDhysqT8N+9JGuVNS82nOQ+RfiOFaR20vOwP56WJggOttq2VBuNO0vMu8KFPwAM9+t8BGnArFO1vmszmuD+JT4x+s3xnPb/OzJCaPGwDX+WzRkmEQH6p1epivhP2R/W59W9slt/iMqoe8+AFe3AowVs7veME90dXSPZ0phvHBMNatHD/Gd1i/Q/TRAnVjMtCtEW1tp3z9zbuxucR1SKtuq5BTvQfr9g6smDfSIWkkWewfc1wovGw0KrI975/kxTqqqoR/WEIsnS8+SrpA6+s0Ke/43YP5vKRg3QVHnXc5eSLkOFvhGeXmhSvaGhFG2aT9FEBJa8FcdTVp9W0/ygY20uyGZn8YxT9OM9yv+Ec4Fy0JUgeOMsY0DXMFy1MRMK9CYN/7XXOT+BRl9U1EzwnFJVrjfcjXjgdhdGBVMzTbqtJ9S8jbVXwM2PB1DrVhqRNAG+Mfhpt4bC4HAi2KPRdtHRe1sjwME3HInFw+NRj2LLR1LHSOyFSt2qnzjUhmcTnFI6sRTMM/vUb0RqIpvXHC9laCzept7p3jp6MiL8sLlKZBOIrH5rKxaVE8X65aaltqsgk078l6bkosKpctSV1/oqlnG5U9wisFsosClD7zIhRuDXv8o0Y1WtEHzA6qKorv/HNhgCgd2MUQzzRswzTOI+KK7IwJ0S0iZmiRRYRCGxbntSVbTekkEf3Zum8W+6PwqI3tczb+2uOnZI1EF+rh6zYuvGDLzW+RzbcZJ3mR8Bk2h692gWFiOJgtBpHphq824WR8EJldbbEX3TeLSzS0SZ/4jvQfrfGIXRoVWNgYjzjL4pFN7cOYM0zSD/OH5ZGbf64j9MuS118XeE1f0lxpnH9H/9kOF9A15rPFVQrdtWPwhXO+zKqHK2JljdL0SegkU7I1Qp4dqp+UZzE5iHl19Yku5sTncq0fym0P1qIkGhXY+TCJLkxWu1de8AEu9IvFlXWJikwY0PDVRvErSzFnzvDVNmbsouTna/hsNb536Caq0wpLtRXmxiRKS2GwEMjWrNujaL2JklvPW9AOi9MNgKzqtpptn/Muu6PJjNvIuECXpGNkLvejOgzbytpY32z2zyYqnWcAoHgXl024kKBplL+pPObclUoZxsFa9QYyrADFLrjfqnG/9cH9sxr3z3LcC4m6z+ih/IiqChXhnJhgnA6CzxTRRPIPbF3MPaWCm1fpt0vAMeNhezc8+j6hqKwL1OSl9930GVROW76y/rZu+FMkJD0nZn3Bwz2xssvt3AV0O//vEtxk6S2QeWzuDKlAsefJZ86Tp+tNXpD3km4S39jZESoHbtTU31ZWPMlTPKUiHva73d0jlJ+c/W7rww/hG5fYmsYCpFP6kXBB8s0Xi0ui6Ad/RUS/2EbLfMn4kw773RwbljgnCUpX5C/uPMYV2XPDUZ7dJLd1AdyUS0AsZvSxKiLxspr5bBPcSRB0IS9cTOeoyOb6razT6jS7EW7yhu8WfNckaMN9ICnaBPVVKN0aQX3xlMVhgpoGRC4xTaraE92cBAtpusjrIkZCZg3m81LhUTTi+bES0Y0KbEcqpsRmv9vG2JwKb1IMn21xXVSFJIC0K7HFeJjnKYSv+W6jWWYxeE4eFWyNWDh+JErTO7RJ8wAPN/HYXO5WtSimkRKt3gjElN1Ym/78lcJQalLYzW+YFUglFEt3+xbXN/fIh6raXBZRVq4TmrcUopkMxq8VfRu2SmRzKBZdI/kyq6ua5oQjXNZ0n22vSOD7JPfLJFoTvFEalyx9lUN4O7lHn4TsEaMCq7UouC5037Zs3wpidxihct6x9nYHSl4sBiostEUpMSq0NdSJvXQLK8B/3id4dNLIBaDc5s63FYUtK3BXv1zhAjYT77OrYvbxxAoaOfPZao6IXBUMFex3+xlvzBuimQIqX0q9+nJzUyJuq+m+Wd7sA/f5Vv4PURXfXSR/cjzMfLaYAbycaNK5Md37r0tvn0f5ekNfNVBpEVIg2xvUfyabgyK+E25kxVILzCmKMj5naP9x/i37oCzzOKGGRDGADhWtGtokH75iEz1jeiri5TQ1hfg4Dp4FB/SFlci2iuauGkMIoC0Y7fUCcohlCZn7Xnl3+DIqbhF0IDfqMIvLsrO/vQb5wZxlLu4S+mcTT3iFZWtVJOQd9yOy2Bu/fIW50qC2YKQc1WkAhXxywBTo2/LkG66BAFxj0GVPzmmoNzfTdAx7xchqVbpBEVrMK9jAdO9G6mkrYvWc/h5hgIkHOuc30S2exSb6MEmJ8to/62kw23wV2ZTbzPUYpydNOWQhp51D7ceY1h3D6vYqIfP54rT8XKfp7y9vorTk1U3d6L2Z511SUo3x6mCzSRPi8domC9JsKup6PBt10F0iIgN2UjXgOVc96gDcpOym577REmtueSIO6axL92HFEEMtGTucwVk7jMjcYd9qXug76ccJLZq5GaFrtv3/MrpVH08gcEkaqQHG5CAiIvY9fTgR1LhzQWYbY1pMkzA+kspqyPQIyyMojH4bdcjnctQ8Rw9RsTrLk6wqPyS4H3j/+Vqi1fekums9L1U5wrSVxaxgQhUDvtA25DkDY1wB+ETf4W1UP3VkCCdwukO4zdlFqBPi8MIh9eQjHltIicPj3kYG0o9fz0KdrZX4cUVJhgoepDfmtl/6v8vuQ5sLiVwDp+VQj9z3rCNKkHITxdRUs0InSVFWhNOuoxI1IC9fdJck3bVba1n7Iz1Km3ekO4BPUZbcoLK6zH+g7PeXb3968/blC/oULIn/S29evnhcp1n5a0ynMcqyvKJD//3lXVVtfn39uqQtlq/WSVzkZX5TvYrz9etolb/GuH5+/ebNa7Rav+art2iNsPz07x2WslyNsvQwl8adi3C+SeIxT/32H0hgho5JztHNCxk//faar/gbwJOk7d9fJoSkdDnTB81o3sjm5pRAIdrLly8I25ErwJ71XivRs1eaTTPZfVTEd1Hx39bR439n8VWF+OqS0NssTusVOs0uEow22nRIr8lFk2XXTsvOPQcXVCimL0y7oxt8fQKM8zKp0jAUayKIAyD6hKqoDU8pgyEc5dMLhDMc7YSAZHfuOEcZFlQH5fdkRbY2D0wNhn/mWZgxNui+F9Gmfb5I0jdzXBd3+cNoDtxHeZgTJchzXfY5yRgxZ4mDDoecm+0pzt5qqqV+9Ahb6nZW9kNS/+WLT9HjR5TdVne/v/zbTz9ZIx0HpJlOqfEsYG2oEoIEnu8MvHWYgfZ5RM1KMNMOurjr4PNIn4lK/kS9Kv1MZnSIH2Ea4RXTX0/xZvP4+8v/h1b69cXpf14J9LgiviV5na3+7QVdS7++ePPi/7XuDvtAcPAO/c2lQ/R5sOHlWp73w/TsZ9IzT0HW93SqTr4N1knnFW++XFs+eiarVCt337jMRUujozolz9No5Lo1+q9Z8keNLlDevPqnQm6rF56k0e3pGne988hVov/lJ1v851XqoyEGVPOZJ/rckUys4jRruglYO0clYMHa2XXnsR31NXv5+ZPD9tPRcxLFq0MeUAE7LUma5LO0vk0yj4PfaXmZ17Gc7T2ORbx/4TPhVCOLmKeFbcxwv/xijbo/j/ohNp5r+UXls5pn73k5KRDqrPw+G81l9Hj8iNYbL3sURtJuWE2EH7hfmQiRLjuYj0W3WQyUfzzwuC0pHyGXp+kzYfjFt+DQkrVPyRTA2BpEPST21S/Zh5wEtt168flBmuYP72tUVnj//pZXXsjctFbIJhQV5CoQ0YwFDR6S2KZKyLza0fs4WwXC5HxGsJIBB1n5wF+d76wkIAOylwJNrS2RAJ/r9TUqvtyQtVH6MPXER7re06u9wnkeDMS+hWzHRENNL0Y63YxcY8yOcmbnpYPNpsjv/TaCcQpUuXwzM//Q7GFOyKzZ9JnxZ5Net2mopla0hKK8Schit52HPvmI0kRmzXJtAt+wSGU+D6HwnuTFOqpsLojkuC6itArdz4PVOsmO8vWauTf3828pg5zJDm5ukjTBXO5HOv8T2TtEw5dHKOTy20FCtye+7lWciQ59yi77sdDHqKzM9pk39siD7hCkpx/z2yQLpVJjfJTDsOw1QGnURyB2y05zEBDIFAiTEUKemHbdETGY98d4WwZDS3d2V6aupEZHAFMmDYYM3IOdMJHL3igL49jXhko7aNldRa81gtEj6sibxQ5GI666T0+OGCcxL4K26+lNSGRvgyD7Z7I5y8sqSqE7XjeL1l2eoeaYHATfSfQYEJvH/md+qIHyNOys9JzEc4HaGct2a/U2WJZBrj4e8uadlNNyAoeHy7sCIXP8P9vix0sEFUnM4XaytjbpBC/zb5GX9ryg48MSxtpx/pK/ytp38tcN7OEfikdObzNMvaM7Emo2CYewiskzYZDpNMUF96br6wLdJxHEoL7H713wBjtM81uiEz4TFl38+tks+sjMmGESumWuN7V2Si//xu7aosV1xBrdXKTw57wKjXLImOG502znRbQqDE+1WW9dRJ5vZwN67c5xTDyD0u7srJhtR0Na8L2dKXAXaLAGjWUgWP0wfkvKBENjAZ/cJ6v23Vd33phEKb64o49NhF1pJxg6NM7gF1wd47TvR/tNdThPqA5HqNPFXkqPNI/Orxcr2ughiAJCnjjMbi/qdSDtIwi+Dtkl1jJTbrCe/QuFsrf3HMTbEjNz8aMOvkSirL6JYhImVeAdpoJNyub3ug5deF8lwHlywgZPy/fJTXUUFV6nzQ6H/85+jv6okwJ9qe5QcTbKZeiaQYHiG5QE7W25vbCqyeRUSUyUhoPVimvSq/un5bv8IUvzyM8Y0OLwm5qvWdos3w6d18g+dVc2X24EfE6+ki2S48dN0rwL+S56kmE0Mhm2CKmzAUXoz90fovIiIsn5Q8zqGJPDJShX3+cWFA+MuKAd3BYIsVqfy7hGiC7RYygfqXMU10XheQPUIzl6ilPUiA0/ecfiO0NFknsu0x4j3fwpWq+FdUpvzvoXXHxkWagIJCxkaT6zKO2wNRb5/hSO4mQdpSTVF/5V0pxdb/6Oz7MkhBVvkw5dD+Jgd1oel14kZLK9+DEJVnWI0TK7x0sMI2supDyPVVUe//hHHbWmAQ9J3pymKL6D+yjBdZOUwelhDgf76LR7JVmw8X7MH5qxtl5tftOAtf/k5omewE/youvfIcIHKh+0h1H8g6ZVIpn5PP1DyeGO4DttaIgPF/2x1EuloNsXnplkXa9DTEyDL3oMhY8OHK1aVAkKr4piNifAh/XTYV1VuSw423TJEOjvSXmXJmXlj7BdyynCfIkF88ha4mR/xfo2RZXEXlacEYLgW8eXdDVtA+0x44jcxk3VxsUG44lS54EYXZwwbfSXKJfJOsT1B4u7vVQJhLkzUh1nFSpKb15spdcIK5qYgVoJN2ub+LxwmaBm7XrJfLx5orI6qKoiua4rdJSvr5OMnngmZVbc/+4JgbJ9QsBnFN9Rcns33fIdH1OCo/+erCbE/mFa2vS7UmiZ0yMOK3BC3TWE8TKZOFBnW2IVwZEn96h4IrNqb3oZ1/YxvHR66dcs4W9ADfoxru3Tj8OoRO326m0o6HF9QlFZF4j0Tqkq2+f+6ps4WLOeI6G3ib4Z8mPclINF8rDOVilqssoCZjHfq4AG/RkqTiu0DmGmGSEkZAiJ7+Iubyw/WGpP4IjZ7e7PxPNC64/gklmso5G/XfxrSaY1xoPzvJHvn6sSsAVXHLqm3CXIDic2ae+4yqO83uQZG07gdJIXsIType0miV48013Qj1EHPGTrtrCrmwcG7bN5a5ESnyVbxEbcEtg/5jCvsAYW3LuKPFESeqF0bBfGxWrvyDRyVAxzZ3OWUNcUh+RIXUUfBZ/4uV0kfyKfEYzcusrL/AIfH+OKx+z2jEqD48v4FiFU5h2qw59H2a3GZu/AIQG9EcPapLbQ52snbBahDDTbbPu4ieq0+pagh09uEeHm/u+N6HomylA7msMki4b0uJhq1/SD286FJ5Dg0Ygl+9WPcpJ2PdNpWT+7uBx+Rg8+K/i0vCyirEx4PyODrbBfB1cMEq+3LNQry7tL+mAy21iSZ7akwOAPE6Woq+gVtzedTrYNUZ6dOtGe9tfPJ0lkNzIH031f03fmeNKe1ykK/PDhxQZNbqBqE3TAUd62u07nzhcC2TkqK6y3U92TzREvxWpm+OqQnkk9yp2mikEcPRF2aHzyQiPvKCw5KHlib5V8LwofP1ZFRN5snVKvZC/gnolI0xrEfrHXAo/yNC8+oEfw7RBf5O3m1TwsE/h+b+KNsV37zYUUuY16Jjy0sDbVXvC5doKr7tWVQI7M8ZS3y/LLrjlClg2NWxNELCcpZvPe5cvLovQhWaHLu3p9nTE5Fl0QtfHjy1u3npn1yV0k9wzS8MszkdDD+FzlY1PXSzj2pPUMBOjQUPXCD9dpiYVqc59gs4Bs2euZ3c4uvN9L7sVMUoP1Nf2eaihPsJirh2Dd+UWvLQeyQep7LgzBhSxF7fswrv2X4cZnyIaTuKoE91Rg6L731Qjrq7H3r9D2bvf8K/buB3+tAyAN9btJYkrI/oTxTLaohTUlmLaNTHDIP6LE5ndAJYKwOfd7xhw07lnd1bm/d/qUOpmKoM9kAcBDDMV7W/OC5CKM8kxYZGdmoRUql9HtM6a85QHKlnjfoiKJMjD++5kQdYI8h3CSwXlyJ3oe0aaM6XePkjfB7hwkb5SOwDlG/tkGyx6UZXKb4RXYHc4nyLazjwHn7Jf+T38ufMIZbpX/cx3mzb1gacLomeZLXX25oSjpIKd4goblh2eyh07hGqUwa4bxXdoivyhoqbdBINNdtfKq3TPhRV8B1/3f0+XqU0Q9OgcJeMXOsldwA9+Wc69DHbFJ3gv8icnD6Z0UtknW7LNnMaePrCrylGDyu5ORLtMZ+ORt8IiT57aEJwnADmi28Lpw8xXTjavw85hoyRCdpSBIK1lhSDm+HMeaPSWd5sXFH3VUoPL89jp0P5uj6+pfeJrY1OTh0+DRQ/gMDTn6uk2yzQRhaf2WI/adNuS/035Msh+BnpCyP+SZh6t0zwU8D7naDed9nfQN1VnyR40SivImQbpXT2wvoMv+oYOvhZejN4DG72XaFl/IoHCih6ImL1coj3swEtwJ2fEj7lsZyhtoH0y+TDB5n733mYikpT0rlGGTZg478hBXE4fqUe0Q1mifsYRMPm3M0liQlM8olu85vWlM48jCoCK548JgCuIOuJ3pHf9KLz9vx4vAwI36X9Mv21hcEz2yyKL0oK7uiIhsvJnOUYwJ9kxEeLcpu2/nniL8eM2ExXpr/WS2TpkDXkC0rVkhMPYvhLcu8x8ozAqh6A7iGJVlOKT4z3t8ai68MscYL7qTvKjXZ+Spmeexwi7zTRLbL6+2ml/w49KL2+hpRbMz9NnBaoX3zvBZaXctCIGuD8odz2SB0AHZs2hbbccXCJlHfxNzm0hVrRY5eKB8biSx1+Oe33xfBv8YlRXphacVvsUimXJHbDS5k+dJaDcl0HMSPu+LvN44SqC2bvCoDv+nMwIfvj63u47XWg4hUMjag5SqvViZxA9vN8UTXZbPREbthHh4buwW0IpnzLkN8Z4H09KxwCE6bpYNgk99m7/chdhi54c2M257NnbI18XV97thbBO/OvdGQOBzSUhrXuC2ar4njsMKgatNvBsQVZPDN3BS5447j+qiQFn8BL2b5Yi4QXiOhYzh5evfnVflZfTY7kr+J+xvkSTe0F2Y4UN7hddCeprFKe7qZL58o8aOH+dp7JI01icyn2mEo0bnGWkrG+YZYdvYrCPDDVksVvvGRmIMC/6E7A1ReoLQ1DSVtzw1geUtT03tFn+YpOyUTyZnRJ8Ux+atUNExWRNYU6+xtrOa+J2Ec/QQFauzHG9w5XdUIMxafq4jR3co/pHXg3t16FOj0ECwoNhOCZD4KNk6mdzcJGninQiv1/o3QcZIfWjIMYa8Z1EgLEeO8PyP9RSnacdYSO0wE0G6FEwxFcbnmeqw/IFWMtJ59/To/v5tMGTHj5ukoCenT3k25HkIiPe/UBRm7CxfvkuwcKveoevhgXW3OJIezUFM94MPeboKNFci8oCMwCA/jLIfwY5SHN5gS4zFe3oUGmX77E5otKfXUaANqZXQVClo/f/CrIkaK3tF8mfzsjPxm4/icVK7SdAHYzdZA+eoZGL0PaXRhuRRCU8cEXHAXuNzZ68The/6WU3qlii0OfUsSkJ58nYnxeY4E4amLUpyVsGLcFM3JsNJbF1TZ0vd3gsN9rhwjtZRkjGZLB3yIX2ISKRUe7j9nFd9/j8v9+44Rpvq8i7BPY3wZ+ov+iHKVl/ubZRc6wcJv5b40PAhwXzwbJ5iWPxFQlrVvvW2mp+nl/sqNGad98kNPUU8Q9bphmY/eUNNr/n7WqLV96S6c2Qhrrp3V0av1QW348zJrM+EQTs9iZllpweMZHh8biI7UvvfQk1sPzwtu67SLHuRZ+6HDhk+820CnkTO8WA3JOg4mArYYwwXjHKBMqKwh+phgy5c9z6hsmQyjXtnuOpmhCp9njbnGYRfv7CfifTrxxPUQWZBde1s2Vj5EAHmJLklDTKf+h6zb2jya8s5RjPLSKa+Zu3OS81+PTXJxq1NTbvJ7gLD3wF2iW+8A7hPyw5VEMXoI2bxbEgX5PWWOZH636fNzU3f9HR8Ll0q2o9IhGcYHrXb9rGO8lwyyiy4QVMq6tjBVHpRF728u3rZduWRNZs+LyvP4u7MDVG9vGIphsMojbIhvY+T+lZO7q4T8hAW9paBtX9ceVrbrmyMJ+bvC90lG3o3+zxW3oKS/LKI4h9JdhvwcpE6002rlNAbQhTqCrN7KCMQujl2oW4BPCMrRj8kp8CRpmaA+ynRbjy59cBC96jqIiMZ5dGzSZASIA4r0MQtrv+EsDudo6jMs5O8aHgljJLechyi51+D874LUlPfBqsAJi7BnOc7O9HNDTn4hEF3sFonmcTFjE9oZp3WZiQoQoSbbYsbi8UGmVMfoqOoeFabpL+0PIuKVnHwsjw1diG3a1a2rs/VKjvJ/teri8v//93etfXGjSvpvzKYx8XiZDOLAywWOQvYjjMxkEw8tpPB7ougdNNtIWqpV1In8QH2vy9F3XgpXkXqlrzMxM1isar4iddi1czXHuFeYeCvExV1EHx/QXfDzJ6rG+aEFzQbGeegq3PLTK/AxnSJuZ3IVrambz9iHzxvyof4+/V3RGnqwgYzucLdeciL55GOK9Plr7K21PiMkuEjgwhf+ZZyIAnKOcy/IouwEblcRp+fWYL8fswWpya7c1E/Rmt907d1a8NrZ//1iBy8u3kv8h6P1XojcLh63qWITTTu1AM1m1tUJLncP8FsDVFfBhBuoy72TLMYTDbpQ++GbMXIkiqJU8fLH7b24l9HEIvjn97lh418aZRGwHrWoAcFBusOZmz6XHaJkKx/3Qgsm/eRbV4P5SL2pcMiVrsydglGeZ3VxBZjqXEXb2e0wZq8Q19ROjaYXV5Upo4nRhzrbEdenxOcTCL9/+YQ6T/g+Gj25PqAfKV0xEhFRYEKX/xmOfY0ygtZ1FdY2W7coW6TrPJtXAphU7hNvSfvJBKCmA4MsJExiFbJ61OT2dcuUBIK643O6JjkUyx6bgviwN4N2RsB5vhb0TdFfnSHIVt7ZE4QdzHouuNi+QbKKOIxI1B5h+KRlyTtHv7yuYlu44lZ/xxy6Z7sfVS7jYwCzoGn/cS/9nAiZHssZe4Ns3tC+3OKHuLyy0Z6W7sT/bvDTvQCfw/qncffXXKM5dn191MNMcB5jt+SW7MnZxcCU5PZosbH/zHt/4en3btB63/gqeDunEVt9VFZzTG2Th+y66IYN4i3IrmmRpdp5LBZrDPBkCNvV1l6BuRf19loaTAL/7K8dLTMmaS49LC/JALRsPH2Vd6Ub5P9Ho0KfoX/PNSjxi0qdtR6wsFZr+MkOakZre1d/u0TKujhrci/fe1+kQz9TWi3euTwdZ6bbio5b6PguG1NwyOIP0O31bjLUxeXDab2qIXXDV65pSFWTR+Ln4CaEFD36fngnakXBy+HIOLmkbjq/k12W3Pg9IE1rEKTyM03KDBj7zyZmHDjzjasj7OMwfbwhI7oU1wkNauNII3oZNOdRukuLQc4E54gQvhrLWuu4cBCOG8EJEEmLOD6bfwkWKbW1+EAl9q5DbpuHIvht3T6Vm/bh3f5Ib9NdnWU7mW4hb+tjullvqfmr3H3q80NZPfO/A9UfcuLL7775rZIjnHxTD6dLtOay1siiMvId02E5fV3rGV2QCSs91j5YGYWYpo737fcf5SR0OFaR5UokOVt794cMlVgO1y8y2sGFkYx9zd5kxdH3GVUZHVP7J0fnQCf5/lzmpRPc/jwe734AiZon/HyX+d1jPzrbI87c+wUaLcGex+TV64bGYCWfgoRbD19Ge++3GSY/+7LFp2DPLxMn/f98+xeSFNc+r+Ps/NjTJbaxQM64jF7M0dOQYaDT0md00RMuDaaceBHb93D3Z99vN0+boe9n1283S6+QymxatPTG+nhfrp+6WW18NsYLqGH4SIvy3uUpj970E8Pmg+OdJbtrZid1kmeCE8ZPsz4VY8iwLtlfDLjPvvzjM5oT/KcXFRVvHva0MNsSjf7DQZTeZxnN1YrPqD6yIvuWbdAYpr51OUq4E3i4k3S1Brj40YHuMe6fE6yuHh2OuzXfpEuTyTf4y0g7OhpxdjlS9zI93dbJHkxMjxY/czDu89xzdTgUN7eLzj3LupDHkLQO3RKn+2ktWAbQuIrITH2WI6Xu51vliZveFxG6Ppe1M+tqM+j/ns8LDwUycgoI5iJl3erzRJm55hvla09brpH2b4+8ovTtO80jx6StKQbmSfALHvsNPt3+2vMdpupXxuwvE1dMrzLe5sXMn9zs5u3EveCpbKGi86y1Ea2cGHceN3cl6n5ZwKq/Ro9xue0woMbQSCuFeC7u4qPpzg5bOXuCPosHF0n4UnPjZnRTDf9rXb4eJPtM5Ef7WDZZWOGF27NJNi8IRWDw4+NW7L4ZZwPD3vFemmqj8PX2knkMxxlvQRON+yMPfD6zSOvf7fkZTyQ/IG+le9Q/eFu0OMBVs5rYBTJYtToMeq3MhWEa7YWUeurwb7DHLl+Gj8I+N0PAu4ovs32mz8n0/coLs8FarNmbeT70M23LgHowoa3u6txEdrdNNRdY4uh1/hzycoNDbM/YTQpjG6OJ7z5x20+Jpt5IxYEQ2/ytE4oHIQ1Nn3N0oeXrA8+4x8kfUlOY0R4iL+gMfUbT/AP2biVPEbxmwSl+/ov/6+guk6/yrPH5HAuYujq3WmLdv29KmJvKbyv8vR8zDqndB8c71B5Tqub7FG4BHLL1NZEO8LS1fGO7BeBfP0Qr4run7PdD+DXb2LtwRLR5XPDZbD4v/5yU34k26j//OUB973LHUh+LnYIfPNtLV7DSy0ePQw4PKZSvW0Ya86XorymqgJVR09z5F3k9yqMrr+56wpUHb/iJE2MDkILGJHY6WZUfKCGx31V+BrRG4aXeS67TjEax3EfBX4Ocf29Xuy+Rqc031IWkHb17vSS5lHpSzXfctvDKtTzOmOAzfg1ttGFqcujcJP7Uge+BrEF7IMLVNXpoYiz8piQUInjrQpxHOUAhr+N5ujA9jGB4VVPs6f0zdjihsql3wh7o5DBrqL7uBmqv4TkK3pPvVF3vOK3chKwnIp+nrv4Ondxec1Tf9/1v1rNvX/gxi771sN8kX9NsFVCPgq4Kduhr4XomPteDydK823yQdxgvPhaPOM/66HU1+lPh43mXMnbMZDnxdSHx8cSjfI5I1f/YxhcxtXu6T7556glwi3+CEngrEW4Q9QxVklYcrslgJsr7/8kpwvMbuw1cIribAj653GGxV3TrgObsJ50DFKXCZfnF4kzMFiPbheoo5+1wYYtN3oiCy/BE3qd7MThqgc5ca0TbeMhunk9d1V/W25JYFlGxp3eNOjU30CLlpMdz2Bkpr9BFctu7muGidTVgKjLIObQtx0L015tm3LqVaYtt89lbE/S4ttJMNQM0ZOvk5JMfl0UC5eu7HgYf6BtW059yTRmZ0mq6rivkpbfEk5D1ZC9eev+Bt62M9um1tqXtPjWB7htzRA92XJv//cQO91dyvtGRY4bc1srTWtKqQCd/E4yNJVDzplj18K2E+foNfACZs/lr3lv8zT9lNfhJ2dNZGCSznzkR4YVvcjKby7R2+i6ITqh9inukmNv1f6tfg9JJQZSdXQyahiaJAw0isqAO+GmQnyST0OX8KZmCHBcpvnhRwGHr76sbXablw4vq4eaAVdHd+hrgr69Renp8ZxmjhvRVXQso7Dz4qarPkqUv+KytXiIg0Na0K335lzhV/3NHU0/eRtuam/+4WbHyQ2hQeZ/o/IBWzH1wOqP3JKTDOsXZZnvEtKz3TUKiYbe+ILdoZK4rUVdCiQO/NfZ/pd6+TrkSOokukfp49+GH9+f0yo5pckOi/CPX1/+yn8yH7ImfeovF+Teqz7NKHfxXjQHVmMvlQGQnJUHJGBl+xehSfwZo6J5y3+VZ2VVxNjc4jefZLvkFKe8PThCw+Gh1rRnyZe8RieU1VfyKr1N2qUTW4nt981wPaCzx6sXFKgMsJb8kwS4JLKtCGi02CLK2NJtQIzRaRX4Em656D13Gd0DnwrVzXxtppfFwkmgp7zDVMnHEgYBpGCSCYBpfqcrad/sEncBYH2IiwPiN4kDMKRAUHX8DwhSa4DMDVDNSelU4MzTdB2Tcy0pCzLyw+qnYKLGOmbd/kA1EoW27amAKGlkFCTofg4zR5r2oge0tIoYzYKYfDa8cN4peAuDtzK7CrVOOol+pcZyYHqUL5oEXbCvkVysniII5jgTTIA+A18rScMmzlULwaRuQQb0uqSffwgU2qBgNvjJvfSmwl0VV4h4bGc7FMFeiovCGC0viy22ZPWYYtSxwNJsUOpc9DyC6N/+9reXQs8NnDrHS5pT/9vaAQB6lS686xWgHf8NLw4M9p/ohJBghJsNGL3rU/8URLe07mqAZ0oTTTK8YzckSuBxplN4AlQp3dglTard72ZClmaBbDFAbBZXNn08A6zkzxumRtVlktZvNeGnHU6Y0kxfVuPeetFg1BZr/PnRQOIGZVUEK7HQyasVGhSlL9vM5NVpZDN5zYarzi1wHRccnbSMDMOPq7/o6FUxaWv+y472SUxCcmpH7f/ldx5tOXPp0P02zc0HIzArCFcU5gYEslAYJHH6mLTY0i4GTuCTOaEzoV78UeAEWWg5cBp8audZMXUvOz2OS7qFc/dml1no9D+ufUyBnyTLun/m0aR/j3pxwiav06K04hvcqXZ1mX4cfpxkcBHegUOyBMZWr/IE4FK/e5e0qX4bPTvMdNekNsPFhmFm1eVzwEwRH2E2mLXT66rGMmjJJpRtZiSzWZ4tcSDrEaYZxuZfiM+PrwmX4i7wUsadmBRd3T/u0P+ekwLVj2PlR9pLGrsogUFxmPLNjGG0Vjbj2NyHCBHu+eQrKp4f6ojp0r6liZhOZQqs9oAWo6EvXEhVDYMNq3mNkm1uTFyes32K6ngGF1VVJJ/PFWqCl0ZDiW6SoyiBDqZLpzyBkmqmFlIgDjk1ymwcFKNyXU1kGGovB7otVldzOOr6waxvXeaGc6Y7FwQzrwDTzpY/UaJudCH46M9BJMEpl3W0xQkNIWtDB1u8RjbnWovB1WqmtRlBNf1gZXfvM/NQVaetf4zJ3VMx4dUf3SzDji1Y+xUgo40RHJbhVMCgQq7E2A4NO+hIRQLLg0DNuv/9jEDWzdIVFoW91cxwCwDb9DOd/fC2jIX5/QntksdkR4r6o471gA2WH5JLRrkRAErUWwMUYdE/nOo/IxO96Gd5ejyYASHU01+FrgZSdoRhngK6Q2jsO2GVsibtwwwWOriaaxsaOD/k2DwabLMP2CoN5sY8lX1BHytwKauIQWbQF5sq3chqgVLJYoUwYyQ1AFxmzkWSPgU680eAmGmHz4kyOGnNtED7FBdJnFX92HqVHz8nGSGc3UFEIRsELSX5ltxKVIqaSLEkjxMV/lazN188UKefd8didO6tugE8/zzHJGj+xyyRY5QhorHAFmxyeJQbaNHQo8VeGv7WOyaaQHKzo9+ah7wOcvojSYEQ6PKpjyEXi8ZJjx09wHIxZ428Ep/i9NyDVK1hGFxMi1yiromYLWFIDDvhKQyUG20t8MwzmBvWsy8nJ3yOMdPCcHWrv+g+Pp5S9Dr/lqV5zOdnpc5gOgLm/KX/ceFogNWbHwms8WfDwkOCCqxfHe9ZnoWZcit2yfRCMBFkFuulZ2Shft1EGpdBH5PGaOkWAKvlbynnAdGE20Y7/My9UexyHq8jIF4nLSPD8OPqA+L1qpi0NXtAvD5kq1WuvUVF6tRNsBOmKZskpBWkls00N39E2PXmylsi6KZaVI0GXl1hPvBdf69QkcXpxbl6qjk2Tkxc8sZFj3gqDRi51ISrHwGV6tkAcjYsvsmL85HEMPYNPPlBQt8mw4n6dfW4GHRZDwge8lOymxoFpFERBu3P28BBo8x6gBCR//5e5OeTFAUUidB57c+TTESkQVGEQNCRGSYgeIwaGuRawhACyG3fYyHxMsOgY96X0444hHwBiw+Z2GO6LiSEJl+9WPbrpOsXItZsIPpQ7IPk+VGtXUibDJP2l7Wn92nUMGmINfjMvb+KTfO0oJlymWuOmtlXuF2MzY9lfEBvEyxN8RzBwUEXGkuVlhyUhyXYTDRVRi2TdmcPpwpiDfhW7MeIHwBi5mPKXPgiEs478ZG71iUjqpdSlCDgJetk2BkUWRNglu/WMQ9sJnTrsAPO3G4dvyeP1VVc7KPbc7F7iku0/yupniQ6uHejxv+wk4JhNvwYbiSZKu5yr4sRJsCumB0izFoHVsi1SwONNZDkoDwTLHqsEOAJbdbrnq7isrD2kf4UFroaWhLUJlsjOeOM6dF5l01/5BVa/jq7llKUoPl13RgaFFn+OvsOfcNov80xg7IbnFZxPgkIzogDlq/+7BLSahUnmRDOvE6CmuX4cuAy2TDkihWmW+bzO7x/Sk51MqpFz2SdkGxAv/7HdQOo12P501gnKjkxguV27bXAyBEOHNiCMAEVbTrWE4iMjyX6CvNe0tZinPze0WumqJ/X9L9yRp9xtVKdi6zOiIgC+BoHWg9TInNLG6ZkA2tgWp9VrH77BzzTjypW2Fzf2GIFPM76c65tcyLIVVxUVKq/kGkpNTDhJeKWJHzhdtJHCrqZtLmAdJEChFYxSy0BZlPOVU7omn26ErC1/Dv4JQBrwht5J1zNfTF/9YR2X/IzH/JM+Fk+ggmUzFAmlk7zthlUSy1ayKBmGnuGAaREQ6Phjq8647Zvdy6wxofb+JkcPd5kSc3X0wmk6nSabZjbv/GF6z5WFPQxapPqicXgo7vIUGvkq5+DHRyASilFC3tD4gYQv6C0uTDh686GzxoGX3FPvMsPEfXvuiPlJw0cHXPiwJdNAkiqVZk0oc4tVDYLgztaKZPmOBEXAbVV7DznQ9WU+01bOM2+1fSPn3AxIHnobAMyq4EKeeZ7f/5c7oqkSTU5cfgPum3xOTVbunpYiDqtAiRYv69xhd6jsnbgjN4U+XE6lLCNc6dhbNHq8cEpZNIi3RlLAchD/hMeC4HH0BXzrWofH5MU/4KiiUIz9A2yjIZf134/O6hitLuZ2e/jYpdygQ1rLbQjAyGaOcJmLzq3u0knCV8omikQnHp9bFYjdd35LtOqvKiDkyfHuHi+/r57irMDusNfxNW5wE3snhXwaglYaHU/mo8yRAT2Rqz5JRAmIL3C4KHRw6QhRQcsAxrkj5+YmAETjOVnA8NlvPtyk2FZdl+CbnKDzD4S4RmRpDSrX+zKNFvFjlmGu+X7fCwPdBN6gIzB3NyOIH+e0Rntr49xkl5UVbx7IpftbxLFUts+NVQQyIGScwnMQIrVZ5yC9TI69klmXH1TYkeN8Dt18DSGiAkOzxRMMsBRwsswFghZclMFR5dRc7R8S8AWNYxJVRnXsT/EgGYLg9nGNKrmbPC7OZ7yosKyPeIBNrrfPaH9OUUPcflF/vCRJmL2fkyB+S6SkYHhyJWEecco1TkMXlidTBpsJUyyQy3jjDlc5ocKIwOXdmV7UGF1Wh1UcENp3vjAgjqM79dguX5Y8QGB6MIwiyfrvvcCNkovw03g45wr89s8TT/lFcZ7e+VSd9TAlFzd94df+PeqBuNDHvH1tOdibV043VhXZnGLy7fPHIAIhQav8Eadb3UaTAAzteWt2pwDX/UPF1n5TXGUSpHwvdr9PMkoNgpjvs6yJOZaELYGEWfNIXuVH8lMaTiAUVWmHrvopvk0sv3vGxqxpKa2am5iGMGJcF0zAQfMRmyJJE/jklWe4Inx08k232VOmh8shyOqytTDEd00c2lD/76h4UhqaqvmJoZR/W8xuRLXi0KGqeHHaa4C7ZHkaTiCzbMM/HSyzehsSy4a79DXBH17i9LT4znN6sgrpns9Sf3J93wyOYDLb4BoQ0OYWY9YtT0nDpkCnatDSyXt89kQ5dd7ATLGAsHEVF4GspyGtVnHMnMwb2fUWtFQtQInLEc4rc/hyh5D07pZXeM61XP9UeEaqOjcC/M9epMUZfU6ruLPcSne49S17lHVP8IhuWmbn6nObH+vL6mO8T9+3X/OcV/Hn9OhijDgcIzj71dxhQ4kbITIni4FG6EJNE3hf9UHiUAzfQnURF+oYf8u38Vp8k+073ocaAiggZoEyHSNx9nhTJ6IiW32RWBTfamJeui+KshZbJmfix3YGkgmVVKg1Ehxi4pjUpYY490htyCBSAK1LlJpWmbf7gitssVQiyyFTs88TSHdyM+gPqTEgGt3YQHy7gplLXTlhrbqlyFSc/UUKov1RIbNKtpTN6RtoX/KJzTQl0D8+0KdArXfGjgQ9iWg+F2hbgBs45y+R9VTDn06PAE4HHI0ujYrPDzjYewrnk6h74YrB1tkSTQNDidMQltDEdTMUKr7irqFlPgJdSXg99MVatgPGXkF/kMR1MBQqoOZfMJVz7bGU+1tsqvOBdTffQlooq7QrAcUrXAEiv6IWqLofUxwbdw/daTppEBHeIADqVS9xhDqREBp8hUVzw/JEdKeLQYbZSjMrE3HEJYZnKZR2Jwms228Dwf4JkkreCLTVjESTahlJqni4xIoVLDsqIxx2VZ8H2fnx5hAWmEclkolB01pLItGCH3rLIWZ5vcntEsekx3ZAlHRPmU2kNGrrAHXMbYLXP1D+/ZInByV5OBcqazhJJ2xXDYSmfbpQwzt1+hCRW+RcrN2PsVFEmdDrNGr/Pg5yWJJv5hUUsilrKeR989zTH75mCXQtMMWQzKwFG7WMTeJZupt/m//HfEV5QKZSWKNS17PNnCvqVlacguMtDV0C4P+5Zu4KOiLwAVBX6o7r0lQcVsk4FqeKgPPaoZiTSOD04rQxlAENVGXarlff8fTeRanF+fqqT5Ra4Ym6XmCmhySQl1DIx0JsCXZwFBlULukuIyMNjGEVnaqRxcqGjI74SPEskaU/FsKE/6/F/n5JGukLVS01FJoWmrjNwuNtL9D/Nsiwy0FmzFXuqdgyVSbCpZSIwWct1eQAiaDpIApDaVQtKxuzawbJcMLVSbtTqN9C5W1Fm6kKZM20hRrGgGzUwrNgVRQwyChwZmSZAs8FMnOkYw2u2ziO2kr8k5jKbQmZbJiAcZkymEzMiRa9fgkFICKPAmsJk+lOwYSEyOI50EiDXgwJJLZNi5bQckIjcQwWzVJw5gDvS+hhHEgIbYUx0AOMwH0x9t00GPxiJsuBY+5aQLzppoY4qrmGgpNkw2R9hYL0kyqkYkmQJRXeBnC0ihWIyyhdkPChogEtiAsgWrlyJHq+nCIRyj23lAG9ttQrBsfmXel4tjIFIPjIkOhhWUq3QhQZTAUU8Ml/sdC3ghVBjVCFevWUShDeLOlGtxFEnBdJVDptoiYByL718/gVSlXDm4VWRLtHVMO3gK0v8N3SrnBPcMQQE2clvoi+EKxKzURvT/1gDXoi6WKmB6bSKMeCQ1LKcGbMxmxxSnxAzqeUngogcl0p8YDpeFtmEICkUR1O2bccndMKW9YoFCddpo2e4dqsr38GpMngNcWLI3OyEVelph5Km9VJAGNLFDpjNysfaR33lw5aGCWRHtCCwZGAY5qQTr4zBYkNRdE3by2Uf2RGhMESTxCY4rBIzOGQvvFHk9xcoDGraEI/kK7Ut3I1KyAVIMSTwGORzyRwbnnO1RVqNAMzzJC2ZkoRKs1QVzicfQvlByeoD7lymH1GRKzBl8nGNwlrLZIomiWotK0zMWkEZrlyqE2ORLdrP+c7RSTPl0Kzvk0gfawm49tARxw8yTwoTZPZdSy3KpcubxNU6vyLn0ajz01uYnPYFSTmt/9Mo5TclcvCZ3eF+sOlXiJTny2DVzNWm3lHm0ChcpzriVC+oa7o2mFn4RIojrjji5OpzRB+4e8pU8spJAvRwQKMxlacr0EnYuSwT234XV3NNAZg7J3sTTzbjX3cmUi5Zvf6woxLESbCCQyZ1WWymDq7V+EgvNtXyqbZHsCAw9FeVNMqcxP0bQp+UsxGdYAUgXkAGobiXRiGLQtNEg9g1A7r0eD6ztVR+HGPlTgX21QucA4P3wsgtzHnq2p8K9vuJj4y79g1Tc1Df3WQG8XmDqYUaA3FYNFlM8k7M0hzPH0OFZG963ZRcuYVZSrKnvZQDTVvVZQcoLGdpCr8oWCd1M+xMUBVQ6mbCvKDSBVWKXgMk2IpzHl98gShPgE6ZcojcrQSxM31ZrnJFHDEVaOJhkppFCDfQrT15M8crFXkV2eR936HLWL+kQ5nphXlisJP/ggiqqfcSi4cBsXgJnsOUoQA8pHEfPKipEEUFai3lJMRj+WiXq+gHlAwgCmgB4ANSZQvuuxV73dg6qU5kn8q8vtp0k92fMvdxU508kVZQm999KEqvcby/6oQTF4yon1qwZwxaBUXHKWwjAIagj5ICgn9tKPSzDDZZLW0ZF7zgojcKThTGCEoRFKd0FDBtZyrQXakJ8A9+KXYSB7xmtvhu6ppnJpLBKFWB7zz01JTelTUodlMvOSMepfNAKrZZhSsQRmn08261/4BSZbD3x92VTniryr3x3G6tWHI4CIakDyL0b9/hGgot8FGv89zp/VN5+17DGwvZqqo3zFLG9STa4U/86aKCV7Qw3WBM0iFoY1j3zuN6nmqc8XZp7uEsYKO3ylkMiBxh6hLKRhbFDDV/I/pM5kFuDpPbh+VFcIiRMgxgDDRBU0wHmyjdjoANKplqVTKANEK2i0UEQhcMWVu9rSl/8RHa5Aag2j6loVxSALtLLyuAlqXpKYCTBrXdwDDwZuTaJfzUmrBFvWuXaAF7NYGWSDpuiXssPqQGoJkTbEWkYS1IM2hMeVDK+aHg8CaTA0TGQCJtyIYtcD0vnf+UCBTEhlZYAS557ngq3QTUghIK/jWS0IFVJGYHkQM+k/EpA82Icyj0kkgWn0xtFUDGYmdbAZmpthBBmHuzJFsBxJpB/wJs2BjeImRm8XM4MYcGUDDyl4SwII+UarunFbCCu5TWyqhX4Boueo0sVJTh3umFXwj2VOWqnSIMZQOCnJqbVKSbQB1FiGSRQBq0wPC4xZTHCOYBDsi7aoTbiuICbWT+ImtYN9omszJxsBzcmmDAu5gaBgbcQUqiBsazWxA0ynx6aJtcKbSLmcNK6rVV6ptdm6cZFmJFFdImmzxiZV85nEKCZ8mcg4KuZwtBv3hZDhQDn5aBj2HiC6j4+nFA0REeWKc5SKpQsXl7FZtkhiLk6och+SMWId10WVJZR6jyB3J3wh1CSpLo8iOUZ9xdwlEvmfrcKr2sXNVLpMiURykd1dpvjonqTm8KM/z0DTpzvqCnqIj3CX030knt+TSDQ18B+d/AnOXCZSBW9V+luaVQyBJpOAtoSbVWBae9P14WmVdgKoQhhFiKNLqlK/+lGXxMDV68uRBVOYieg7aAxH6nVUOaKD6kr0pWk0IjOheweRwZC8Ym2xot+ebVkqu7WhGS3oPL1JvkSareqLpeg8ij7xd0vi6xo8GwDp5IK7vhlgokqTWnDQaFdFVWMTRxFiWAqnHhTkOhrc6+R+fHAFxe5ttB8fFFKb4aIMke3JNK3dDe3SUI/u10WYow/bLbcBT+JXcSHk+FDP06ZnkF+xlRWJ/G9lw6vaBXqPbvH+7Cku0f6vpHqiWhAV11Xxpg5Tlw9lT6pKw9S7G4L5bgf+cjPAFTwpAtaUfvFGOQI8meYj3e/G9mFr+R0X5jJPn2JAMyBSJAEGRDo9wlAPzHxgryKQzEC5FFLSh1gYKZI3ECYm2Rj8mEX7Xegr+UXHPKbp8kTIvwmOwq/SfCaN+y5MhKfXHXQajGjgK9eSJfQkOFhTmF+VKTtcN0JdyA3tlo8nXNOej0k+ohnwQMowQx2QVKX9klW5UkYccBt0tpTWf3+HjgvA537RPFlSkWt3BF6eKMmy3rRfvyaTjQcDqb4MOXGIj2N2Uyg2jFJa//vGqc0A5wqKgExGwMhhWleBF1nypQY4unxKal6ix4hhZiSX+YbNMRTdZEmVxKliLamq4HsdCWdSaqceTXak8cbo1spiU3q7SOsGUlfJCV6Em6W3cohyNSR1ioQET6LlVOSKqRxOQNVM6eqsUjI+Mh6eTaKaukC6ELPWNGrr1A2uJq+eH7WEdGD6C2SYOtg9MpR/YriIUyaScHG8o9OORW+K/Kiyh4o8hEHgDGvtwkaZMG20KR5yC0NQxGs3Q58eLlLs30Qi/xs3IcVdU1Oavc5lZE+BKMfw2A5S6vva3QlLSIfXjvGyJHQu8UpzEq0rOcbF8/X33VOcHdAdNu2QQg3YlmgrqYzC5nRrDQLna2N3KHSauWZbAmaRG2kE8oex9iz1utSW5I1TTobaOiGGPk3ePMLINAmePzMp9u26Kv637/OaCEyQFr1J4IFUQa3wXXB24VcmhLtvHkCY5HcbZZaIzbCmNApLK1cMyvpG9FFlc5OZRmaQYOhg2jAFCV3Jq1oLQQyTxyxiU+eKNlJQK+YTIKFvM60oEvUyHMB8bYSFOg2bizO4jTkU1CHNASZaa75DZf40V3MMOeCinrXMGACtd0UAHmK+O4qNIoedWxoLOtFUVB8YXeVZWRVxUs8qeHfaL066UMwPeSQmqAL2d75469dEDnGb2WWDJHFXs37QJePyYHY6cYeBJSlytVJWOUJmNQmVgswUKEzWMtFqIzmGRx2QtO2+e0wmS5E2zrDDMzW1uXo6tfDGz+BmUJvKM2fa+0xqOmBXMo5jeDwBmfmajYsi5d44ww6R4tXm6unUwhtHop9B7XYXKaQMNJ7VJPUVD7T9tzXBTCpVU9yR69MveuwmpsDK6GxNQ9Xl+q7HfE5w05rWjevEyDXvvtFG1sed4Qj9n375V/3Vi4ZJbXjcy6joy169aJKitj/gP6u8iA/ofb5HaUl+ffXi7oxrH1Hz12tUJoeBxSvMM0MkT/TAtKO5yR7rgO8knSYnUUfSFfd526t4H1fxRVEldVxLXLzD3xJJ90u8TOrzoM9of5N9OFenc4VVRsfPKXPo/OqFuv1XLwSZXzWB9kofKmAxE6wC+pBdnpN038v9Jk5LbrspY3GFrf87wr83fYk/zQodnntOf+SZIaPWfHiTirI9/uQe0PGUYmblh+w+/opcZPtYonfoEO+eb+s0XsRpRsZE3xGs2V+9TuJDER/LlsdQH/+JMbw/fv+v/wfoLmYv4rUIAA== + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.Designer.cs new file mode 100644 index 0000000000..16b788408f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.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.1.3-40302")] + public sealed partial class FixExportDeploymentIsPublic : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(FixExportDeploymentIsPublic)); + + string IMigrationMetadata.Id + { + get { return "201607111548571_FixExportDeploymentIsPublic"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.cs b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.cs new file mode 100644 index 0000000000..f48207c9fc --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.cs @@ -0,0 +1,21 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using Core.Data; + + public partial class FixExportDeploymentIsPublic : DbMigration + { + public override void Up() + { + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + Sql("IF EXISTS(SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'ExportDeployment' AND c.name = 'IsPublic') ALTER TABLE [dbo].[ExportDeployment] DROP COLUMN [IsPublic];"); + } + } + + public override void Down() + { + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.resx b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.resx new file mode 100644 index 0000000000..55e598ca59 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.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/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index b91c46a83d..ff7fd39e2b 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -93,9 +93,9 @@ True ..\..\packages\Microsoft.SqlServer.Scripting.11.0.2100.61\lib\Microsoft.SqlServer.Smo.dll - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -384,6 +384,30 @@ 201603121451066_ThirdPartyEmailHandOver.cs + + + 201605020640016_GtinMpnIndex.cs + + + + 201605061916117_SwapColumnMappingValues.cs + + + + 201605111140288_ImportExtraData.cs + + + + 201605191216116_CheckoutAttributeMultiStore.cs + + + + 201605201911421_ExportRevision.cs + + + + 201607111548571_FixExportDeploymentIsPublic.cs + @@ -717,6 +741,24 @@ 201603121451066_ThirdPartyEmailHandOver.cs + + 201605020640016_GtinMpnIndex.cs + + + 201605061916117_SwapColumnMappingValues.cs + + + 201605111140288_ImportExtraData.cs + + + 201605191216116_CheckoutAttributeMultiStore.cs + + + 201605201911421_ExportRevision.cs + + + 201607111548571_FixExportDeploymentIsPublic.cs + diff --git a/src/Libraries/SmartStore.Data/packages.config b/src/Libraries/SmartStore.Data/packages.config index b8d63c5067..b1c169951c 100644 --- a/src/Libraries/SmartStore.Data/packages.config +++ b/src/Libraries/SmartStore.Data/packages.config @@ -6,5 +6,5 @@ - + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs index dcc6c9762c..35e853bbfc 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs @@ -17,7 +17,7 @@ public AuthorizeState(string returnUrl, OpenAuthenticationStatus openAuthenticat _returnUrl = returnUrl; AuthenticationStatus = openAuthenticationStatus; - //in way SEO friendly language URLs will be persisted + // in a way SEO friendly language URLs will be persisted if (AuthenticationStatus == OpenAuthenticationStatus.Authenticated) Result = new RedirectResult(!string.IsNullOrEmpty(_returnUrl) ? _returnUrl : "~/"); } diff --git a/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs b/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs index b948930425..05cddc7f7b 100644 --- a/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs +++ b/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs @@ -12,7 +12,7 @@ namespace SmartStore.Services.Authentication ///
public partial class FormsAuthenticationService : IAuthenticationService { - private readonly HttpContextBase _httpContext; + private readonly HttpContextBase _httpContext; private readonly ICustomerService _customerService; private readonly CustomerSettings _customerSettings; private readonly TimeSpan _expirationTimeSpan; @@ -33,7 +33,6 @@ public FormsAuthenticationService(HttpContextBase httpContext, ICustomerService this._expirationTimeSpan = FormsAuthentication.Timeout; } - public virtual void SignIn(Customer customer, bool createPersistentCookie) { var now = DateTime.UtcNow.ToLocalTime(); @@ -95,6 +94,21 @@ public virtual Customer GetAuthenticatedCustomer() if (customer != null && customer.Active && !customer.Deleted && customer.IsRegistered()) { + if (customer.LastLoginDateUtc == null) + { + try + { + // This is most probably the very first "login" after registering. Delete the + // ASP.NET anonymous id cookie so that a new guest account can be created + // upon signing out. + System.Web.Security.AnonymousIdentificationModule.ClearAnonymousIdentifier(); + } + finally + { + customer.LastLoginDateUtc = DateTime.UtcNow; + _customerService.UpdateCustomer(customer); + } + } _cachedCustomer = customer; } @@ -117,5 +131,5 @@ public virtual Customer GetAuthenticatedCustomerFromTicket(FormsAuthenticationTi return customer; } - } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs index 2dcb1cb673..f786bf9c78 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs @@ -84,8 +84,8 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli var utcNow = DateTime.UtcNow; // product download & sample download - int downloadId = product.DownloadId; - int? sampleDownloadId = product.SampleDownloadId; + int downloadId = 0; + int? sampleDownloadId = null; if (product.IsDownload) { diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs index 64a412d339..f8565562e5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs @@ -33,21 +33,20 @@ public partial interface IProductAttributeParser IEnumerable ParseProductVariantAttributeValues(string attributesXml); /// - /// Get list of localized product variant attribute values + /// Get list of product variant attribute values /// - /// XML formatted attributes + /// Map of combined attributes /// Product variant attributes - /// Language identifier - /// List of localized product variant attribute values - IList ParseProductVariantAttributeValues(string attributesXml, IEnumerable attributes, int languageId = 0); + /// List of product variant attribute values + IList ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes); - /// - /// Gets selected product variant attribute value - /// + /// + /// Gets selected product variant attribute value + /// /// XML formatted attributes - /// Product variant attribute identifier - /// Product variant attribute value - IList ParseValues(string attributesXml, int productVariantAttributeId); + /// Product variant attribute identifier + /// Product variant attribute value + IList ParseValues(string attributesXml, int productVariantAttributeId); /// /// Adds an attribute diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs index b140603b7a..3f7315905b 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Expressions; using SmartStore.Core.Async; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Seo; +using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; using SmartStore.Services.DataExchange.Import; using SmartStore.Services.Localization; @@ -21,168 +23,166 @@ namespace SmartStore.Services.Catalog.Importer public class CategoryImporter : EntityImporterBase { private readonly IRepository _categoryRepository; - private readonly IRepository _urlRecordRepository; private readonly IRepository _pictureRepository; private readonly ICommonServices _services; - private readonly IUrlRecordService _urlRecordService; private readonly ICategoryTemplateService _categoryTemplateService; - private readonly IStoreMappingService _storeMappingService; private readonly IPictureService _pictureService; private readonly ILocalizedEntityService _localizedEntityService; private readonly FileDownloadManager _fileDownloadManager; - private readonly SeoSettings _seoSettings; - private readonly DataExchangeSettings _dataExchangeSettings; + + private static readonly Dictionary>> _localizableProperties = new Dictionary>> + { + { "Name", x => x.Name }, + { "FullName", x => x.FullName }, + { "Description", x => x.Description }, + { "BottomDescription", x => x.BottomDescription }, + { "MetaKeywords", x => x.MetaKeywords }, + { "MetaDescription", x => x.MetaDescription }, + { "MetaTitle", x => x.MetaTitle } + }; public CategoryImporter( IRepository categoryRepository, - IRepository urlRecordRepository, IRepository pictureRepository, ICommonServices services, - IUrlRecordService urlRecordService, ICategoryTemplateService categoryTemplateService, - IStoreMappingService storeMappingService, IPictureService pictureService, ILocalizedEntityService localizedEntityService, - FileDownloadManager fileDownloadManager, - SeoSettings seoSettings, - DataExchangeSettings dataExchangeSettings) + FileDownloadManager fileDownloadManager) { _categoryRepository = categoryRepository; - _urlRecordRepository = urlRecordRepository; _pictureRepository = pictureRepository; _services = services; - _urlRecordService = urlRecordService; _categoryTemplateService = categoryTemplateService; - _storeMappingService = storeMappingService; _pictureService = pictureService; _localizedEntityService = localizedEntityService; _fileDownloadManager = fileDownloadManager; - _seoSettings = seoSettings; - _dataExchangeSettings = dataExchangeSettings; } - private int ProcessSlugs(IImportExecuteContext context, ImportRow[] batch) + protected override void Import(ImportExecuteContext context) { - var slugMap = new Dictionary(100); - Func slugLookup = ((s) => - { - if (slugMap.ContainsKey(s)) - { - return slugMap[s]; - } - return (UrlRecord)null; - }); + var srcToDestId = new Dictionary(); - var entityName = typeof(Category).Name; + var templateViewPaths = _categoryTemplateService.GetAllCategoryTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); - foreach (var row in batch) + using (var scope = new DbContextScope(ctx: context.Services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) { - if (row.IsNew || row.NameChanged || row.Segmenter.HasColumn("SeName")) + var segmenter = context.DataSegmenter; + + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _categoryRepository.Context.DetachAll(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + try { - string seName = row.GetDataValue("SeName"); - seName = row.Entity.ValidateSeName(seName, row.Entity.Name, true, _urlRecordService, _seoSettings, extraSlugLookup: slugLookup); + ProcessCategories(context, batch, templateViewPaths, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCategories"); + } - UrlRecord urlRecord = null; + // 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(); - if (row.IsNew) + // update result object + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // process slugs + if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) + { + try { - // dont't bother validating SeName for new entities. - urlRecord = new UrlRecord - { - EntityId = row.Entity.Id, - EntityName = entityName, - Slug = seName, - LanguageId = 0, - IsActive = true, - }; - _urlRecordRepository.Insert(urlRecord); + _categoryRepository.Context.AutoDetectChangesEnabled = true; + ProcessSlugs(context, batch, typeof(Category).Name); } - else + catch (Exception exception) { - urlRecord = _urlRecordService.SaveSlug(row.Entity, seName, 0); + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); } + finally + { + _categoryRepository.Context.AutoDetectChangesEnabled = false; + } + } - if (urlRecord != null) + // process store mappings + if (segmenter.HasColumn("StoreIds")) + { + try { - // a new record was inserted to the store: keep track of it for this batch. - slugMap[seName] = urlRecord; + ProcessStoreMappings(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); } } + + // localizations + try + { + ProcessLocalizations(context, batch, _localizableProperties); + } catch (Exception exception) { - context.Result.AddWarning(exception.Message, row.GetRowInfo(), "SeName"); + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizedProperties"); } - } - } - // commit whole batch at once - return _urlRecordRepository.Context.SaveChanges(); - } - - private int ProcessLocalizations(IImportExecuteContext context, ImportRow[] batch) - { - foreach (var row in batch) - { - foreach (var lang in context.Languages) - { - var name = row.GetDataValue("Name", lang.UniqueSeoCode); - var fullName = row.GetDataValue("FullName", lang.UniqueSeoCode); - var description = row.GetDataValue("Description", lang.UniqueSeoCode); - var bottomDescription = row.GetDataValue("BottomDescription", lang.UniqueSeoCode); - var metaKeywords = row.GetDataValue("MetaKeywords", lang.UniqueSeoCode); - var metaDescription = row.GetDataValue("MetaDescription", lang.UniqueSeoCode); - var metaTitle = row.GetDataValue("MetaTitle", lang.UniqueSeoCode); - - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.Name, name, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.FullName, fullName, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.Description, description, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.BottomDescription, bottomDescription, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.MetaKeywords, metaKeywords, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.MetaDescription, metaDescription, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.MetaTitle, metaTitle, lang.Id); + // process pictures + if (srcToDestId.Any() && segmenter.HasColumn("ImageUrl") && !segmenter.IsIgnored("PictureId")) + { + try + { + _categoryRepository.Context.AutoDetectChangesEnabled = true; + ProcessPictures(context, batch, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessPictures"); + } + finally + { + _categoryRepository.Context.AutoDetectChangesEnabled = false; + } + } } - } - - var num = _categoryRepository.Context.SaveChanges(); - return num; - } - - private int ProcessParentMappings(IImportExecuteContext context, - ImportRow[] batch, - Dictionary srcToDestId) - { - foreach (var row in batch) - { - var id = row.GetDataValue("Id"); - var rawParentId = row.GetDataValue("ParentCategoryId"); - var parentId = rawParentId.ToInt(-1); - - if (id != 0 && parentId != -1 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentId)) + // map parent id of inserted categories + if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentCategoryId") && !segmenter.IsIgnored("ParentCategoryId")) { - // only touch hierarchical data if child and parent were inserted - if (srcToDestId[id].Inserted && srcToDestId[parentId].Inserted && srcToDestId[id].DestinationId != 0) + segmenter.Reset(); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) { - var category = _categoryRepository.GetById(srcToDestId[id].DestinationId); - if (category != null) - { - category.ParentCategoryId = srcToDestId[parentId].DestinationId; + var batch = segmenter.GetCurrentBatch(); + _categoryRepository.Context.DetachAll(false); - _categoryRepository.Update(category); + try + { + ProcessParentMappings(context, batch, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); } } } } - - var num = _categoryRepository.Context.SaveChanges(); - - return num; } - private int ProcessPictures(IImportExecuteContext context, - ImportRow[] batch, + protected virtual int ProcessPictures( + ImportExecuteContext context, + IEnumerable> batch, Dictionary srcToDestId) { Picture picture = null; @@ -255,23 +255,81 @@ private int ProcessPictures(IImportExecuteContext context, return num; } - private void ProcessStoreMappings(IImportExecuteContext context, ImportRow[] batch) + protected virtual int ProcessLocalizations( + ImportExecuteContext context, + IEnumerable> batch, + string[] localizedProperties) + { + if (localizedProperties.Length == 0) + { + return 0; + } + + bool shouldSave = false; + + foreach (var row in batch) + { + foreach (var prop in localizedProperties) + { + var lambda = _localizableProperties[prop]; + foreach (var lang in context.Languages) + { + var code = lang.UniqueSeoCode; + string value; + + if (row.TryGetDataValue(prop /* ColumnName */, code, out value)) + { + _localizedEntityService.SaveLocalizedValue(row.Entity, lambda, value, lang.Id); + shouldSave = true; + } + } + } + } + + if (shouldSave) + { + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } + + return 0; + } + + protected virtual int ProcessParentMappings( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary srcToDestId) { foreach (var row in batch) { - var limitedToStore = row.GetDataValue("LimitedToStores"); + var id = row.GetDataValue("Id"); + var rawParentId = row.GetDataValue("ParentCategoryId"); + var parentId = rawParentId.ToInt(-1); - if (limitedToStore) + if (id != 0 && parentId != -1 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentId)) { - var storeIds = row.GetDataValue>("StoreIds"); + // only touch hierarchical data if child and parent were inserted + if (srcToDestId[id].Inserted && srcToDestId[parentId].Inserted && srcToDestId[id].DestinationId != 0) + { + var category = _categoryRepository.GetById(srcToDestId[id].DestinationId); + if (category != null) + { + category.ParentCategoryId = srcToDestId[parentId].DestinationId; - _storeMappingService.SaveStoreMappings(row.Entity, storeIds == null ? new int[0] : storeIds.ToArray()); + _categoryRepository.Update(category); + } + } } } + + var num = _categoryRepository.Context.SaveChanges(); + + return num; } - private int ProcessCategories(IImportExecuteContext context, - ImportRow[] batch, + protected virtual int ProcessCategories( + ImportExecuteContext context, + IEnumerable> batch, Dictionary templateViewPaths, Dictionary srcToDestId) { @@ -324,7 +382,7 @@ private int ProcessCategories(IImportExecuteContext context, category = new Category(); } - row.Initialize(category, name); + row.Initialize(category, name ?? category.Name); if (!row.IsNew && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { @@ -332,29 +390,34 @@ private int ProcessCategories(IImportExecuteContext context, row.NameChanged = true; } - row.SetProperty(context.Result, category, (x) => x.Name); - row.SetProperty(context.Result, category, (x) => x.FullName); - row.SetProperty(context.Result, category, (x) => x.Description); - row.SetProperty(context.Result, category, (x) => x.BottomDescription); - row.SetProperty(context.Result, category, (x) => x.MetaKeywords); - row.SetProperty(context.Result, category, (x) => x.MetaDescription); - row.SetProperty(context.Result, category, (x) => x.MetaTitle); - row.SetProperty(context.Result, category, (x) => x.PageSize, 12); - row.SetProperty(context.Result, category, (x) => x.AllowCustomersToSelectPageSize, true); - row.SetProperty(context.Result, category, (x) => x.PageSizeOptions); - row.SetProperty(context.Result, category, (x) => x.PriceRanges); - row.SetProperty(context.Result, category, (x) => x.ShowOnHomePage); - row.SetProperty(context.Result, category, (x) => x.HasDiscountsApplied); - row.SetProperty(context.Result, category, (x) => x.Published, true); - row.SetProperty(context.Result, category, (x) => x.DisplayOrder); - row.SetProperty(context.Result, category, (x) => x.LimitedToStores); - row.SetProperty(context.Result, category, (x) => x.Alias); - row.SetProperty(context.Result, category, (x) => x.DefaultViewMode); - - var tvp = row.GetDataValue("CategoryTemplateViewPath"); - category.CategoryTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); - - row.SetProperty(context.Result, category, (x) => x.CreatedOnUtc, UtcNow); + row.SetProperty(context.Result, (x) => x.Name); + row.SetProperty(context.Result, (x) => x.FullName); + row.SetProperty(context.Result, (x) => x.Description); + row.SetProperty(context.Result, (x) => x.BottomDescription); + row.SetProperty(context.Result, (x) => x.MetaKeywords); + row.SetProperty(context.Result, (x) => x.MetaDescription); + row.SetProperty(context.Result, (x) => x.MetaTitle); + row.SetProperty(context.Result, (x) => x.PageSize, 12); + row.SetProperty(context.Result, (x) => x.AllowCustomersToSelectPageSize, true); + row.SetProperty(context.Result, (x) => x.PageSizeOptions); + row.SetProperty(context.Result, (x) => x.PriceRanges); + row.SetProperty(context.Result, (x) => x.ShowOnHomePage); + row.SetProperty(context.Result, (x) => x.HasDiscountsApplied); + row.SetProperty(context.Result, (x) => x.Published, true); + row.SetProperty(context.Result, (x) => x.DisplayOrder); + row.SetProperty(context.Result, (x) => x.Alias); + row.SetProperty(context.Result, (x) => x.DefaultViewMode); + // With new entities, "LimitedToStores" is an implicit field, meaning + // it has to be set to true by code if it's absent but "StoreIds" exists. + row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); + + string tvp; + if (row.TryGetDataValue("CategoryTemplateViewPath", out tvp, row.IsTransient)) + { + category.CategoryTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); + } + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); category.UpdatedOnUtc = UtcNow; if (id != 0 && !srcToDestId.ContainsKey(id)) @@ -416,140 +479,11 @@ public static string[] DefaultKeyFields } } - protected override void Import(IImportExecuteContext context) + public class ImportCategoryMapping { - var srcToDestId = new Dictionary(); - - var templateViewPaths = _categoryTemplateService.GetAllCategoryTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); - - using (var scope = new DbContextScope(ctx: _categoryRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) - { - var segmenter = context.GetSegmenter(); - - Init(context, _dataExchangeSettings); - - context.Result.TotalRecords = segmenter.TotalRows; - - while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) - { - var batch = segmenter.CurrentBatch; - - // Perf: detach all entities - _categoryRepository.Context.DetachAll(false); - - context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); - - try - { - ProcessCategories(context, batch, templateViewPaths, srcToDestId); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCategories"); - } - - // reduce batch to saved (valid) products. - // No need to perform import operations on errored products. - batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); - - // update result object - context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); - context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); - - // process slugs - if (segmenter.HasColumn("SeName") || batch.Any(x => x.IsNew || x.NameChanged)) - { - try - { - _categoryRepository.Context.AutoDetectChangesEnabled = true; - ProcessSlugs(context, batch); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); - } - finally - { - _categoryRepository.Context.AutoDetectChangesEnabled = false; - } - } - - // process store mappings - if (segmenter.HasColumn("LimitedToStores") && segmenter.HasColumn("StoreIds")) - { - try - { - _categoryRepository.Context.AutoDetectChangesEnabled = true; - ProcessStoreMappings(context, batch); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); - } - finally - { - _categoryRepository.Context.AutoDetectChangesEnabled = false; - } - } - - // localizations - try - { - ProcessLocalizations(context, batch); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizedProperties"); - } - - // process pictures - if (srcToDestId.Any() && segmenter.HasColumn("ImageUrl")) - { - try - { - _categoryRepository.Context.AutoDetectChangesEnabled = true; - ProcessPictures(context, batch, srcToDestId); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessPictures"); - } - finally - { - _categoryRepository.Context.AutoDetectChangesEnabled = false; - } - } - } - - // map parent id of inserted categories - if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentCategoryId")) - { - segmenter.Reset(); - - while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) - { - var batch = segmenter.CurrentBatch; - - _categoryRepository.Context.DetachAll(false); - - try - { - ProcessParentMappings(context, batch, srcToDestId); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); - } - } - } - } + public int DestinationId { get; set; } + public bool Inserted { get; set; } } } - - internal class ImportCategoryMapping - { - public int DestinationId { get; set; } - public bool Inserted { get; set; } - } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs index 57f8c41976..6228f5b443 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs @@ -3,17 +3,15 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Linq.Expressions; using SmartStore.Core.Async; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Domain.Seo; using SmartStore.Core.Events; using SmartStore.Services.DataExchange.Import; using SmartStore.Services.Localization; using SmartStore.Services.Media; -using SmartStore.Services.Seo; -using SmartStore.Services.Stores; using SmartStore.Utilities; namespace SmartStore.Services.Catalog.Importer @@ -23,7 +21,6 @@ public class ProductImporter : EntityImporterBase private readonly IRepository _productPictureRepository; private readonly IRepository _productManufacturerRepository; private readonly IRepository _productCategoryRepository; - private readonly IRepository _urlRecordRepository; private readonly IRepository _productRepository; private readonly ICommonServices _services; private readonly ILocalizedEntityService _localizedEntityService; @@ -31,18 +28,24 @@ public class ProductImporter : EntityImporterBase private readonly IManufacturerService _manufacturerService; private readonly ICategoryService _categoryService; private readonly IProductService _productService; - private readonly IUrlRecordService _urlRecordService; private readonly IProductTemplateService _productTemplateService; - private readonly IStoreMappingService _storeMappingService; private readonly FileDownloadManager _fileDownloadManager; - private readonly SeoSettings _seoSettings; - private readonly DataExchangeSettings _dataExchangeSettings; + + private static readonly Dictionary>> _localizableProperties = new Dictionary>> + { + { "Name", x => x.Name }, + { "ShortDescription", x => x.ShortDescription }, + { "FullDescription", x => x.FullDescription }, + { "MetaKeywords", x => x.MetaKeywords }, + { "MetaDescription", x => x.MetaDescription }, + { "MetaTitle", x => x.MetaTitle }, + { "BundleTitleText", x => x.BundleTitleText } + }; public ProductImporter( IRepository productPictureRepository, IRepository productManufacturerRepository, IRepository productCategoryRepository, - IRepository urlRecordRepository, IRepository productRepository, ICommonServices services, ILocalizedEntityService localizedEntityService, @@ -50,17 +53,12 @@ public ProductImporter( IManufacturerService manufacturerService, ICategoryService categoryService, IProductService productService, - IUrlRecordService urlRecordService, IProductTemplateService productTemplateService, - IStoreMappingService storeMappingService, - FileDownloadManager fileDownloadManager, - SeoSettings seoSettings, - DataExchangeSettings dataExchangeSettings) + FileDownloadManager fileDownloadManager) { _productPictureRepository = productPictureRepository; _productManufacturerRepository = productManufacturerRepository; _productCategoryRepository = productCategoryRepository; - _urlRecordRepository = urlRecordRepository; _productRepository = productRepository; _services = services; _localizedEntityService = localizedEntityService; @@ -68,30 +66,403 @@ public ProductImporter( _manufacturerService = manufacturerService; _categoryService = categoryService; _productService = productService; - _urlRecordService = urlRecordService; _productTemplateService = productTemplateService; - _storeMappingService = storeMappingService; _fileDownloadManager = fileDownloadManager; - - _seoSettings = seoSettings; - _dataExchangeSettings = dataExchangeSettings; } - private int? ZeroToNull(object value, CultureInfo culture) + protected override void Import(ImportExecuteContext context) { - int result; - if (CommonHelper.TryConvert(value, culture, out result) && result > 0) + var srcToDestId = new Dictionary(); + + var templateViewPaths = _productTemplateService.GetAllProductTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); + + using (var scope = new DbContextScope(ctx: _productRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) { - return result; + var segmenter = context.DataSegmenter; + + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _productRepository.Context.DetachAll(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + // =========================================================================== + // 1.) Import products + // =========================================================================== + try + { + ProcessProducts(context, batch, templateViewPaths, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProducts"); + } + + // reduce batch to saved (valid) products. + // No need to perform import operations on errored products. + batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); + + // update result object + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // =========================================================================== + // 2.) Import SEO Slugs + // IMPORTANT: Unlike with Products AutoCommitEnabled must be TRUE, + // as Slugs are going to be validated against existing ones in DB. + // =========================================================================== + if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) + { + try + { + _productRepository.Context.AutoDetectChangesEnabled = true; + ProcessSlugs(context, batch, typeof(Product).Name); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); + } + finally + { + _productRepository.Context.AutoDetectChangesEnabled = false; + } + } + + // =========================================================================== + // 3.) Import StoreMappings + // =========================================================================== + if (segmenter.HasColumn("StoreIds")) + { + try + { + ProcessStoreMappings(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); + } + } + + // =========================================================================== + // 4.) Import Localizations + // =========================================================================== + try + { + ProcessLocalizations(context, batch, _localizableProperties); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizations"); + } + + // =========================================================================== + // 5.) Import product category mappings + // =========================================================================== + if (segmenter.HasColumn("CategoryIds")) + { + try + { + ProcessProductCategories(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductCategories"); + } + } + + // =========================================================================== + // 6.) Import product manufacturer mappings + // =========================================================================== + if (segmenter.HasColumn("ManufacturerIds")) + { + try + { + ProcessProductManufacturers(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductManufacturers"); + } + } + + // =========================================================================== + // 7.) Import product picture mappings + // =========================================================================== + if (segmenter.HasColumn("ImageUrls")) + { + try + { + ProcessProductPictures(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductPictures"); + } + } + } + + // =========================================================================== + // 8.) Map parent id of inserted products + // =========================================================================== + if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentGroupedProductId") && !segmenter.IsIgnored("ParentGroupedProductId")) + { + segmenter.Reset(); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + _productRepository.Context.DetachAll(false); + + try + { + ProcessProductMappings(context, batch, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); + } + } + } } + } - return (int?)null; + protected virtual int ProcessProducts( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary templateViewPaths, + Dictionary srcToDestId) + { + _productRepository.AutoCommitEnabled = false; + + Product lastInserted = null; + Product lastUpdated = null; + var defaultTemplateId = templateViewPaths["ProductTemplate.Simple"]; + + foreach (var row in batch) + { + Product product = null; + var id = row.GetDataValue("Id"); + + foreach (var keyName in context.KeyFieldNames) + { + var keyValue = row.GetDataValue(keyName); + + if (keyValue.HasValue() || id > 0) + { + switch (keyName) + { + case "Id": + product = _productService.GetProductById(id); + break; + case "Sku": + product = _productService.GetProductBySku(keyValue); + break; + case "Gtin": + product = _productService.GetProductByGtin(keyValue); + break; + case "ManufacturerPartNumber": + product = _productService.GetProductByManufacturerPartNumber(keyValue); + break; + case "Name": + product = _productService.GetProductByName(keyValue); + break; + } + } + + if (product != null) + break; + } + + if (product == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + // a Name is required for new products. + if (!row.HasDataValue("Name")) + { + ++context.Result.SkippedRecords; + context.Result.AddError("The 'Name' field is required for new products. Skipping row.", row.GetRowInfo(), "Name"); + continue; + } + + product = new Product(); + } + + var name = row.GetDataValue("Name"); + + row.Initialize(product, name ?? product.Name); + + if (!row.IsNew) + { + if (!product.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + // Perf: use this later for SeName updates. + row.NameChanged = true; + } + } + + row.SetProperty(context.Result, (x) => x.ProductTypeId, (int)ProductType.SimpleProduct); + row.SetProperty(context.Result, (x) => x.VisibleIndividually, true); + row.SetProperty(context.Result, (x) => x.Name); + row.SetProperty(context.Result, (x) => x.ShortDescription); + row.SetProperty(context.Result, (x) => x.FullDescription); + row.SetProperty(context.Result, (x) => x.AdminComment); + row.SetProperty(context.Result, (x) => x.ShowOnHomePage); + row.SetProperty(context.Result, (x) => x.HomePageDisplayOrder); + row.SetProperty(context.Result, (x) => x.MetaKeywords); + row.SetProperty(context.Result, (x) => x.MetaDescription); + row.SetProperty(context.Result, (x) => x.MetaTitle); + row.SetProperty(context.Result, (x) => x.AllowCustomerReviews, true); + row.SetProperty(context.Result, (x) => x.ApprovedRatingSum); + row.SetProperty(context.Result, (x) => x.NotApprovedRatingSum); + row.SetProperty(context.Result, (x) => x.ApprovedTotalReviews); + row.SetProperty(context.Result, (x) => x.NotApprovedTotalReviews); + row.SetProperty(context.Result, (x) => x.Published, true); + row.SetProperty(context.Result, (x) => x.Sku); + row.SetProperty(context.Result, (x) => x.ManufacturerPartNumber); + row.SetProperty(context.Result, (x) => x.Gtin); + row.SetProperty(context.Result, (x) => x.IsGiftCard); + row.SetProperty(context.Result, (x) => x.GiftCardTypeId); + row.SetProperty(context.Result, (x) => x.RequireOtherProducts); + row.SetProperty(context.Result, (x) => x.RequiredProductIds); // TODO: global scope + row.SetProperty(context.Result, (x) => x.AutomaticallyAddRequiredProducts); + row.SetProperty(context.Result, (x) => x.IsDownload); + row.SetProperty(context.Result, (x) => x.DownloadId); + row.SetProperty(context.Result, (x) => x.UnlimitedDownloads, true); + row.SetProperty(context.Result, (x) => x.MaxNumberOfDownloads, 10); + row.SetProperty(context.Result, (x) => x.DownloadExpirationDays); + row.SetProperty(context.Result, (x) => x.DownloadActivationTypeId, 1); + row.SetProperty(context.Result, (x) => x.HasSampleDownload); + row.SetProperty(context.Result, (x) => x.SampleDownloadId, (int?)null, ZeroToNull); // TODO: global scope + row.SetProperty(context.Result, (x) => x.HasUserAgreement); + row.SetProperty(context.Result, (x) => x.UserAgreementText); + row.SetProperty(context.Result, (x) => x.IsRecurring); + row.SetProperty(context.Result, (x) => x.RecurringCycleLength, 100); + row.SetProperty(context.Result, (x) => x.RecurringCyclePeriodId); + row.SetProperty(context.Result, (x) => x.RecurringTotalCycles, 10); + row.SetProperty(context.Result, (x) => x.IsShipEnabled, true); + row.SetProperty(context.Result, (x) => x.IsFreeShipping); + row.SetProperty(context.Result, (x) => x.AdditionalShippingCharge); + row.SetProperty(context.Result, (x) => x.IsEsd); + row.SetProperty(context.Result, (x) => x.IsTaxExempt); + row.SetProperty(context.Result, (x) => x.TaxCategoryId, 1); // TODO: global scope + row.SetProperty(context.Result, (x) => x.ManageInventoryMethodId); + row.SetProperty(context.Result, (x) => x.StockQuantity, 10000); + row.SetProperty(context.Result, (x) => x.DisplayStockAvailability); + row.SetProperty(context.Result, (x) => x.DisplayStockQuantity); + row.SetProperty(context.Result, (x) => x.MinStockQuantity); + row.SetProperty(context.Result, (x) => x.LowStockActivityId); + row.SetProperty(context.Result, (x) => x.NotifyAdminForQuantityBelow, 1); + row.SetProperty(context.Result, (x) => x.BackorderModeId); + row.SetProperty(context.Result, (x) => x.AllowBackInStockSubscriptions); + row.SetProperty(context.Result, (x) => x.OrderMinimumQuantity, 1); + row.SetProperty(context.Result, (x) => x.OrderMaximumQuantity, 10000); + row.SetProperty(context.Result, (x) => x.AllowedQuantities); + row.SetProperty(context.Result, (x) => x.DisableBuyButton); + row.SetProperty(context.Result, (x) => x.DisableWishlistButton); + row.SetProperty(context.Result, (x) => x.AvailableForPreOrder); + row.SetProperty(context.Result, (x) => x.CallForPrice); + row.SetProperty(context.Result, (x) => x.Price); + row.SetProperty(context.Result, (x) => x.OldPrice); + row.SetProperty(context.Result, (x) => x.ProductCost); + row.SetProperty(context.Result, (x) => x.SpecialPrice); + row.SetProperty(context.Result, (x) => x.SpecialPriceStartDateTimeUtc); + row.SetProperty(context.Result, (x) => x.SpecialPriceEndDateTimeUtc); + row.SetProperty(context.Result, (x) => x.CustomerEntersPrice); + row.SetProperty(context.Result, (x) => x.MinimumCustomerEnteredPrice); + row.SetProperty(context.Result, (x) => x.MaximumCustomerEnteredPrice, 1000); + // HasTierPrices... ignore as long as no tier prices are imported + // LowestAttributeCombinationPrice... ignore as long as no combinations are imported + row.SetProperty(context.Result, (x) => x.Weight); + row.SetProperty(context.Result, (x) => x.Length); + row.SetProperty(context.Result, (x) => x.Width); + row.SetProperty(context.Result, (x) => x.Height); + row.SetProperty(context.Result, (x) => x.DisplayOrder); + row.SetProperty(context.Result, (x) => x.DeliveryTimeId); // TODO: global scope + row.SetProperty(context.Result, (x) => x.QuantityUnitId); // TODO: global scope + row.SetProperty(context.Result, (x) => x.BasePriceEnabled); + row.SetProperty(context.Result, (x) => x.BasePriceMeasureUnit); + row.SetProperty(context.Result, (x) => x.BasePriceAmount); + row.SetProperty(context.Result, (x) => x.BasePriceBaseAmount); + row.SetProperty(context.Result, (x) => x.BundleTitleText); + row.SetProperty(context.Result, (x) => x.BundlePerItemShipping); + row.SetProperty(context.Result, (x) => x.BundlePerItemPricing); + row.SetProperty(context.Result, (x) => x.BundlePerItemShoppingCart); + row.SetProperty(context.Result, (x) => x.AvailableStartDateTimeUtc); + row.SetProperty(context.Result, (x) => x.AvailableEndDateTimeUtc); + // With new entities, "LimitedToStores" is an implicit field, meaning + // it has to be set to true by code if it's absent but "StoreIds" exists. + row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); + + string tvp; + if (row.TryGetDataValue("ProductTemplateViewPath", out tvp, row.IsTransient)) + { + product.ProductTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); + } + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); + product.UpdatedOnUtc = UtcNow; + + if (id != 0 && !srcToDestId.ContainsKey(id)) + { + srcToDestId.Add(id, new ImportProductMapping { Inserted = row.IsTransient }); + } + + if (row.IsTransient) + { + _productRepository.Insert(product); + lastInserted = product; + } + else + { + _productRepository.Update(product); + lastUpdated = product; + } + } + + // commit whole batch at once + var num = _productRepository.Context.SaveChanges(); + + // get new product ids + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + + if (id != 0 && srcToDestId.ContainsKey(id)) + srcToDestId[id].DestinationId = row.Entity.Id; + } + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + { + _services.EventPublisher.EntityInserted(lastInserted); + } + + if (lastUpdated != null) + { + _services.EventPublisher.EntityUpdated(lastUpdated); + } + + return num; } - private int ProcessProductMappings(IImportExecuteContext context, - ImportRow[] batch, + protected virtual int ProcessProductMappings( + ImportExecuteContext context, + IEnumerable> batch, Dictionary srcToDestId) { + _productRepository.AutoCommitEnabled = false; + foreach (var row in batch) { var id = row.GetDataValue("Id"); @@ -106,7 +477,6 @@ private int ProcessProductMappings(IImportExecuteContext context, if (product != null) { product.ParentGroupedProductId = srcToDestId[parentGroupedProductId].DestinationId; - _productRepository.Update(product); } } @@ -118,18 +488,19 @@ private int ProcessProductMappings(IImportExecuteContext context, return num; } - private void ProcessProductPictures(IImportExecuteContext context, ImportRow[] batch) + protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnumerable> batch) { // true, cause pictures must be saved and assigned an id prior adding a mapping. _productPictureRepository.AutoCommitEnabled = true; ProductPicture lastInserted = null; var equalPictureId = 0; + var numberOfPictures = (context.ExtraData.NumberOfPictures ?? int.MaxValue); foreach (var row in batch) { var imageUrls = row.GetDataValue>("ImageUrls"); - if (imageUrls == null || !imageUrls.Any()) + if (imageUrls.IsNullOrEmpty()) continue; var imageNumber = 0; @@ -144,6 +515,9 @@ private void ProcessProductPictures(IImportExecuteContext context, ImportRow= numberOfPictures) + break; } // download images @@ -225,78 +599,7 @@ private void ProcessProductPictures(IImportExecuteContext context, ImportRow[] batch) - { - // true, cause pictures must be saved and assigned an id prior adding a mapping. - _productPictureRepository.AutoCommitEnabled = true; - - ProductPicture lastInserted = null; - var equalPictureId = 0; - - foreach (var row in batch) - { - var count = -1; - var thumbPaths = row.GetDataValue>("PictureThumbPaths"); - - if (thumbPaths == null) - continue; - - foreach (var path in thumbPaths.Where(x => x.HasValue() && File.Exists(x))) - { - try - { - var currentProductPictures = _productPictureRepository.TableUntracked.Expand(x => x.Picture) - .Where(x => x.ProductId == row.Entity.Id) - .ToList(); - - var currentPictures = currentProductPictures - .Select(x => x.Picture) - .ToList(); - - if (count == -1) - { - count = (currentProductPictures.Any() ? currentProductPictures.Select(x => x.DisplayOrder).Max() : 0); - } - - var pictureBinary = _pictureService.FindEqualPicture(path, currentPictures, out equalPictureId); - - if (pictureBinary != null && pictureBinary.Length > 0) - { - // no equal picture found in sequence - var newPicture = _pictureService.InsertPicture(pictureBinary, "image/jpeg", _pictureService.GetPictureSeName(row.EntityDisplayName), true, false, false); - if (newPicture != null) - { - var mapping = new ProductPicture - { - ProductId = row.Entity.Id, - PictureId = newPicture.Id, - DisplayOrder = ++count - }; - - _productPictureRepository.Insert(mapping); - lastInserted = mapping; - } - } - else - { - var idx = thumbPaths.IndexOf(path) + 1; - context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "PictureThumbPaths" + idx.ToString()); - } - } - catch (Exception exception) - { - var idx = thumbPaths.IndexOf(path) + 1; - context.Result.AddWarning(exception.Message, row.GetRowInfo(), "PictureThumbPaths" + idx.ToString()); - } - } - } - - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - _services.EventPublisher.EntityInserted(lastInserted); - } - - private int ProcessProductManufacturers(IImportExecuteContext context, ImportRow[] batch) + protected virtual int ProcessProductManufacturers(ImportExecuteContext context, IEnumerable> batch) { _productManufacturerRepository.AutoCommitEnabled = false; @@ -305,7 +608,7 @@ private int ProcessProductManufacturers(IImportExecuteContext context, ImportRow foreach (var row in batch) { var manufacturerIds = row.GetDataValue>("ManufacturerIds"); - if (manufacturerIds != null && manufacturerIds.Any()) + if (!manufacturerIds.IsNullOrEmpty()) { try { @@ -347,7 +650,7 @@ private int ProcessProductManufacturers(IImportExecuteContext context, ImportRow return num; } - private int ProcessProductCategories(IImportExecuteContext context, ImportRow[] batch) + protected virtual int ProcessProductCategories(ImportExecuteContext context, IEnumerable> batch) { _productCategoryRepository.AutoCommitEnabled = false; @@ -356,7 +659,7 @@ private int ProcessProductCategories(IImportExecuteContext context, ImportRow>("CategoryIds"); - if (categoryIds != null && categoryIds.Any()) + if (!categoryIds.IsNullOrEmpty()) { try { @@ -398,323 +701,16 @@ private int ProcessProductCategories(IImportExecuteContext context, ImportRow[] batch) - { - foreach (var row in batch) - { - foreach (var lang in context.Languages) - { - var name = row.GetDataValue("Name", lang.UniqueSeoCode); - var shortDescription = row.GetDataValue("ShortDescription", lang.UniqueSeoCode); - var fullDescription = row.GetDataValue("FullDescription", lang.UniqueSeoCode); - var metaKeywords = row.GetDataValue("MetaKeywords", lang.UniqueSeoCode); - var metaDescription = row.GetDataValue("MetaDescription", lang.UniqueSeoCode); - var metaTitle = row.GetDataValue("MetaTitle", lang.UniqueSeoCode); - var bundleTitleText = row.GetDataValue("BundleTitleText", lang.UniqueSeoCode); - - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.Name, name, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.ShortDescription, shortDescription, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.FullDescription, fullDescription, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.MetaKeywords, metaKeywords, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.MetaDescription, metaDescription, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.MetaTitle, metaTitle, lang.Id); - _localizedEntityService.SaveLocalizedValue(row.Entity, x => x.BundleTitleText, bundleTitleText, lang.Id); - } - } - - // commit whole batch at once - var num = _productManufacturerRepository.Context.SaveChanges(); - - return num; - } - - private int ProcessSlugs(IImportExecuteContext context, ImportRow[] batch) - { - var slugMap = new Dictionary(100); - Func slugLookup = ((s) => - { - if (slugMap.ContainsKey(s)) - { - return slugMap[s]; - } - return (UrlRecord)null; - }); - - var entityName = typeof(Product).Name; - - foreach (var row in batch) - { - if (row.IsNew || row.NameChanged || row.Segmenter.HasColumn("SeName")) - { - try - { - string seName = row.GetDataValue("SeName"); - seName = row.Entity.ValidateSeName(seName, row.Entity.Name, true, _urlRecordService, _seoSettings, extraSlugLookup: slugLookup); - - UrlRecord urlRecord = null; - if (row.IsNew) - { - // dont't bother validating SeName for new entities. - urlRecord = new UrlRecord - { - EntityId = row.Entity.Id, - EntityName = entityName, - Slug = seName, - LanguageId = 0, - IsActive = true, - }; - _urlRecordRepository.Insert(urlRecord); - } - else - { - urlRecord = _urlRecordService.SaveSlug(row.Entity, seName, 0); - } - - if (urlRecord != null) - { - // a new record was inserted to the store: keep track of it for this batch. - slugMap[seName] = urlRecord; - } - } - catch (Exception exception) - { - context.Result.AddWarning(exception.Message, row.GetRowInfo(), "SeName"); - } - } - } - - // commit whole batch at once - return _urlRecordRepository.Context.SaveChanges(); - } - - private void ProcessStoreMappings(IImportExecuteContext context, ImportRow[] batch) - { - foreach (var row in batch) - { - var limitedToStore = row.GetDataValue("LimitedToStores"); - - if (limitedToStore) - { - var storeIds = row.GetDataValue>("StoreIds"); - - _storeMappingService.SaveStoreMappings(row.Entity, storeIds == null ? new int[0] : storeIds.ToArray()); - } - } - } - - private int ProcessProducts( - IImportExecuteContext context, - ImportRow[] batch, - Dictionary templateViewPaths, - Dictionary srcToDestId) + private int? ZeroToNull(object value, CultureInfo culture) { - _productRepository.AutoCommitEnabled = true; - - Product lastInserted = null; - Product lastUpdated = null; - var defaultTemplateId = templateViewPaths["ProductTemplate.Simple"]; - - foreach (var row in batch) - { - Product product = null; - var id = row.GetDataValue("Id"); - - foreach (var keyName in context.KeyFieldNames) - { - switch (keyName) - { - case "Id": - product = _productService.GetProductById(id); - break; - case "Sku": - product = _productService.GetProductBySku(row.GetDataValue("Sku")); - break; - case "Gtin": - product = _productService.GetProductByGtin(row.GetDataValue("Gtin")); - break; - case "ManufacturerPartNumber": - product = _productService.GetProductByManufacturerPartNumber(row.GetDataValue("ManufacturerPartNumber")); - break; - case "Name": - product = _productService.GetProductByName(row.GetDataValue("Name")); - break; - } - - if (product != null) - break; - } - - if (product == null) - { - if (context.UpdateOnly) - { - ++context.Result.SkippedRecords; - continue; - } - - // a Name is required with new products. - if (!row.Segmenter.HasColumn("Name")) - { - ++context.Result.SkippedRecords; - context.Result.AddError("The 'Name' field is required for new products. Skipping row.", row.GetRowInfo(), "Name"); - continue; - } - - product = new Product(); - } - - var name = row.GetDataValue("Name"); - - row.Initialize(product, name); - - if (!row.IsNew) - { - if (!product.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) - { - // Perf: use this later for SeName updates. - row.NameChanged = true; - } - } - - row.SetProperty(context.Result, product, (x) => x.ProductTypeId, (int)ProductType.SimpleProduct); - row.SetProperty(context.Result, product, (x) => x.VisibleIndividually, true); - row.SetProperty(context.Result, product, (x) => x.Name); - row.SetProperty(context.Result, product, (x) => x.ShortDescription); - row.SetProperty(context.Result, product, (x) => x.FullDescription); - row.SetProperty(context.Result, product, (x) => x.AdminComment); - row.SetProperty(context.Result, product, (x) => x.ShowOnHomePage); - row.SetProperty(context.Result, product, (x) => x.HomePageDisplayOrder); - row.SetProperty(context.Result, product, (x) => x.MetaKeywords); - row.SetProperty(context.Result, product, (x) => x.MetaDescription); - row.SetProperty(context.Result, product, (x) => x.MetaTitle); - row.SetProperty(context.Result, product, (x) => x.AllowCustomerReviews, true); - row.SetProperty(context.Result, product, (x) => x.ApprovedRatingSum); - row.SetProperty(context.Result, product, (x) => x.NotApprovedRatingSum); - row.SetProperty(context.Result, product, (x) => x.ApprovedTotalReviews); - row.SetProperty(context.Result, product, (x) => x.NotApprovedTotalReviews); - row.SetProperty(context.Result, product, (x) => x.Published, true); - row.SetProperty(context.Result, product, (x) => x.Sku); - row.SetProperty(context.Result, product, (x) => x.ManufacturerPartNumber); - row.SetProperty(context.Result, product, (x) => x.Gtin); - row.SetProperty(context.Result, product, (x) => x.IsGiftCard); - row.SetProperty(context.Result, product, (x) => x.GiftCardTypeId); - row.SetProperty(context.Result, product, (x) => x.RequireOtherProducts); - row.SetProperty(context.Result, product, (x) => x.RequiredProductIds); // TODO: global scope - row.SetProperty(context.Result, product, (x) => x.AutomaticallyAddRequiredProducts); - row.SetProperty(context.Result, product, (x) => x.IsDownload); - row.SetProperty(context.Result, product, (x) => x.DownloadId); - row.SetProperty(context.Result, product, (x) => x.UnlimitedDownloads, true); - row.SetProperty(context.Result, product, (x) => x.MaxNumberOfDownloads, 10); - row.SetProperty(context.Result, product, (x) => x.DownloadExpirationDays); - row.SetProperty(context.Result, product, (x) => x.DownloadActivationTypeId, 1); - row.SetProperty(context.Result, product, (x) => x.HasSampleDownload); - row.SetProperty(context.Result, product, (x) => x.SampleDownloadId, (int?)null, ZeroToNull); // TODO: global scope - row.SetProperty(context.Result, product, (x) => x.HasUserAgreement); - row.SetProperty(context.Result, product, (x) => x.UserAgreementText); - row.SetProperty(context.Result, product, (x) => x.IsRecurring); - row.SetProperty(context.Result, product, (x) => x.RecurringCycleLength, 100); - row.SetProperty(context.Result, product, (x) => x.RecurringCyclePeriodId); - row.SetProperty(context.Result, product, (x) => x.RecurringTotalCycles, 10); - row.SetProperty(context.Result, product, (x) => x.IsShipEnabled, true); - row.SetProperty(context.Result, product, (x) => x.IsFreeShipping); - row.SetProperty(context.Result, product, (x) => x.AdditionalShippingCharge); - row.SetProperty(context.Result, product, (x) => x.IsEsd); - row.SetProperty(context.Result, product, (x) => x.IsTaxExempt); - row.SetProperty(context.Result, product, (x) => x.TaxCategoryId, 1); // TODO: global scope - row.SetProperty(context.Result, product, (x) => x.ManageInventoryMethodId); - row.SetProperty(context.Result, product, (x) => x.StockQuantity, 10000); - row.SetProperty(context.Result, product, (x) => x.DisplayStockAvailability); - row.SetProperty(context.Result, product, (x) => x.DisplayStockQuantity); - row.SetProperty(context.Result, product, (x) => x.MinStockQuantity); - row.SetProperty(context.Result, product, (x) => x.LowStockActivityId); - row.SetProperty(context.Result, product, (x) => x.NotifyAdminForQuantityBelow, 1); - row.SetProperty(context.Result, product, (x) => x.BackorderModeId); - row.SetProperty(context.Result, product, (x) => x.AllowBackInStockSubscriptions); - row.SetProperty(context.Result, product, (x) => x.OrderMinimumQuantity, 1); - row.SetProperty(context.Result, product, (x) => x.OrderMaximumQuantity, 10000); - row.SetProperty(context.Result, product, (x) => x.AllowedQuantities); - row.SetProperty(context.Result, product, (x) => x.DisableBuyButton); - row.SetProperty(context.Result, product, (x) => x.DisableWishlistButton); - row.SetProperty(context.Result, product, (x) => x.AvailableForPreOrder); - row.SetProperty(context.Result, product, (x) => x.CallForPrice); - row.SetProperty(context.Result, product, (x) => x.Price); - row.SetProperty(context.Result, product, (x) => x.OldPrice); - row.SetProperty(context.Result, product, (x) => x.ProductCost); - row.SetProperty(context.Result, product, (x) => x.SpecialPrice); - row.SetProperty(context.Result, product, (x) => x.SpecialPriceStartDateTimeUtc); - row.SetProperty(context.Result, product, (x) => x.SpecialPriceEndDateTimeUtc); - row.SetProperty(context.Result, product, (x) => x.CustomerEntersPrice); - row.SetProperty(context.Result, product, (x) => x.MinimumCustomerEnteredPrice); - row.SetProperty(context.Result, product, (x) => x.MaximumCustomerEnteredPrice, 1000); - // HasTierPrices... ignore as long as no tier prices are imported - // LowestAttributeCombinationPrice... ignore as long as no combinations are imported - row.SetProperty(context.Result, product, (x) => x.Weight); - row.SetProperty(context.Result, product, (x) => x.Length); - row.SetProperty(context.Result, product, (x) => x.Width); - row.SetProperty(context.Result, product, (x) => x.Height); - row.SetProperty(context.Result, product, (x) => x.DisplayOrder); - row.SetProperty(context.Result, product, (x) => x.DeliveryTimeId); // TODO: global scope - row.SetProperty(context.Result, product, (x) => x.QuantityUnitId); // TODO: global scope - row.SetProperty(context.Result, product, (x) => x.BasePriceEnabled); - row.SetProperty(context.Result, product, (x) => x.BasePriceMeasureUnit); - row.SetProperty(context.Result, product, (x) => x.BasePriceAmount); - row.SetProperty(context.Result, product, (x) => x.BasePriceBaseAmount); - row.SetProperty(context.Result, product, (x) => x.BundleTitleText); - row.SetProperty(context.Result, product, (x) => x.BundlePerItemShipping); - row.SetProperty(context.Result, product, (x) => x.BundlePerItemPricing); - row.SetProperty(context.Result, product, (x) => x.BundlePerItemShoppingCart); - row.SetProperty(context.Result, product, (x) => x.AvailableStartDateTimeUtc); - row.SetProperty(context.Result, product, (x) => x.AvailableEndDateTimeUtc); - row.SetProperty(context.Result, product, (x) => x.LimitedToStores); - - var tvp = row.GetDataValue("ProductTemplateViewPath"); - product.ProductTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); - - row.SetProperty(context.Result, product, (x) => x.CreatedOnUtc, UtcNow); - product.UpdatedOnUtc = UtcNow; - - if (id != 0 && !srcToDestId.ContainsKey(id)) - { - srcToDestId.Add(id, new ImportProductMapping { Inserted = row.IsTransient }); - } - - if (row.IsTransient) - { - _productRepository.Insert(product); - lastInserted = product; - } - else - { - _productRepository.Update(product); - lastUpdated = product; - } - } - - // commit whole batch at once - var num = _productRepository.Context.SaveChanges(); - - // get new product ids - foreach (var row in batch) - { - var id = row.GetDataValue("Id"); - - if (id != 0 && srcToDestId.ContainsKey(id)) - srcToDestId[id].DestinationId = row.Entity.Id; - } - - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - { - _services.EventPublisher.EntityInserted(lastInserted); - } - - if (lastUpdated != null) + int result; + if (CommonHelper.TryConvert(value, culture, out result) && result > 0) { - _services.EventPublisher.EntityUpdated(lastUpdated); + return result; } - return num; + return (int?)null; } public static string[] SupportedKeyFields @@ -733,177 +729,10 @@ public static string[] DefaultKeyFields } } - protected override void Import(IImportExecuteContext context) + public class ImportProductMapping { - var srcToDestId = new Dictionary(); - - var templateViewPaths = _productTemplateService.GetAllProductTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); - - using (var scope = new DbContextScope(ctx: _productRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) - { - var segmenter = context.GetSegmenter(); - - Init(context, _dataExchangeSettings); - - context.Result.TotalRecords = segmenter.TotalRows; - - while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) - { - var batch = segmenter.CurrentBatch; - - // Perf: detach all entities - _productRepository.Context.DetachAll(false); - - context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); - - // =========================================================================== - // 1.) Import products - // =========================================================================== - try - { - ProcessProducts(context, batch, templateViewPaths, srcToDestId); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProducts"); - } - - // reduce batch to saved (valid) products. - // No need to perform import operations on errored products. - batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); - - // update result object - context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); - context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); - - // =========================================================================== - // 2.) Import SEO Slugs - // IMPORTANT: Unlike with Products AutoCommitEnabled must be TRUE, - // as Slugs are going to be validated against existing ones in DB. - // =========================================================================== - if (segmenter.HasColumn("SeName") || batch.Any(x => x.IsNew || x.NameChanged)) - { - try - { - _productRepository.Context.AutoDetectChangesEnabled = true; - ProcessSlugs(context, batch); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); - } - finally - { - _productRepository.Context.AutoDetectChangesEnabled = false; - } - } - - if (segmenter.HasColumn("LimitedToStores") && segmenter.HasColumn("StoreIds")) - { - try - { - _productRepository.Context.AutoDetectChangesEnabled = true; - ProcessStoreMappings(context, batch); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); - } - finally - { - _productRepository.Context.AutoDetectChangesEnabled = false; - } - } - - // =========================================================================== - // 3.) Import Localizations - // =========================================================================== - try - { - ProcessLocalizations(context, batch); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizations"); - } - - // =========================================================================== - // 4.) Import product category mappings - // =========================================================================== - if (segmenter.HasColumn("CategoryIds")) - { - try - { - ProcessProductCategories(context, batch); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductCategories"); - } - } - - // =========================================================================== - // 5.) Import product manufacturer mappings - // =========================================================================== - if (segmenter.HasColumn("ManufacturerIds")) - { - try - { - ProcessProductManufacturers(context, batch); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductManufacturers"); - } - } - - // =========================================================================== - // 6.) Import product picture mappings - // =========================================================================== - if (segmenter.HasColumn("ImageUrls")) - { - try - { - ProcessProductPictures(context, batch); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductPictures"); - } - } - } - - // =========================================================================== - // 7.) Map parent id of inserted products - // =========================================================================== - if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentGroupedProductId")) - { - segmenter.Reset(); - - while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) - { - var batch = segmenter.CurrentBatch; - - _productRepository.Context.DetachAll(false); - - try - { - ProcessProductMappings(context, batch, srcToDestId); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); - } - } - } - } + public int DestinationId { get; set; } + public bool Inserted { get; set; } } } - - - internal class ImportProductMapping - { - public int DestinationId { get; set; } - public bool Inserted { get; set; } - } } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index 7a06368334..ca45f495d7 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -251,6 +251,12 @@ protected virtual decimal GetPreselectedPrice(Product product, PriceCalculationC if (selectedCombination != null && selectedCombination.IsActive && selectedCombination.Price.HasValue) { product.MergedDataValues = new Dictionary { { "Price", selectedCombination.Price.Value } }; + + if (selectedCombination.BasePriceAmount.HasValue) + product.MergedDataValues.Add("BasePriceAmount", selectedCombination.BasePriceAmount.Value); + + if (selectedCombination.BasePriceBaseAmount.HasValue) + product.MergedDataValues.Add("BasePriceBaseAmount", selectedCombination.BasePriceBaseAmount.Value); } } @@ -758,10 +764,12 @@ public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, var attributesTotalPrice = decimal.Zero; - var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml).ToList(); + var pvaValuesEnum = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml); - if (pvaValues != null) + if (pvaValuesEnum != null) { + var pvaValues = pvaValuesEnum.ToList(); + foreach (var pvaValue in pvaValues) { attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue); diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs index 23ee794f0b..cf4531f322 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs @@ -13,14 +13,13 @@ using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; -using SmartStore.Services.Localization; namespace SmartStore.Services.Catalog { - /// - /// Product attribute parser - /// - public partial class ProductAttributeParser : IProductAttributeParser + /// + /// Product attribute parser + /// + public partial class ProductAttributeParser : IProductAttributeParser { // 0 = ProductId, 1 = AttributeXml Hash private const string ATTRIBUTECOMBINATION_BY_ID_HASH = "SmartStore.parsedattributecombination.id-{0}-{1}"; @@ -194,21 +193,20 @@ where id.HasValue() return values; } - public IList ParseProductVariantAttributeValues(string attributesXml, IEnumerable attributes, int languageId = 0) + public virtual IList ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes) { - var values = new List(); + var result = new List(); - if (attributesXml.IsEmpty()) - return values; + if (attributeCombination == null || !attributeCombination.Any()) + return result; var allValueIds = new List(); - var combinedAttributes = DeserializeProductVariantAttributes(attributesXml); foreach (var pva in attributes.Where(x => x.ShouldHaveValues()).OrderBy(x => x.DisplayOrder)) { - if (combinedAttributes.ContainsKey(pva.Id)) + if (attributeCombination.ContainsKey(pva.Id)) { - var pvaValuesStr = combinedAttributes[pva.Id]; + var pvaValuesStr = attributeCombination[pva.Id]; var ids = pvaValuesStr.Where(x => x.HasValue()).Select(x => x.ToInt()); allValueIds.AddRange(ids); @@ -220,28 +218,24 @@ public IList ParseProductVariantAttributeValues(string attributesXml, IE foreach (var attribute in attributes) { var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == id); - if (attributeValue != null) + if (attributeValue != null && !result.Any(x => x.Id == attributeValue.Id)) { - var value = attributeValue.GetLocalized(x => x.Name, languageId, true, false); - - if (!values.Any(x => x.IsCaseInsensitiveEqual(value))) - values.Add(value); + result.Add(attributeValue); break; } } } - return values; + return result; } - - /// - /// Gets selected product variant attribute value - /// - /// Attributes - /// Product variant attribute identifier - /// Product variant attribute value - public virtual IList ParseValues(string attributesXml, int productVariantAttributeId) + /// + /// Gets selected product variant attribute value + /// + /// Attributes + /// Product variant attribute identifier + /// Product variant attribute value + public virtual IList ParseValues(string attributesXml, int productVariantAttributeId) { var selectedProductVariantAttributeValues = new List(); try @@ -374,15 +368,20 @@ public virtual ProductVariantAttributeCombination FindProductVariantAttributeCom public virtual List> DeserializeQueryData(string jsonData) { - if (jsonData.HasValue()) + try { - if (jsonData.StartsWith("[")) + if (jsonData.HasValue()) { - return JsonConvert.DeserializeObject>>(jsonData); - } + if (jsonData.StartsWith("[")) + { + return JsonConvert.DeserializeObject>>(jsonData); + } - return new List> { JsonConvert.DeserializeObject>(jsonData) }; + return new List> { JsonConvert.DeserializeObject>(jsonData) }; + } } + catch { } + return new List>(); } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs index 85b55cfb4f..051fc841f2 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs @@ -14,7 +14,7 @@ namespace SmartStore.Services.Catalog { - public static class ProductExtensions + public static class ProductExtensions { public static ProductVariantAttributeCombination MergeWithCombination(this Product product, string selectedAttributes) { @@ -299,7 +299,7 @@ public static int[] ParseRequiredProductIds(this Product product) } /// - /// Gets the base price + /// Gets the base price info /// /// Product /// Localization service @@ -309,17 +309,23 @@ public static int[] ParseRequiredProductIds(this Product product) /// Price calculation service /// Target currency /// Price adjustment - /// Whether the result string should be language independent - /// The base price - public static string GetBasePriceInfo(this Product product, ILocalizationService localizationService, IPriceFormatter priceFormatter, - ICurrencyService currencyService, ITaxService taxService, IPriceCalculationService priceCalculationService, - Currency currency, decimal priceAdjustment = decimal.Zero, bool languageIndependent = false) + /// Whether the result string should be language insensitive + /// The base price info + public static string GetBasePriceInfo(this Product product, + ILocalizationService localizationService, + IPriceFormatter priceFormatter, + ICurrencyService currencyService, + ITaxService taxService, + IPriceCalculationService priceCalculationService, + Currency currency, + decimal priceAdjustment = decimal.Zero, + bool languageInsensitive = false) { - if (product == null) - throw new ArgumentNullException("product"); - - if (localizationService == null && !languageIndependent) - throw new ArgumentNullException("localizationService"); + Guard.ArgumentNotNull(() => product); + Guard.ArgumentNotNull(() => currencyService); + Guard.ArgumentNotNull(() => taxService); + Guard.ArgumentNotNull(() => priceCalculationService); + Guard.ArgumentNotNull(() => currency); if (product.BasePriceHasValue && product.BasePriceAmount != Decimal.Zero) { @@ -327,24 +333,58 @@ public static string GetBasePriceInfo(this Product product, ILocalizationService var taxrate = decimal.Zero; var currentPrice = priceCalculationService.GetFinalPrice(product, workContext.CurrentCustomer, true); - decimal price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), out taxrate); + var price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), out taxrate); price = currencyService.ConvertFromPrimaryStoreCurrency(price, currency); - decimal basePriceValue = Convert.ToDecimal((price / product.BasePriceAmount) * product.BasePriceBaseAmount); + return product.GetBasePriceInfo(price, localizationService, priceFormatter, currency, languageInsensitive); + } - string basePrice = priceFormatter.FormatPrice(basePriceValue, true, currency); - string unit = "{0} {1}".FormatWith(product.BasePriceBaseAmount, product.BasePriceMeasureUnit); + return ""; + } - if (languageIndependent) - { - return "{0} / {1}".FormatWith(basePrice, unit); - } + /// + /// Gets the base price info + /// + /// Product + /// The calculated product price + /// 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) + { + Guard.ArgumentNotNull(() => product); + Guard.ArgumentNotNull(() => localizationService); + Guard.ArgumentNotNull(() => priceFormatter); + Guard.ArgumentNotNull(() => currency); + + if (product.BasePriceHasValue && product.BasePriceAmount != Decimal.Zero) + { + var value = Convert.ToDecimal((productPrice / product.BasePriceAmount) * product.BasePriceBaseAmount); + var valueFormatted = priceFormatter.FormatPrice(value, true, currency); + var amountFormatted = Math.Round(product.BasePriceAmount.Value, 2).ToString("G29"); + + var infoTemplate = localizationService.GetResource(languageInsensitive ? "Products.BasePriceInfo.LanguageInsensitive" : "Products.BasePriceInfo"); + + var result = infoTemplate.FormatInvariant( + amountFormatted, + product.BasePriceMeasureUnit, + valueFormatted, + product.BasePriceBaseAmount + ); + + return result; + } - return localizationService.GetResource("Products.BasePriceInfo").FormatWith(basePrice, unit); - } return ""; - } + } public static string GetProductTypeLabel(this Product product, ILocalizationService localizationService) { diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs index 3f4f76cc20..6dc93b582d 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs @@ -1156,7 +1156,7 @@ public virtual Product GetProductByManufacturerPartNumber(string manufacturerPar manufacturerPartNumber = manufacturerPartNumber.Trim(); - var product = _productRepository + var product = _productRepository.Table .Where(x => !x.Deleted && x.ManufacturerPartNumber == manufacturerPartNumber) .OrderBy(x => x.Id) .FirstOrDefault(); @@ -1171,7 +1171,7 @@ public virtual Product GetProductByName(string name) name = name.Trim(); - var product = _productRepository + var product = _productRepository.Table .Where(x => !x.Deleted && x.Name == name) .OrderBy(x => x.Id) .FirstOrDefault(); diff --git a/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs b/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs index 73afed9723..1055aaa340 100644 --- a/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs +++ b/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using System.Text; using SmartStore.Core.Domain.Common; @@ -7,50 +8,13 @@ namespace SmartStore.Services.Common { public static class AddressExtentions { - /// - /// Find an address - /// - /// Source - /// First name - /// Last name - /// Phone number - /// Email - /// Fax number - /// Company - /// Address 1 - /// Address 2 - /// City - /// State/province identifier - /// Zip postal code - /// Country identifier - /// Address - public static Address FindAddress(this List
source, - string firstName, string lastName, string phoneNumber, - string email, string faxNumber, string company, string address1, - string address2, string city, int? stateProvinceId, - string zipPostalCode, int? countryId) - { - return source.Find((a) => ((String.IsNullOrEmpty(a.FirstName) && String.IsNullOrEmpty(firstName)) || a.FirstName == firstName) && - ((String.IsNullOrEmpty(a.LastName) && String.IsNullOrEmpty(lastName)) || a.LastName == lastName) && - ((String.IsNullOrEmpty(a.PhoneNumber) && String.IsNullOrEmpty(phoneNumber)) || a.PhoneNumber == phoneNumber) && - ((String.IsNullOrEmpty(a.Email) && String.IsNullOrEmpty(email)) || a.Email == email) && - ((String.IsNullOrEmpty(a.FaxNumber) && String.IsNullOrEmpty(faxNumber)) || a.FaxNumber == faxNumber) && - ((String.IsNullOrEmpty(a.Company) && String.IsNullOrEmpty(company)) || a.Company == company) && - ((String.IsNullOrEmpty(a.Address1) && String.IsNullOrEmpty(address1)) || a.Address1 == address1) && - ((String.IsNullOrEmpty(a.Address2) && String.IsNullOrEmpty(address2)) || a.Address2 == address2) && - ((String.IsNullOrEmpty(a.City) && String.IsNullOrEmpty(city)) || a.City == city) && - ((a.StateProvinceId.IsNullOrDefault() && stateProvinceId.IsNullOrDefault()) || a.StateProvinceId == stateProvinceId) && - ((String.IsNullOrEmpty(a.ZipPostalCode) && String.IsNullOrEmpty(zipPostalCode)) || a.ZipPostalCode == zipPostalCode) && - ((a.CountryId.IsNullOrDefault() && countryId.IsNullOrDefault()) || a.CountryId == countryId)); - } - /// - /// Find first occurrence of address + /// Find first occurrence of an address /// /// Addresses to be searched /// Address to find /// First occurrence of address - public static Address FindAddress(this List
source, Address address) + public static Address FindAddress(this ICollection
source, Address address) { return source.FindAddress( address.FirstName, @@ -68,6 +32,57 @@ public static Address FindAddress(this List
source, Address address) ); } + /// + /// Find an address + /// + /// Source + /// First name + /// Last name + /// Phone number + /// Email + /// Fax number + /// Company + /// Address 1 + /// Address 2 + /// City + /// State/province identifier + /// Zip postal code + /// Country identifier + /// Address + public static Address FindAddress( + this ICollection
source, + string firstName, + string lastName, + string phoneNumber, + string email, + string faxNumber, + string company, + string address1, + string address2, + string city, + int? stateProvinceId, + string zipPostalCode, + int? countryId) + { + Func addressMatcher = (x) => + { + return x.Email.IsCaseInsensitiveEqual(email) + && x.LastName.IsCaseInsensitiveEqual(lastName) + && x.FirstName.IsCaseInsensitiveEqual(firstName) + && x.Address1.IsCaseInsensitiveEqual(address1) + && x.Address2.IsCaseInsensitiveEqual(address2) + && x.Company.IsCaseInsensitiveEqual(company) + && x.ZipPostalCode.IsCaseInsensitiveEqual(zipPostalCode) + && x.City.IsCaseInsensitiveEqual(city) + && x.PhoneNumber.IsCaseInsensitiveEqual(phoneNumber) + && x.FaxNumber.IsCaseInsensitiveEqual(faxNumber) + && x.StateProvinceId == stateProvinceId + && x.CountryId == countryId; + }; + + return source.FirstOrDefault(addressMatcher); + } + /// Returns the full name of the address. public static string GetFullName(this Address address) { diff --git a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs index d57222a18a..8f23424525 100644 --- a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs @@ -229,12 +229,12 @@ public virtual void SaveAttribute(int entityId, string key, string ke { if (string.IsNullOrWhiteSpace(valueStr)) { - //delete + // delete DeleteAttribute(prop); } else { - //update + // update prop.Value = valueStr; UpdateAttribute(prop); } @@ -243,7 +243,7 @@ public virtual void SaveAttribute(int entityId, string key, string ke { if (!string.IsNullOrWhiteSpace(valueStr)) { - //insert + // insert prop = new GenericAttribute { EntityId = entityId, diff --git a/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs b/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs index 060c127d3f..a6b8e24305 100644 --- a/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs +++ b/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs @@ -205,7 +205,8 @@ public virtual bool IsBot { if (!_isBot.HasValue) { - _isBot = _httpContext.Request.Browser.Crawler || this.Device.IsBot || this.UserAgent.IsBot; + // empty useragent > bad bot! + _isBot = this.RawValue.IsEmpty() || _httpContext.Request.Browser.Crawler || this.Device.IsBot || this.UserAgent.IsBot; } return _isBot.Value; } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs b/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs index 719f144ed5..afc1acdbe0 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs @@ -53,7 +53,7 @@ public static bool IsBackgroundTaskAccount(this Customer customer) } /// - /// Gets a value indicating whether customer a search engine + /// Gets a value indicating whether customer is a search engine /// /// Customer /// Result @@ -84,13 +84,13 @@ public static bool IsPdfConverter(this Customer customer) return result; } - /// - /// Gets a value indicating whether customer is administrator - /// - /// Customer - /// A value indicating whether we should look only in active customer roles - /// Result - public static bool IsAdmin(this Customer customer, bool onlyActiveCustomerRoles = true) + /// + /// Gets a value indicating whether customer is administrator + /// + /// Customer + /// A value indicating whether we should look only in active customer roles + /// Result + public static bool IsAdmin(this Customer customer, bool onlyActiveCustomerRoles = true) { return IsInCustomerRole(customer, SystemCustomerRoleNames.Administrators, onlyActiveCustomerRoles); } @@ -287,14 +287,15 @@ public static int CountProductsInCart(this Customer customer, ShoppingCartType c return count; } - public static List GetCartItems(this Customer customer, ShoppingCartType cartType, int? storeId = null, bool orderById = false) + + public static List GetCartItems(this Customer customer, ShoppingCartType cartType, int? storeId = null) { var rawItems = customer.ShoppingCartItems.Filter(cartType, storeId); - if (orderById) - rawItems = rawItems.OrderByDescending(x => x.Id); - - var organizedItems = rawItems.ToList().Organize(); + var organizedItems = rawItems + .OrderByDescending(x => x.Id) + .ToList() + .Organize(); return organizedItems.ToList(); } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs index 3e60ff4baa..d48fe10d6c 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; +using System.Web; using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; @@ -16,6 +17,7 @@ using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Events; using SmartStore.Core.Localization; +using SmartStore.Core.Fakes; using SmartStore.Services.Common; using SmartStore.Services.Localization; @@ -26,9 +28,9 @@ namespace SmartStore.Services.Customers ///
public partial class CustomerService : ICustomerService { - #region Constants + #region Constants - private const string CUSTOMERROLES_ALL_KEY = "SmartStore.customerrole.all-{0}"; + private const string CUSTOMERROLES_ALL_KEY = "SmartStore.customerrole.all-{0}"; private const string CUSTOMERROLES_BY_SYSTEMNAME_KEY = "SmartStore.customerrole.systemname-{0}"; private const string CUSTOMERROLES_PATTERN_KEY = "SmartStore.customerrole."; @@ -41,31 +43,38 @@ public partial class CustomerService : ICustomerService private readonly IRepository _gaRepository; private readonly IRepository _rewardPointsHistoryRepository; private readonly IGenericAttributeService _genericAttributeService; - private readonly ICacheManager _cacheManager; - private readonly IEventPublisher _eventPublisher; + private readonly ICommonServices _services; + private readonly ICacheManager _cache; private readonly RewardPointsSettings _rewardPointsSettings; + private readonly HttpContextBase _httpContext; + private readonly IUserAgent _userAgent; - #endregion + #endregion - #region Ctor + #region Ctor - public CustomerService(ICacheManager cacheManager, + public CustomerService( IRepository customerRepository, IRepository customerRoleRepository, IRepository gaRepository, IRepository rewardPointsHistoryRepository, IGenericAttributeService genericAttributeService, - IEventPublisher eventPublisher, - RewardPointsSettings rewardPointsSettings) + ICommonServices services, + ICacheManager cache, + RewardPointsSettings rewardPointsSettings, + HttpContextBase httpContext, + IUserAgent userAgent) { - this._cacheManager = cacheManager; this._customerRepository = customerRepository; this._customerRoleRepository = customerRoleRepository; this._gaRepository = gaRepository; this._rewardPointsHistoryRepository = rewardPointsHistoryRepository; this._genericAttributeService = genericAttributeService; - this._eventPublisher = eventPublisher; + this._services = services; + this._cache = cache; this._rewardPointsSettings = rewardPointsSettings; + this._httpContext = httpContext; + this._userAgent = userAgent; T = NullLocalizer.Instance; } @@ -332,6 +341,7 @@ orderby c.Id where c.SystemName == systemName select c; var customer = query.FirstOrDefault(); + return customer; } @@ -349,27 +359,62 @@ orderby c.Id return customer; } - public virtual Customer InsertGuestCustomer() + public virtual Customer InsertGuestCustomer(Guid? customerGuid = null) { var customer = new Customer { - CustomerGuid = Guid.NewGuid(), + CustomerGuid = customerGuid ?? Guid.NewGuid(), Active = true, CreatedOnUtc = DateTime.UtcNow, LastActivityDateUtc = DateTime.UtcNow, }; - //add to 'Guests' role + // add to 'Guests' role var guestRole = GetCustomerRoleBySystemName(SystemCustomerRoleNames.Guests); if (guestRole == null) throw new SmartException("'Guests' role could not be loaded"); customer.CustomerRoles.Add(guestRole); - _customerRepository.Insert(customer); + _customerRepository.Insert(customer); + + var clientIdent = _services.WebHelper.GetClientIdent(); + if (clientIdent.HasValue()) + { + //_genericAttributeService.SaveAttribute(customer, "ClientIdent", clientIdent); + } return customer; } + + public virtual Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60) + { + if (_httpContext.IsFakeContext() || _userAgent.IsBot || _userAgent.IsPdfConverter) + { + return null; + } + + clientIdent = clientIdent.NullEmpty() ?? _services.WebHelper.GetClientIdent(); + if (clientIdent.IsEmpty()) + { + return null; + } + + var dateFrom = DateTime.UtcNow.AddSeconds(maxAgeSeconds * -1); + + var query = from a in _gaRepository.TableUntracked + join c in _customerRepository.Table on a.EntityId equals c.Id into Customers + from c in Customers.DefaultIfEmpty() + where c.LastActivityDateUtc >= dateFrom + && c.Username == null + && c.Email == null + && a.KeyGroup == "Customer" + && a.Key == "ClientIdent" + && a.Value == clientIdent + select c; + + return query.FirstOrDefault(); + } public virtual void InsertCustomer(Customer customer) { @@ -378,8 +423,7 @@ public virtual void InsertCustomer(Customer customer) _customerRepository.Insert(customer); - //event notification - _eventPublisher.EntityInserted(customer); + _services.EventPublisher.EntityInserted(customer); } public virtual void UpdateCustomer(Customer customer) @@ -389,8 +433,7 @@ public virtual void UpdateCustomer(Customer customer) _customerRepository.Update(customer); - //event notification - _eventPublisher.EntityUpdated(customer); + _services.EventPublisher.EntityUpdated(customer); } public virtual void ResetCheckoutData(Customer customer, int storeId, @@ -569,10 +612,9 @@ public virtual void DeleteCustomerRole(CustomerRole customerRole) _customerRoleRepository.Delete(customerRole); - _cacheManager.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); + _cache.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); - //event notification - _eventPublisher.EntityDeleted(customerRole); + _services.EventPublisher.EntityDeleted(customerRole); } public virtual CustomerRole GetCustomerRoleById(int customerRoleId) @@ -589,7 +631,7 @@ public virtual CustomerRole GetCustomerRoleBySystemName(string systemName) return null; string key = string.Format(CUSTOMERROLES_BY_SYSTEMNAME_KEY, systemName); - return _cacheManager.Get(key, () => + return _cache.Get(key, () => { var query = from cr in _customerRoleRepository.Table orderby cr.Id @@ -603,7 +645,7 @@ orderby cr.Id public virtual IList GetAllCustomerRoles(bool showHidden = false) { string key = string.Format(CUSTOMERROLES_ALL_KEY, showHidden); - return _cacheManager.Get(key, () => + return _cache.Get(key, () => { var query = from cr in _customerRoleRepository.Table orderby cr.Name @@ -621,10 +663,9 @@ public virtual void InsertCustomerRole(CustomerRole customerRole) _customerRoleRepository.Insert(customerRole); - _cacheManager.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); + _cache.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); - //event notification - _eventPublisher.EntityInserted(customerRole); + _services.EventPublisher.EntityInserted(customerRole); } public virtual void UpdateCustomerRole(CustomerRole customerRole) @@ -634,10 +675,9 @@ public virtual void UpdateCustomerRole(CustomerRole customerRole) _customerRoleRepository.Update(customerRole); - _cacheManager.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); + _cache.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); - //event notification - _eventPublisher.EntityUpdated(customerRole); + _services.EventPublisher.EntityUpdated(customerRole); } #endregion diff --git a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs index 0f7a914a77..57a2c6f8c9 100644 --- a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs @@ -125,14 +125,26 @@ IPagedList GetOnlineCustomers(DateTime lastActivityFromUtc, /// /// Insert a guest customer /// + /// The customer GUID. Pass null to create a random one. /// Customer - Customer InsertGuestCustomer(); + Customer InsertGuestCustomer(Guid? customerGuid = null); - /// - /// Insert a customer - /// - /// Customer - void InsertCustomer(Customer customer); + /// + /// Tries to find a guest/anonymous customer record by client ident. This method should be called when an + /// anonymous visitor rejects cookies and therefore cannot be identified automatically. + /// + /// + /// The client ident string, which is a hashed combination of client IP address and user agent. + /// Call to obtain an ident string, or pass null to let this method obtain it automatically. + /// The max age of the newly created guest customer record. The shorter, the better (default is 1 min.) + /// The identified customer or null + Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60); + + /// + /// Insert a customer + /// + /// Customer + void InsertCustomer(Customer customer); /// /// Updates the customer diff --git a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs index c50e3cad9d..452f281fb8 100644 --- a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs +++ b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Expressions; using SmartStore.Core.Async; using SmartStore.Core.Data; using SmartStore.Core.Domain.Common; @@ -38,7 +39,6 @@ public class CustomerImporter : EntityImporterBase private readonly CustomerSettings _customerSettings; private readonly DateTimeSettings _dateTimeSettings; private readonly ForumSettings _forumSettings; - private readonly DataExchangeSettings _dataExchangeSettings; public CustomerImporter( IRepository customerRepository, @@ -53,8 +53,7 @@ public CustomerImporter( FileDownloadManager fileDownloadManager, CustomerSettings customerSettings, DateTimeSettings dateTimeSettings, - ForumSettings forumSettings, - DataExchangeSettings dataExchangeSettings) + ForumSettings forumSettings) { _customerRepository = customerRepository; _pictureRepository = pictureRepository; @@ -69,171 +68,363 @@ public CustomerImporter( _customerSettings = customerSettings; _dateTimeSettings = dateTimeSettings; _forumSettings = forumSettings; - _dataExchangeSettings = dataExchangeSettings; } - private int? CountryCodeToId(Dictionary allCountries, string code) + + protected override void Import(ImportExecuteContext context) { - if (code.HasValue() && allCountries.ContainsKey(code)) + var customer = _services.WorkContext.CurrentCustomer; + var allowManagingCustomerRoles = _services.Permissions.Authorize(StandardPermissionProvider.ManageCustomerRoles, customer); + + var allAffiliateIds = _affiliateService.GetAllAffiliates(true) + .Select(x => x.Id) + .ToList(); + + var allCountries = new Dictionary(); + foreach (var country in _countryService.GetAllCountries(true)) { - var countryId = allCountries[code]; + if (!allCountries.ContainsKey(country.TwoLetterIsoCode)) + allCountries.Add(country.TwoLetterIsoCode, country.Id); - if (countryId != 0) - return countryId; + if (!allCountries.ContainsKey(country.ThreeLetterIsoCode)) + allCountries.Add(country.ThreeLetterIsoCode, country.Id); } - return null; - } - private int? StateAbbreviationToId(Dictionary, int> allStateProvinces, int? countryId, string abbreviation) - { - if (countryId.HasValue && abbreviation.HasValue()) + var allStateProvinces = _stateProvinceService.GetAllStateProvinces(true) + .ToDictionarySafe(x => new Tuple(x.CountryId, x.Abbreviation), x => x.Id); + + var allCustomerNumbers = new HashSet( + _genericAttributeService.GetAttributes(SystemCustomerAttributeNames.CustomerNumber, _attributeKeyGroup).Select(x => x.Value), + StringComparer.OrdinalIgnoreCase); + + using (var scope = new DbContextScope(ctx: _services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) { - var key = Tuple.Create(countryId.Value, abbreviation); + var segmenter = context.DataSegmenter; - if (allStateProvinces.ContainsKey(key)) + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) { - var stateId = allStateProvinces[key]; + var batch = segmenter.GetCurrentBatch(); - if (stateId != 0) - return stateId; - } - } - return null; - } + _customerRepository.Context.DetachAll(false); - private void SaveAttribute(ImportRow row, string key) - { - _genericAttributeService.SaveAttribute(row.Entity.Id, key, _attributeKeyGroup, row.GetDataValue(key)); - } + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); - private void UpsertRole(ImportRow row, CustomerRole role, bool value) - { - if (role != null) - { - var hasRole = row.Entity.CustomerRoles.Any(x => x.SystemName == role.SystemName); + // =========================================================================== + // Process customers + // =========================================================================== + try + { + ProcessCustomers(context, batch, allAffiliateIds); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCustomers"); + } + + // reduce batch to saved (valid) records. + // No need to perform import operations on errored records. + batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); + + // update result object + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // =========================================================================== + // Process generic attributes + // =========================================================================== + try + { + ProcessGenericAttributes(context, batch, allCountries, allStateProvinces, allCustomerNumbers); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessGenericAttributes"); + } + + // =========================================================================== + // Process avatars + // =========================================================================== + if (_customerSettings.AllowCustomersToUploadAvatars) + { + try + { + ProcessAvatars(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessAvatars"); + } + } - if (value && !hasRole) - row.Entity.CustomerRoles.Add(role); - else if (!value && hasRole) - row.Entity.CustomerRoles.Remove(role); + // =========================================================================== + // Process addresses + // =========================================================================== + try + { + _services.DbContext.AutoDetectChangesEnabled = true; + ProcessAddresses(context, batch, allCountries, allStateProvinces); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessAddresses"); + } + finally + { + _services.DbContext.AutoDetectChangesEnabled = false; + } + } } } - private void ImportAvatar(IImportExecuteContext context, ImportRow row) + protected virtual int ProcessCustomers( + ImportExecuteContext context, + IEnumerable> batch, + List allAffiliateIds) { - var urlOrPath = row.GetDataValue("AvatarPictureUrl"); - if (urlOrPath.IsEmpty()) - return; + _customerRepository.AutoCommitEnabled = true; - Picture picture = null; - var equalPictureId = 0; - var currentPictures = new List(); - var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); - var image = CreateDownloadImage(urlOrPath, seoName, 1); + Customer lastInserted = null; + Customer lastUpdated = null; + var currentCustomer = _services.WorkContext.CurrentCustomer; - if (image == null) - return; + var guestRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Guests); + var registeredRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Registered); + var forumModeratorRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.ForumModerators); - if (image.Url.HasValue() && !image.Success.HasValue) - { - AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, new FileDownloadManagerItem[] { image })); - } + var customerQuery = _customerRepository.Table.Expand(x => x.Addresses); - if ((image.Success ?? false) && File.Exists(image.Path)) + foreach (var row in batch) { - Succeeded(image); - var pictureBinary = File.ReadAllBytes(image.Path); + Customer customer = null; + var id = row.GetDataValue("Id"); + var email = row.GetDataValue("Email"); - if (pictureBinary != null && pictureBinary.Length > 0) + foreach (var keyName in context.KeyFieldNames) { - var currentPictureId = row.Entity.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId); - if (currentPictureId != 0 && (picture = _pictureRepository.GetById(currentPictureId)) != null) - currentPictures.Add(picture); + switch (keyName) + { + case "Id": + if (id != 0) + { + customer = customerQuery.FirstOrDefault(x => x.Id == id); + } + break; + case "CustomerGuid": + var customerGuid = row.GetDataValue("CustomerGuid"); + if (customerGuid.HasValue()) + { + var guid = new Guid(customerGuid); + customer = customerQuery.FirstOrDefault(x => x.CustomerGuid == guid); + } + break; + case "Email": + if (email.HasValue()) + { + customer = customerQuery.FirstOrDefault(x => x.Email == email); + } + break; + case "Username": + var userName = row.GetDataValue("Username"); + if (userName.HasValue()) + { + customer = customerQuery.FirstOrDefault(x => x.Username == userName); + } + break; + } - pictureBinary = _pictureService.ValidatePicture(pictureBinary); - pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); + if (customer != null) + break; + } - if (pictureBinary != null && pictureBinary.Length > 0) + if (customer == null) + { + if (context.UpdateOnly) { - if ((picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false)) != null) - { - _pictureRepository.Context.SaveChanges(); - - _genericAttributeService.SaveAttribute(row.Entity.Id, SystemCustomerAttributeNames.AvatarPictureId, _attributeKeyGroup, picture.Id.ToString()); - } + ++context.Result.SkippedRecords; + continue; } - else + + customer = new Customer { - context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "AvatarPictureUrl"); - } + CustomerGuid = new Guid(), + AffiliateId = 0, + Active = true + }; + } + else + { + _customerRepository.Context.LoadCollection(customer, (Customer x) => x.CustomerRoles); + } + + var isGuest = row.GetDataValue("IsGuest"); + var isRegistered = row.GetDataValue("IsRegistered"); + var isAdmin = row.GetDataValue("IsAdministrator"); + var isForumModerator = row.GetDataValue("IsForumModerator"); + var affiliateId = row.GetDataValue("AffiliateId"); + + row.Initialize(customer, email ?? id.ToString()); + + row.SetProperty(context.Result, (x) => x.CustomerGuid); + row.SetProperty(context.Result, (x) => x.Username); + row.SetProperty(context.Result, (x) => x.Email); + + if (email.HasValue() && currentCustomer.Email.IsCaseInsensitiveEqual(email)) + { + context.Result.AddInfo("Security. Ignored password of current customer (who started this import).", row.GetRowInfo(), "Password"); + } + else + { + row.SetProperty(context.Result, (x) => x.Password); + row.SetProperty(context.Result, (x) => x.PasswordFormatId); + row.SetProperty(context.Result, (x) => x.PasswordSalt); + } + + row.SetProperty(context.Result, (x) => x.AdminComment); + row.SetProperty(context.Result, (x) => x.IsTaxExempt); + row.SetProperty(context.Result, (x) => x.Active); + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); + row.SetProperty(context.Result, (x) => x.LastActivityDateUtc, UtcNow); + + if (affiliateId > 0 && allAffiliateIds.Contains(affiliateId)) + { + customer.AffiliateId = affiliateId; + } + + if (isAdmin) + { + context.Result.AddInfo("Security. Ignored administrator role.", row.GetRowInfo(), "IsAdministrator"); + } + + UpsertRole(row, guestRole, isGuest); + UpsertRole(row, registeredRole, isRegistered); + UpsertRole(row, forumModeratorRole, isForumModerator); + + if (row.IsTransient) + { + _customerRepository.Insert(customer); + lastInserted = customer; + } + else + { + _customerRepository.Update(customer); + lastUpdated = customer; } } - else + + var num = _customerRepository.Context.SaveChanges(); + + if (lastInserted != null) { - context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "AvatarPictureUrl"); + _services.EventPublisher.EntityInserted(lastInserted); } + + if (lastUpdated != null) + { + _services.EventPublisher.EntityUpdated(lastUpdated); + } + + return num; + } + + protected virtual int ProcessAddresses( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary allCountries, + Dictionary, int> allStateProvinces) + { + foreach (var row in batch) + { + ImportAddress("BillingAddress.", row, context, allCountries, allStateProvinces); + ImportAddress("ShippingAddress.", row, context, allCountries, allStateProvinces); + } + + return _services.DbContext.SaveChanges(); } private void ImportAddress( string fieldPrefix, ImportRow row, + ImportExecuteContext context, Dictionary allCountries, Dictionary, int> allStateProvinces) { - // last name is mandatory for an address to be imported - var lastName = row.GetDataValue(fieldPrefix + "LastName"); - if (lastName.IsEmpty()) + // last name is mandatory for an address to be imported or updated + if (!row.HasDataValue(fieldPrefix + "LastName")) return; - var countryId = CountryCodeToId(allCountries, row.GetDataValue(fieldPrefix + "CountryCode")); - var stateId = StateAbbreviationToId(allStateProvinces, countryId, row.GetDataValue(fieldPrefix + "StateAbbreviation")); - - var importAddress = new Address + var address = new Address { - FirstName = row.GetDataValue(fieldPrefix + "FirstName"), - LastName = lastName, - Email = row.GetDataValue(fieldPrefix + "Email"), - Company = row.GetDataValue(fieldPrefix + "Company"), - City = row.GetDataValue(fieldPrefix + "City"), - Address1 = row.GetDataValue(fieldPrefix + "Address1"), - Address2 = row.GetDataValue(fieldPrefix + "Address2"), - ZipPostalCode = row.GetDataValue(fieldPrefix + "ZipPostalCode"), - PhoneNumber = row.GetDataValue(fieldPrefix + "PhoneNumber"), - FaxNumber = row.GetDataValue(fieldPrefix + "FaxNumber"), - CreatedOnUtc = UtcNow, - CountryId = countryId, - StateProvinceId = stateId + CreatedOnUtc = UtcNow }; - var appliedAddress = row.Entity.Addresses - .ToList() - .FindAddress(importAddress); + var childRow = new ImportRow
(row.Segmenter, row.DataRow, row.Position); + childRow.Initialize(address, row.EntityDisplayName); + + childRow.SetProperty(context.Result, fieldPrefix + "LastName", x => x.LastName); + childRow.SetProperty(context.Result, fieldPrefix + "FirstName", x => x.FirstName); + childRow.SetProperty(context.Result, fieldPrefix + "Email", x => x.Email); + childRow.SetProperty(context.Result, fieldPrefix + "Company", x => x.Company); + childRow.SetProperty(context.Result, fieldPrefix + "City", x => x.City); + childRow.SetProperty(context.Result, fieldPrefix + "Address1", x => x.Address1); + childRow.SetProperty(context.Result, fieldPrefix + "Address2", x => x.Address2); + childRow.SetProperty(context.Result, fieldPrefix + "ZipPostalCode", x => x.ZipPostalCode); + childRow.SetProperty(context.Result, fieldPrefix + "PhoneNumber", x => x.PhoneNumber); + childRow.SetProperty(context.Result, fieldPrefix + "FaxNumber", x => x.FaxNumber); + + childRow.SetProperty(context.Result, fieldPrefix + "CountryId", x => x.CountryId); + if (childRow.Entity.CountryId == null) + { + // try with country code + childRow.SetProperty(context.Result, fieldPrefix + "CountryCode", x => x.CountryId, converter: (val, ci) => CountryCodeToId(allCountries, val.ToString())); + } + + var countryId = childRow.Entity.CountryId; + + if (countryId.HasValue) + { + childRow.SetProperty(context.Result, fieldPrefix + "StateProvinceId", x => x.StateProvinceId); + if (childRow.Entity.StateProvinceId == null) + { + // try with state abbreviation + childRow.SetProperty(context.Result, fieldPrefix + "StateAbbreviation", x => x.StateProvinceId, converter: (val, ci) => StateAbbreviationToId(allStateProvinces, countryId, val.ToString())); + } + } + + if (!childRow.IsDirty) + { + // Not one single property could be set. Get out! + return; + } + + var appliedAddress = row.Entity.Addresses.FindAddress(address); if (appliedAddress == null) { - appliedAddress = importAddress; + appliedAddress = address; row.Entity.Addresses.Add(appliedAddress); } - if (fieldPrefix.IsCaseInsensitiveEqual("BillTo")) + if (fieldPrefix == "BillingAddress.") { row.Entity.BillingAddress = appliedAddress; } - else if (fieldPrefix.IsCaseInsensitiveEqual("ShipTo")) + else if (fieldPrefix == "ShippingAddress.") { row.Entity.ShippingAddress = appliedAddress; } - _customerService.UpdateCustomer(row.Entity); - - _services.DbContext.SaveChanges(); + _customerRepository.Update(row.Entity); } - private void ProcessGenericAttributes(IImportExecuteContext context, - ImportRow[] batch, + protected virtual int ProcessGenericAttributes( + ImportExecuteContext context, + IEnumerable> batch, Dictionary allCountries, Dictionary, int> allStateProvinces, - List allCustomerNumbers) + HashSet allCustomerNumbers) { foreach (var row in batch) { @@ -247,7 +438,7 @@ private void ProcessGenericAttributes(IImportExecuteContext context, SaveAttribute(row, SystemCustomerAttributeNames.Gender); if (_customerSettings.DateOfBirthEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.DateOfBirth); + SaveAttribute(row, SystemCustomerAttributeNames.DateOfBirth); if (_customerSettings.CompanyEnabled) SaveAttribute(row, SystemCustomerAttributeNames.Company); @@ -265,10 +456,10 @@ private void ProcessGenericAttributes(IImportExecuteContext context, SaveAttribute(row, SystemCustomerAttributeNames.City); if (_customerSettings.CountryEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.CountryId); + SaveAttribute(row, SystemCustomerAttributeNames.CountryId); if (_customerSettings.CountryEnabled && _customerSettings.StateProvinceEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId); + SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId); if (_customerSettings.PhoneEnabled) SaveAttribute(row, SystemCustomerAttributeNames.Phone); @@ -277,7 +468,7 @@ private void ProcessGenericAttributes(IImportExecuteContext context, SaveAttribute(row, SystemCustomerAttributeNames.Fax); if (_forumSettings.ForumsEnabled) - SaveAttribute(row, SystemCustomerAttributeNames.ForumPostCount); + SaveAttribute(row, SystemCustomerAttributeNames.ForumPostCount); if (_forumSettings.SignaturesEnabled) SaveAttribute(row, SystemCustomerAttributeNames.Signature); @@ -287,12 +478,12 @@ private void ProcessGenericAttributes(IImportExecuteContext context, if (countryId.HasValue) { - _genericAttributeService.SaveAttribute(row.Entity.Id, SystemCustomerAttributeNames.CountryId, _attributeKeyGroup, countryId.Value); + SaveAttribute(row, SystemCustomerAttributeNames.CountryId, countryId.Value); } if (stateId.HasValue) { - _genericAttributeService.SaveAttribute(row.Entity.Id, SystemCustomerAttributeNames.StateProvinceId, _attributeKeyGroup, stateId.Value); + SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId, stateId.Value); } string customerNumber = null; @@ -302,164 +493,79 @@ private void ProcessGenericAttributes(IImportExecuteContext context, else customerNumber = row.GetDataValue("CustomerNumber"); - if (customerNumber.IsEmpty() || !allCustomerNumbers.Any(x => x.IsCaseInsensitiveEqual(customerNumber))) + if (customerNumber.IsEmpty() || !allCustomerNumbers.Contains(customerNumber)) { - _genericAttributeService.SaveAttribute(row.Entity.Id, SystemCustomerAttributeNames.CustomerNumber, _attributeKeyGroup, customerNumber); + SaveAttribute(row, SystemCustomerAttributeNames.CustomerNumber, customerNumber); if (!customerNumber.IsEmpty()) allCustomerNumbers.Add(customerNumber); } - - if (_customerSettings.AllowCustomersToUploadAvatars) - { - ImportAvatar(context, row); - } - - _services.DbContext.SaveChanges(); } + + return _services.DbContext.SaveChanges(); } - private int ProcessCustomers(IImportExecuteContext context, - ImportRow[] batch, - List allAffiliateIds, - IList allCustomerRoles) + protected virtual int ProcessAvatars( + ImportExecuteContext context, + IEnumerable> batch) { - _customerRepository.AutoCommitEnabled = true; - - Customer lastInserted = null; - Customer lastUpdated = null; - var currentCustomer = _services.WorkContext.CurrentCustomer; - - var guestRole = allCustomerRoles.FirstOrDefault(x => x.SystemName == SystemCustomerRoleNames.Guests); - var registeredRole = allCustomerRoles.FirstOrDefault(x => x.SystemName == SystemCustomerRoleNames.Registered); - var forumModeratorRole = allCustomerRoles.FirstOrDefault(x => x.SystemName == SystemCustomerRoleNames.ForumModerators); - foreach (var row in batch) { - if (row.GetDataValue("IsSystemAccount")) - { - ++context.Result.SkippedRecords; - context.Result.AddInfo("Skipped system account.", row.GetRowInfo(), "IsSystemAccount"); + var urlOrPath = row.GetDataValue("AvatarPictureUrl"); + if (urlOrPath.IsEmpty()) continue; - } - - Customer customer = null; - var id = row.GetDataValue("Id"); - var email = row.GetDataValue("Email"); - - foreach (var keyName in context.KeyFieldNames) - { - switch (keyName) - { - case "Id": - customer = _customerService.GetCustomerById(id); - break; - case "CustomerGuid": - var guid = row.GetDataValue("CustomerGuid"); - if (guid.HasValue()) - customer = _customerService.GetCustomerByGuid(new Guid(guid)); - break; - case "Email": - customer = _customerService.GetCustomerByEmail(email); - break; - case "Username": - customer = _customerService.GetCustomerByUsername(row.GetDataValue("Username")); - break; - } - if (customer != null) - break; - } + Picture picture = null; + var equalPictureId = 0; + var currentPictures = new List(); + var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); - if (customer == null) - { - if (context.UpdateOnly) - { - ++context.Result.SkippedRecords; - continue; - } + var image = CreateDownloadImage(urlOrPath, seoName, 1); + if (image == null) + continue; - customer = new Customer - { - CustomerGuid = new Guid(), - AffiliateId = 0, - Active = true - }; - } - else + if (image.Url.HasValue() && !image.Success.HasValue) { - _customerRepository.Context.LoadCollection(customer, (Customer x) => x.CustomerRoles); + AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, new FileDownloadManagerItem[] { image })); } - var isGuest = row.GetDataValue("IsGuest"); - var isRegistered = row.GetDataValue("IsRegistered"); - var isAdmin = row.GetDataValue("IsAdministrator"); - var isForumModerator = row.GetDataValue("IsForumModerator"); - var affiliateId = row.GetDataValue("AffiliateId"); - - row.Initialize(customer, email.HasValue() ? email : id.ToString()); - - row.SetProperty(context.Result, customer, (x) => x.CustomerGuid); - row.SetProperty(context.Result, customer, (x) => x.Username); - row.SetProperty(context.Result, customer, (x) => x.Email); - - if (email.HasValue() && currentCustomer.Email.IsCaseInsensitiveEqual(email)) + if ((image.Success ?? false) && File.Exists(image.Path)) { - context.Result.AddInfo("Security. Ignored password of current customer (who started this import).", row.GetRowInfo(), "Password"); - } - else - { - row.SetProperty(context.Result, customer, (x) => x.Password); - row.SetProperty(context.Result, customer, (x) => x.PasswordFormatId); - row.SetProperty(context.Result, customer, (x) => x.PasswordSalt); - } - - row.SetProperty(context.Result, customer, (x) => x.AdminComment); - row.SetProperty(context.Result, customer, (x) => x.IsTaxExempt); - row.SetProperty(context.Result, customer, (x) => x.Active); - row.SetProperty(context.Result, customer, (x) => x.IsSystemAccount); - row.SetProperty(context.Result, customer, (x) => x.SystemName); - row.SetProperty(context.Result, customer, (x) => x.LastIpAddress); - row.SetProperty(context.Result, customer, (x) => x.LastLoginDateUtc); - row.SetProperty(context.Result, customer, (x) => x.LastActivityDateUtc); - - row.SetProperty(context.Result, customer, (x) => x.CreatedOnUtc, UtcNow); - row.SetProperty(context.Result, customer, (x) => x.LastActivityDateUtc, UtcNow); + Succeeded(image); + var pictureBinary = File.ReadAllBytes(image.Path); - if (affiliateId > 0 && allAffiliateIds.Contains(affiliateId)) - { - customer.AffiliateId = affiliateId; - } + if (pictureBinary != null && pictureBinary.Length > 0) + { + var currentPictureId = row.Entity.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId); + if (currentPictureId != 0 && (picture = _pictureRepository.GetById(currentPictureId)) != null) + { + currentPictures.Add(picture); + } - if (isAdmin) - { - context.Result.AddInfo("Security. Ignored administrator role.", row.GetRowInfo(), "IsAdministrator"); - } + pictureBinary = _pictureService.ValidatePicture(pictureBinary); + pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); - UpsertRole(row, guestRole, isGuest); - UpsertRole(row, registeredRole, isRegistered); - UpsertRole(row, forumModeratorRole, isForumModerator); - - if (row.IsTransient) - { - _customerRepository.Insert(customer); - lastInserted = customer; + if (pictureBinary != null && pictureBinary.Length > 0) + { + if ((picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false)) != null) + { + _pictureRepository.Context.SaveChanges(); + SaveAttribute(row, SystemCustomerAttributeNames.AvatarPictureId, picture.Id); + } + } + else + { + context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "AvatarPictureUrl"); + } + } } else { - _customerRepository.Update(customer); - lastUpdated = customer; + context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "AvatarPictureUrl"); } } - var num = _customerRepository.Context.SaveChanges(); - - if (lastInserted != null) - _services.EventPublisher.EntityInserted(lastInserted); - if (lastUpdated != null) - _services.EventPublisher.EntityUpdated(lastUpdated); - - return num; + return _services.DbContext.SaveChanges(); } public static string[] SupportedKeyFields @@ -478,104 +584,69 @@ public static string[] DefaultKeyFields } } - protected override void Import(IImportExecuteContext context) + private int? CountryCodeToId(Dictionary allCountries, string code) { - var customer = _services.WorkContext.CurrentCustomer; - var allowManagingCustomerRoles = _services.Permissions.Authorize(StandardPermissionProvider.ManageCustomerRoles, customer); - - var allCustomerRoles = _customerService.GetAllCustomerRoles(true); - - var allAffiliateIds = _affiliateService.GetAllAffiliates(true) - .Select(x => x.Id) - .ToList(); - - var allCountries = new Dictionary(); - foreach (var country in _countryService.GetAllCountries(true)) + int countryId; + if (code.HasValue() && allCountries.TryGetValue(code, out countryId) && countryId != 0) { - if (!allCountries.ContainsKey(country.TwoLetterIsoCode)) - allCountries.Add(country.TwoLetterIsoCode, country.Id); - - if (!allCountries.ContainsKey(country.ThreeLetterIsoCode)) - allCountries.Add(country.ThreeLetterIsoCode, country.Id); + return countryId; } - var allStateProvinces = _stateProvinceService.GetAllStateProvinces(true) - .ToDictionarySafe(x => new Tuple(x.CountryId, x.Abbreviation), x => x.Id); - - var allCustomerNumbers = _genericAttributeService.GetAttributes(SystemCustomerAttributeNames.CustomerNumber, _attributeKeyGroup) - .Select(x => x.Value) - .ToList(); + return null; + } - using (var scope = new DbContextScope(ctx: _services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + private int? StateAbbreviationToId(Dictionary, int> allStateProvinces, int? countryId, string abbreviation) + { + if (countryId.HasValue && abbreviation.HasValue()) { - var segmenter = context.GetSegmenter(); - - Init(context, _dataExchangeSettings); - - context.Result.TotalRecords = segmenter.TotalRows; + var key = Tuple.Create(countryId.Value, abbreviation); - while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + int stateId; + if (allStateProvinces.TryGetValue(key, out stateId) && stateId != 0) { - var batch = segmenter.CurrentBatch; + return stateId; + } + } - _customerRepository.Context.DetachAll(false); + return null; + } - context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + private void SaveAttribute(ImportRow row, string key) + { + SaveAttribute(row, key, row.GetDataValue(key)); + } - try - { - ProcessCustomers(context, batch, allAffiliateIds, allCustomerRoles); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCustomers"); - } + private void SaveAttribute(ImportRow row, string key) + { - // reduce batch to saved (valid) records. - // No need to perform import operations on errored records. - batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); + SaveAttribute(row, key, row.GetDataValue(key)); + } - // update result object - context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); - context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + private void SaveAttribute(ImportRow row, string key, TPropType value) + { + if (row.IsTransient) + return; - try - { - _services.DbContext.AutoDetectChangesEnabled = true; + if (row.IsNew || value != null) + { + _genericAttributeService.SaveAttribute(row.Entity.Id, key, _attributeKeyGroup, value); + } + } - ProcessGenericAttributes(context, batch, allCountries, allStateProvinces, allCustomerNumbers); - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessGenericAttributes"); - } - finally - { - _services.DbContext.AutoDetectChangesEnabled = false; - } + private void UpsertRole(ImportRow row, CustomerRole role, bool value) + { + if (role == null) + return; - if (segmenter.HasColumn("BillToLastName") || segmenter.HasColumn("ShipToLastName")) - { - try - { - _services.DbContext.AutoDetectChangesEnabled = true; + var hasRole = row.Entity.CustomerRoles.Any(x => x.SystemName == role.SystemName); - foreach (var row in batch) - { - ImportAddress("BillTo", row, allCountries, allStateProvinces); - ImportAddress("ShipTo", row, allCountries, allStateProvinces); - } - } - catch (Exception exception) - { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessAddresses"); - } - finally - { - _services.DbContext.AutoDetectChangesEnabled = false; - } - } - } + if (value && !hasRole) + { + row.Entity.CustomerRoles.Add(role); + } + else if (!value && hasRole) + { + row.Entity.CustomerRoles.Remove(role); } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs index 83eee20228..8bb9942052 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs @@ -51,7 +51,7 @@ public CsvConfiguration Configuration /// to complete writing of the current row. ///
/// The fields to write. - public virtual void WriteFields(string[] fields) + public virtual void WriteFields(IEnumerable fields) { Guard.ArgumentNotNull(() => fields); fields.Each(x => WriteField(x)); @@ -69,7 +69,7 @@ public virtual void WriteFields(string[] fields) ///
/// The fields to write. /// True to quote the fields, otherwise false. - public virtual void WriteFields(string[] fields, bool shouldQuote) + public virtual void WriteFields(IEnumerable fields, bool shouldQuote) { Guard.ArgumentNotNull(() => fields); fields.Each(x => WriteField(x, shouldQuote)); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Deployment/FileSystemFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Deployment/FileSystemFilePublisher.cs deleted file mode 100644 index fb9750120f..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/FileSystemFilePublisher.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.IO; -using System.Web; -using SmartStore.Core.Domain; -using SmartStore.Core.Logging; -using SmartStore.Utilities; - -namespace SmartStore.Services.DataExchange.Export.Deployment -{ - public class FileSystemFilePublisher : IFilePublisher - { - public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) - { - string folderDestination = null; - - if (deployment.IsPublic) - { - folderDestination = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder); - } - else if (deployment.FileSystemPath.IsEmpty()) - { - return; - } - else if (deployment.FileSystemPath.StartsWith("/") || deployment.FileSystemPath.StartsWith("\\") || !Path.IsPathRooted(deployment.FileSystemPath)) - { - folderDestination = CommonHelper.MapPath(deployment.FileSystemPath); - } - else - { - folderDestination = deployment.FileSystemPath; - } - - if (!System.IO.Directory.Exists(folderDestination)) - { - System.IO.Directory.CreateDirectory(folderDestination); - } - - if (deployment.CreateZip) - { - var path = Path.Combine(folderDestination, deployment.Profile.FolderName + ".zip"); - - if (FileSystemHelper.Copy(context.ZipPath, path)) - context.Log.Information("Copied ZIP archive " + path); - } - else - { - FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(folderDestination)); - - context.Log.Information("Copied export data files to " + folderDestination); - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Deployment/IFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Deployment/IFilePublisher.cs deleted file mode 100644 index 25ffddcaa7..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/IFilePublisher.cs +++ /dev/null @@ -1,22 +0,0 @@ -using SmartStore.Core.Domain; -using SmartStore.Core.Logging; - -namespace SmartStore.Services.DataExchange.Export.Deployment -{ - public interface IFilePublisher - { - void Publish(ExportDeploymentContext context, ExportDeployment deployment); - } - - - public class ExportDeploymentContext - { - public ILogger Log { get; set; } - - public string[] DeploymentFiles { get; set; } - - public string FolderContent { get; set; } - - public string ZipPath { get; set; } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs b/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs index d5eb02d5a9..d46879193c 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs @@ -6,8 +6,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; using OfficeOpenXml; namespace SmartStore.Services.DataExchange.Excel diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs index d7e0d9fa6a..c9d3fa26f2 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs @@ -37,17 +37,11 @@ public bool Succeeded [XmlIgnore] public string FileFolder { get; set; } - /// - /// Suggested download file name - /// - [XmlIgnore] - public string DownloadFileName { get; set; } - [Serializable] public class ExportFileInfo { /// - /// Store identifier + /// Store identifier, can be 0. /// public int StoreId { get; set; } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index 09d444655a..e6b182c270 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -61,7 +61,8 @@ public partial class DataExporter : IDataExporter private readonly Lazy _productAttributeParser; private readonly Lazy _productAttributeService; private readonly Lazy _productTemplateService; - private readonly Lazy _productService; + private readonly Lazy _categoryTemplateService; + private readonly Lazy _productService; private readonly Lazy _orderService; private readonly Lazy _manufacturerService; private readonly ICustomerService _customerService; @@ -101,6 +102,7 @@ public DataExporter( Lazy productAttributeParser, Lazy productAttributeService, Lazy productTemplateService, + Lazy categoryTemplateService, Lazy productService, Lazy orderService, Lazy manufacturerService, @@ -138,6 +140,7 @@ public DataExporter( _productAttributeParser = productAttributeParser; _productAttributeService = productAttributeService; _productTemplateService = productTemplateService; + _categoryTemplateService = categoryTemplateService; _productService = productService; _orderService = orderService; _manufacturerService = manufacturerService; @@ -182,9 +185,7 @@ private void SetProgress(DataExporterContext ctx, int loadedRecords) totalRecords = ctx.Request.Profile.Limit; ctx.RecordCount = Math.Min(ctx.RecordCount + loadedRecords, totalRecords); - var msg = ctx.ProgressInfo.FormatInvariant(ctx.RecordCount, totalRecords); - ctx.Request.ProgressValueSetter.Invoke(ctx.RecordCount, totalRecords, msg); } } @@ -275,17 +276,14 @@ private void DetachAllEntitiesAndClear(DataExporterContext ctx) private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, int pageIndex = 0) { var offset = Math.Max(ctx.Request.Profile.Offset, 0) + (pageIndex * PageSize); - var limit = (ctx.IsPreview ? PageSize : Math.Max(ctx.Request.Profile.Limit, 0)); - var recordsPerSegment = (ctx.IsPreview ? 0 : Math.Max(ctx.Request.Profile.BatchSize, 0)); - var totalCount = Math.Max(ctx.Request.Profile.Offset, 0) + ctx.RecordsPerStore.First(x => x.Key == ctx.Store.Id).Value; switch (ctx.Request.Provider.Value.EntityType) { case ExportEntityType.Product: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetProducts(ctx, skip), entities => @@ -295,7 +293,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in x => _productAttributeService.Value.GetProductVariantAttributesByProductIds(x, null), x => _productAttributeService.Value.GetProductVariantAttributeCombinations(x), x => _productService.Value.GetTierPricesByProductIds(x, (ctx.Projection.CurrencyId ?? 0) != 0 ? ctx.ContextCustomer : null, ctx.Store.Id), - x => _categoryService.Value.GetProductCategoriesByProductIds(x), + x => _categoryService.Value.GetProductCategoriesByProductIds(x, null, true), x => _manufacturerService.Value.GetProductManufacturersByProductIds(x), x => _productService.Value.GetProductPicturesByProductIds(x), x => _productService.Value.GetProductTagsByProductIds(x), @@ -310,7 +308,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.Order: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetOrders(ctx, skip), entities => @@ -329,7 +327,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.Manufacturer: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetManufacturers(ctx, skip), entities => @@ -345,7 +343,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.Category: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetCategories(ctx, skip), entities => @@ -361,7 +359,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.Customer: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetCustomers(ctx, skip), entities => @@ -376,7 +374,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.NewsLetterSubscription: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetNewsLetterSubscriptions(ctx, skip), null, @@ -386,11 +384,11 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; default: - ctx.ExecuteContext.Segmenter = null; + ctx.ExecuteContext.DataSegmenter = null; break; } - return ctx.ExecuteContext.Segmenter as IExportDataSegmenterProvider; + return ctx.ExecuteContext.DataSegmenter as IExportDataSegmenterProvider; } private bool CallProvider(DataExporterContext ctx, string streamId, string method, string path) @@ -416,7 +414,9 @@ private bool CallProvider(DataExporterContext ctx, string streamId, string metho if (ctx.IsFileBasedExport && path.HasValue() && ctx.ExecuteContext.DataStream.Length > 0) { if (!ctx.ExecuteContext.DataStream.CanSeek) + { ctx.Log.Warning("Data stream seems to be closed!"); + } ctx.ExecuteContext.DataStream.Seek(0, SeekOrigin.Begin); @@ -424,7 +424,6 @@ private bool CallProvider(DataExporterContext ctx, string streamId, string metho using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) { ctx.Log.Information("Creating file {0}.".FormatInvariant(path)); - ctx.ExecuteContext.DataStream.CopyTo(fileStream); } } @@ -443,61 +442,90 @@ private bool CallProvider(DataExporterContext ctx, string streamId, string metho ctx.ExecuteContext.DataStream.Dispose(); ctx.ExecuteContext.DataStream = null; } + + if (ctx.ExecuteContext.Abort == DataExchangeAbortion.Hard && ctx.IsFileBasedExport && path.HasValue()) + { + FileSystemHelper.Delete(path); + } } return (ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard); } - private void Deploy(DataExporterContext ctx, string zipPath) + private bool Deploy(DataExporterContext ctx, string zipPath) { - var allFiles = System.IO.Directory.GetFiles(ctx.FolderContent, "*.*", SearchOption.AllDirectories); + var allSucceeded = true; + var deployments = ctx.Request.Profile.Deployments.OrderBy(x => x.DeploymentTypeId).Where(x => x.Enabled); + + if (deployments.Count() == 0) + return false; var context = new ExportDeploymentContext { + T = T, Log = ctx.Log, FolderContent = ctx.FolderContent, - ZipPath = zipPath - }; + ZipPath = zipPath, + CreateZipArchive = ctx.Request.Profile.CreateZipArchive + }; - foreach (var deployment in ctx.Request.Profile.Deployments.OrderBy(x => x.DeploymentTypeId).Where(x => x.Enabled)) + foreach (var deployment in deployments) { - if (deployment.CreateZip) - context.DeploymentFiles = new string[] { zipPath }; - else - context.DeploymentFiles = allFiles; + IFilePublisher publisher = null; - try + context.Result = new DataDeploymentResult { - IFilePublisher publisher = null; + LastExecutionUtc = DateTime.UtcNow + }; - if (deployment.DeploymentType == ExportDeploymentType.Email) - { - publisher = new EmailFilePublisher(_emailAccountService.Value, _queuedEmailService.Value); - } - else if (deployment.DeploymentType == ExportDeploymentType.FileSystem) - { - publisher = new FileSystemFilePublisher(); - } - else if (deployment.DeploymentType == ExportDeploymentType.Ftp) - { - publisher = new FtpFilePublisher(); - } - else if (deployment.DeploymentType == ExportDeploymentType.Http) + try + { + switch (deployment.DeploymentType) { - publisher = new HttpFilePublisher(); + case ExportDeploymentType.Email: + publisher = new EmailFilePublisher(_emailAccountService.Value, _queuedEmailService.Value); + break; + case ExportDeploymentType.FileSystem: + publisher = new FileSystemFilePublisher(); + break; + case ExportDeploymentType.Ftp: + publisher = new FtpFilePublisher(); + break; + case ExportDeploymentType.Http: + publisher = new HttpFilePublisher(); + break; + case ExportDeploymentType.PublicFolder: + publisher = new PublicFolderPublisher(); + break; } if (publisher != null) { publisher.Publish(context, deployment); + + if (!context.Result.Succeeded) + allSucceeded = false; } } catch (Exception exception) { + allSucceeded = false; + + if (context.Result != null) + { + context.Result.LastError = exception.ToAllMessages(); + } + ctx.Log.Error("Deployment \"{0}\" of type {1} failed: {2}".FormatInvariant( deployment.Name, deployment.DeploymentType.ToString(), exception.Message), exception); } + + deployment.ResultInfo = XmlHelper.Serialize(context.Result); + + _exportProfileService.Value.UpdateExportDeployment(deployment); } + + return allSucceeded; } private void SendCompletionEmail(DataExporterContext ctx, string zipPath) @@ -507,7 +535,7 @@ private void SendCompletionEmail(DataExporterContext ctx, string zipPath) if (emailAccount == null) emailAccount = _emailAccountService.Value.GetDefaultEmailAccount(); - var downloadUrl = "{0}Admin/Export/DownloadExportFile/{1}?name=".FormatInvariant(_services.WebHelper.GetStoreLocation(false), ctx.Request.Profile.Id); + var downloadUrl = "{0}Admin/Export/DownloadExportFile/{1}?name=".FormatInvariant(_services.WebHelper.GetStoreLocation(ctx.Store.SslEnabled), ctx.Request.Profile.Id); var languageId = ctx.Projection.LanguageId ?? 0; var smtpContext = new SmtpContext(emailAccount); @@ -525,15 +553,15 @@ private void SendCompletionEmail(DataExporterContext ctx, string zipPath) if (ctx.IsFileBasedExport && File.Exists(zipPath)) { var fileName = Path.GetFileName(zipPath); - body.AppendFormat("

{1}

", downloadUrl + HttpUtility.UrlDecode(fileName), fileName); + body.AppendFormat("

{2}

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

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

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

"); } @@ -543,9 +571,6 @@ private void SendCompletionEmail(DataExporterContext ctx, string zipPath) if (ctx.Request.Profile.CompletedEmailAddresses.HasValue()) message.To.AddRange(ctx.Request.Profile.CompletedEmailAddresses.SplitSafe(",").Where(x => x.IsEmail()).Select(x => new EmailAddress(x))); - if (message.To.Count == 0 && _contactDataSettings.Value.WebmasterEmailAddress.HasValue()) - message.To.Add(new EmailAddress(_contactDataSettings.Value.WebmasterEmailAddress)); - if (message.To.Count == 0 && _contactDataSettings.Value.CompanyEmailAddress.HasValue()) message.To.Add(new EmailAddress(_contactDataSettings.Value.CompanyEmailAddress)); @@ -661,7 +686,7 @@ private List GetProducts(DataExporterContext ctx, int skip) OrderBy = ProductSortingEnum.Position, PageSize = int.MaxValue, StoreId = (ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId), - VisibleIndividuallyOnly = false, + VisibleIndividuallyOnly = true, ParentGroupedProductId = product.Id }; @@ -739,7 +764,7 @@ private IQueryable GetManufacturerQuery(DataExporterContext ctx, i var query = _manufacturerService.Value.GetManufacturers(showHidden, storeId); - if (ctx.Request.EntitiesToExport.Count > 0) + if (ctx.Request.EntitiesToExport.Any()) query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); query = query.OrderBy(x => x.DisplayOrder); @@ -1029,7 +1054,10 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) logHead.Append("Export profile:\t\t" + ctx.Request.Profile.Name); logHead.AppendLine(ctx.Request.Profile.Id == 0 ? " (volatile)" : " (Id {0})".FormatInvariant(ctx.Request.Profile.Id)); - logHead.AppendLine("Export provider:\t{0} ({1})".FormatInvariant(ctx.Request.Provider.Metadata.FriendlyName, ctx.Request.Profile.ProviderSystemName)); + if (ctx.Request.Provider.Metadata.FriendlyName.HasValue()) + logHead.AppendLine("Export provider:\t{0} ({1})".FormatInvariant(ctx.Request.Provider.Metadata.FriendlyName, ctx.Request.Profile.ProviderSystemName)); + else + logHead.AppendLine("Export provider:\t{0}".FormatInvariant(ctx.Request.Profile.ProviderSystemName)); var plugin = ctx.Request.Provider.Metadata.PluginDescriptor; logHead.Append("Plugin:\t\t\t"); @@ -1037,8 +1065,12 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) logHead.AppendLine("Entity:\t\t\t" + ctx.Request.Provider.Value.EntityType.ToString()); - var storeInfo = (ctx.Request.Profile.PerStore ? "{0} (Id {1})".FormatInvariant(ctx.Store.Name, ctx.Store.Id) : "All stores"); - logHead.AppendLine("Store:\t\t\t" + storeInfo); + try + { + var uri = new Uri(store.Url); + logHead.AppendLine("Store:\t\t\t{0} (Id {1})".FormatInvariant(uri.DnsSafeHost.NaIfEmpty(), ctx.Store.Id)); + } + catch { } var customer = _services.WorkContext.CurrentCustomer; logHead.Append("Executed by:\t\t" + (customer.Email.HasValue() ? customer.Email : customer.SystemName)); @@ -1047,12 +1079,11 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) } ctx.ExecuteContext.Store = ToDynamic(ctx, ctx.Store); - ctx.ExecuteContext.MaxFileNameLength = dataExchangeSettings.MaxFileNameLength; - ctx.ExecuteContext.HasPublicDeployment = ctx.Request.Profile.Deployments.Any(x => x.IsPublic && x.DeploymentType == ExportDeploymentType.FileSystem); - - ctx.ExecuteContext.PublicFolderPath = (ctx.ExecuteContext.HasPublicDeployment ? Path.Combine(HttpRuntime.AppDomainAppPath, PublicFolder) : null); + var publicDeployment = ctx.Request.Profile.Deployments.FirstOrDefault(x => x.DeploymentType == ExportDeploymentType.PublicFolder); + ctx.ExecuteContext.HasPublicDeployment = (publicDeployment != null); + ctx.ExecuteContext.PublicFolderPath = publicDeployment.GetDeploymentFolder(true); var fileExtension = (ctx.Request.Provider.Value.FileExtension.HasValue() ? ctx.Request.Provider.Value.FileExtension.ToLower().EnsureStartsWith(".") : ""); @@ -1082,17 +1113,13 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) ctx.ExecuteContext.FileName = resolvedPattern + fileExtension; path = Path.Combine(ctx.ExecuteContext.Folder, ctx.ExecuteContext.FileName); - - if (ctx.ExecuteContext.HasPublicDeployment) - ctx.ExecuteContext.PublicFileUrl = ctx.Store.Url.EnsureEndsWith("/") + PublicFolder.EnsureEndsWith("/") + ctx.ExecuteContext.FileName; } if (CallProvider(ctx, null, "Execute", path)) { ctx.Log.Information("Provider reports {0} successfully exported record(s).".FormatInvariant(ctx.ExecuteContext.RecordsSucceeded)); - // create info for deployment list in profile edit - if (ctx.IsFileBasedExport) + if (ctx.IsFileBasedExport && File.Exists(path)) { ctx.Result.Files.Add(new DataExportResult.ExportFileInfo { @@ -1116,17 +1143,16 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) if (ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard) { // always call OnExecuted - if (ctx.ExecuteContext.ExtraDataStreams.Count == 0) - ctx.ExecuteContext.ExtraDataStreams.Add(new ExportExtraStreams()); + if (ctx.ExecuteContext.ExtraDataUnits.Count == 0) + ctx.ExecuteContext.ExtraDataUnits.Add(new ExportDataUnit()); - ctx.ExecuteContext.ExtraDataStreams.ForEach(x => + ctx.ExecuteContext.ExtraDataUnits.ForEach(x => { var path = (x.FileName.HasValue() ? Path.Combine(ctx.ExecuteContext.Folder, x.FileName) : null); - CallProvider(ctx, x.Id, "OnExecuted", path); }); - ctx.ExecuteContext.ExtraDataStreams.Clear(); + ctx.ExecuteContext.ExtraDataUnits.Clear(); } } } @@ -1177,7 +1203,8 @@ private void ExportCoreOuter(DataExporterContext ctx) { ctx.DeliveryTimes = _deliveryTimeService.Value.GetAllDeliveryTimes().ToDictionary(x => x.Id); ctx.QuantityUnits = _quantityUnitService.Value.GetAllQuantityUnits().ToDictionary(x => x.Id); - ctx.ProductTemplates = _productTemplateService.Value.GetAllProductTemplates().ToDictionary(x => x.Id); + ctx.ProductTemplates = _productTemplateService.Value.GetAllProductTemplates().ToDictionary(x => x.Id, x => x.ViewPath); + ctx.CategoryTemplates = _categoryTemplateService.Value.GetAllCategoryTemplates().ToDictionary(x => x.Id, x => x.ViewPath); if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Product) { @@ -1214,16 +1241,23 @@ private void ExportCoreOuter(DataExporterContext ctx) { if (ctx.IsFileBasedExport) { - if (ctx.Request.Profile.CreateZipArchive || ctx.Request.Profile.Deployments.Any(x => x.Enabled && x.CreateZip)) + if (ctx.Request.Profile.CreateZipArchive) { - ZipFile.CreateFromDirectory(ctx.FolderContent, zipPath, CompressionLevel.Fastest, true); + ZipFile.CreateFromDirectory(ctx.FolderContent, zipPath, CompressionLevel.Fastest, false); } if (ctx.Request.Profile.Deployments.Any(x => x.Enabled)) { - SetProgress(ctx, T("Common.Deployment")); + SetProgress(ctx, T("Common.Publishing")); - Deploy(ctx, zipPath); + var allDeploymentsSucceeded = Deploy(ctx, zipPath); + + if (allDeploymentsSucceeded && ctx.Request.Profile.Cleanup) + { + logger.Information("Cleaning up export folder"); + + FileSystemHelper.ClearDirectory(ctx.FolderContent, false); + } } } @@ -1248,7 +1282,7 @@ private void ExportCoreOuter(DataExporterContext ctx) { if (!ctx.IsPreview && ctx.Request.Profile.Id != 0) { - ctx.Request.Profile.ResultInfo = XmlHelper.Serialize(ctx.Result); + ctx.Request.Profile.ResultInfo = XmlHelper.Serialize(ctx.Result); _exportProfileService.Value.UpdateExportProfile(ctx.Request.Profile); } @@ -1258,24 +1292,13 @@ private void ExportCoreOuter(DataExporterContext ctx) logger.ErrorsAll(exception); } - try - { - if (ctx.IsFileBasedExport && ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard && ctx.Request.Profile.Cleanup) - { - FileSystemHelper.ClearDirectory(ctx.FolderContent, false); - } - } - catch (Exception exception) - { - logger.ErrorsAll(exception); - } - DetachAllEntitiesAndClear(ctx); try { ctx.NewsletterSubscriptions.Clear(); ctx.ProductTemplates.Clear(); + ctx.CategoryTemplates.Clear(); ctx.Countries.Clear(); ctx.Languages.Clear(); ctx.QuantityUnits.Clear(); @@ -1301,7 +1324,7 @@ private void ExportCoreOuter(DataExporterContext ctx) return; // post process order entities - if (ctx.EntityIdsLoaded.Count > 0 && ctx.Request.Provider.Value.EntityType == ExportEntityType.Order && ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) + if (ctx.EntityIdsLoaded.Any() && ctx.Request.Provider.Value.EntityType == ExportEntityType.Order && ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) { using (var logger = new TraceLogger(logPath)) { @@ -1356,38 +1379,6 @@ public DataExportResult Export(DataExportRequest request, CancellationToken canc ExportCoreOuter(ctx); - if (ctx.Result != null && ctx.Result.Succeeded && ctx.Result.Files.Count > 0) - { - string prefix = null; - string suffix = null; - var extension = Path.GetExtension(ctx.Result.Files.First().FileName); - var provider = request.Provider.Value; - - if (provider.EntityType == ExportEntityType.Product) - prefix = T("Admin.Catalog.Products"); - else if (provider.EntityType == ExportEntityType.Order) - prefix = T("Admin.Orders"); - else if (provider.EntityType == ExportEntityType.Category) - prefix = T("Admin.Catalog.Categories"); - else if (provider.EntityType == ExportEntityType.Manufacturer) - prefix = T("Admin.Catalog.Manufacturers"); - else if (provider.EntityType == ExportEntityType.Customer) - prefix = T("Admin.Customers"); - else if (provider.EntityType == ExportEntityType.NewsLetterSubscription) - prefix = T("Admin.Promotions.NewsLetterSubscriptions"); - else - prefix = provider.EntityType.ToString(); - - var selectedEntityCount = (request.EntitiesToExport == null ? 0 : request.EntitiesToExport.Count); - - if (selectedEntityCount == 0) - suffix = T("Common.All"); - else - suffix = (selectedEntityCount == 1 ? request.EntitiesToExport.First().ToString() : T("Admin.Common.Selected").Text); - - ctx.Result.DownloadFileName = string.Concat(prefix, "-", suffix).ToLower().ToValidFileName() + extension; - } - cancellationToken.ThrowIfCancellationRequested(); return ctx.Result; diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs new file mode 100644 index 0000000000..7fff7a46e5 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs @@ -0,0 +1,23 @@ +using System; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + [Serializable] + public class DataDeploymentResult + { + /// + /// Whether the deployment succeeded + /// + public bool Succeeded + { + get { return LastError.IsEmpty(); } + } + + public string LastError { get; set; } + + /// + /// Last execution date + /// + public DateTime LastExecutionUtc { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Deployment/EmailFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs similarity index 88% rename from src/Libraries/SmartStore.Services/DataExchange/Deployment/EmailFilePublisher.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs index 0294229103..298d476c41 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/EmailFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs @@ -35,21 +35,21 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de { From = emailAccount.Email, FromName = emailAccount.DisplayName, - SendManually = true, + SendManually = false, To = email, Subject = deployment.EmailSubject.NaIfEmpty(), CreatedOnUtc = DateTime.UtcNow, EmailAccountId = deployment.EmailAccountId }; - foreach (string path in context.DeploymentFiles) + foreach (var path in context.GetDeploymentFiles()) { - string name = Path.GetFileName(path); + var name = Path.GetFileName(path); queuedEmail.Attachments.Add(new QueuedEmailAttachment { - StorageLocation = EmailAttachmentStorageLocation.Path, - Path = path, + StorageLocation = EmailAttachmentStorageLocation.Blob, + Data = File.ReadAllBytes(path), Name = name, MimeType = MimeTypes.MapNameToMimeType(name) }); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs new file mode 100644 index 0000000000..6bb56682e1 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs @@ -0,0 +1,22 @@ +using System.IO; +using SmartStore.Core.Domain; +using SmartStore.Core.Logging; +using SmartStore.Utilities; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + public class FileSystemFilePublisher : IFilePublisher + { + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var targetFolder = deployment.GetDeploymentFolder(true); + + if (!FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(targetFolder))) + { + context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); + } + + context.Log.Information("Copied export data files to " + targetFolder); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Deployment/FtpFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs similarity index 82% rename from src/Libraries/SmartStore.Services/DataExchange/Deployment/FtpFilePublisher.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs index 2352d7250f..7a0954a1c7 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/FtpFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs @@ -16,7 +16,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de var url = deployment.Url; var buffLength = 32768; byte[] buff = new byte[buffLength]; - var deploymentFiles = context.DeploymentFiles.ToList(); + var deploymentFiles = context.GetDeploymentFiles().ToList(); var lastIndex = (deploymentFiles.Count - 1); if (!url.StartsWith("ftp://", StringComparison.InvariantCultureIgnoreCase)) @@ -26,7 +26,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de { var fileUrl = url.EnsureEndsWith("/") + Path.GetFileName(path); - var request = (FtpWebRequest)System.Net.WebRequest.Create(fileUrl); + var request = (FtpWebRequest)WebRequest.Create(fileUrl); request.Method = WebRequestMethods.Ftp.UploadFile; request.KeepAlive = (deploymentFiles.IndexOf(path) != lastIndex); request.UseBinary = true; @@ -61,10 +61,9 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de } else { - var msg = "The FTP transfer might fail. {0} ({1}), {2}. File {3}".FormatInvariant( - response.StatusCode.ToString(), statusCode, response.StatusDescription.NaIfEmpty(), path); + context.Result.LastError = context.T("Admin.Common.FtpStatus", statusCode, response.StatusCode.ToString()); - context.Log.Error(msg); + context.Log.Error("The FTP transfer failed. FTP status {0} ({1}). File {3}".FormatInvariant(statusCode, response.StatusCode.ToString(), path)); } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Deployment/HttpFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs similarity index 73% rename from src/Libraries/SmartStore.Services/DataExchange/Deployment/HttpFilePublisher.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs index 48303bd200..4f0d9df8d8 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/HttpFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs @@ -12,7 +12,7 @@ public class HttpFilePublisher : IFilePublisher { public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) { - var succeeded = 0; + var succeededFiles = 0; var url = deployment.Url; if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) @@ -20,7 +20,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de if (deployment.HttpTransmissionType == ExportHttpTransmissionType.MultipartFormDataPost) { - var count = 0; + var countFiles = 0; ICredentials credentials = null; if (deployment.Username.HasValue()) @@ -30,24 +30,26 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de using (var client = new HttpClient(handler)) using (var formData = new MultipartFormDataContent()) { - foreach (var path in context.DeploymentFiles) + foreach (var path in context.GetDeploymentFiles()) { byte[] fileData = File.ReadAllBytes(path); - formData.Add(new ByteArrayContent(fileData), "file {0}".FormatInvariant(++count), Path.GetFileName(path)); + formData.Add(new ByteArrayContent(fileData), "file {0}".FormatInvariant(++countFiles), Path.GetFileName(path)); } var response = client.PostAsync(url, formData).Result; if (response.IsSuccessStatusCode) { - succeeded = count; + succeededFiles = countFiles; } else if (response.Content != null) { + context.Result.LastError = context.T("Admin.Common.HttpStatus", (int)response.StatusCode, response.StatusCode.ToString()); + var content = response.Content.ReadAsStringAsync().Result; - var msg = "Multipart form data upload failed. {0} ({1}). Response: {2}".FormatInvariant( - response.StatusCode.ToString(), (int)response.StatusCode, content.NaIfEmpty().Truncate(2000, "...")); + var msg = "Multipart form data upload failed. HTTP status {0} ({1}). Response: {2}".FormatInvariant( + (int)response.StatusCode, response.StatusCode.ToString(), content.NaIfEmpty().Truncate(2000, "...")); context.Log.Error(msg); } @@ -60,16 +62,16 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de if (deployment.Username.HasValue()) webClient.Credentials = new NetworkCredential(deployment.Username, deployment.Password); - foreach (var path in context.DeploymentFiles) + foreach (var path in context.GetDeploymentFiles()) { webClient.UploadFile(url, path); - ++succeeded; + ++succeededFiles; } } } - context.Log.Information("{0} file(s) successfully uploaded via HTTP ({1}).".FormatInvariant(succeeded, deployment.HttpTransmissionType.ToString())); + context.Log.Information("{0} file(s) successfully uploaded via HTTP ({1}).".FormatInvariant(succeededFiles, deployment.HttpTransmissionType.ToString())); } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs new file mode 100644 index 0000000000..1ae3301ec4 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.IO; +using SmartStore.Core.Domain; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + public interface IFilePublisher + { + void Publish(ExportDeploymentContext context, ExportDeployment deployment); + } + + + public class ExportDeploymentContext + { + public Localizer T { get; set; } + public ILogger Log { get; set; } + + public string FolderContent { get; set; } + + public string ZipPath { get; set; } + public bool CreateZipArchive { get; set; } + + public DataDeploymentResult Result { get; set; } + + public IEnumerable GetDeploymentFiles() + { + if (!CreateZipArchive) + { + return System.IO.Directory.EnumerateFiles(FolderContent, "*", SearchOption.AllDirectories); + } + + if (File.Exists(ZipPath)) + { + return new string[] { ZipPath }; + } + + return new string[0]; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs new file mode 100644 index 0000000000..d438e808a2 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs @@ -0,0 +1,44 @@ +using System.IO; +using SmartStore.Core.Domain; +using SmartStore.Core.Logging; +using SmartStore.Utilities; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + public class PublicFolderPublisher : IFilePublisher + { + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var destinationFolder = deployment.GetDeploymentFolder(true); + + if (destinationFolder.IsEmpty()) + return; + + if (!System.IO.Directory.Exists(destinationFolder)) + { + System.IO.Directory.CreateDirectory(destinationFolder); + } + + if (context.CreateZipArchive) + { + if (File.Exists(context.ZipPath)) + { + var destinationFile = Path.Combine(destinationFolder, Path.GetFileName(context.ZipPath)); + + File.Copy(context.ZipPath, destinationFile, true); + + context.Log.Information("Copied zipped export data to " + destinationFile); + } + } + else + { + if (!FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(destinationFolder))) + { + context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); + } + + context.Log.Information("Copied export data files to " + destinationFolder); + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index 232e2ee864..1917084af2 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -136,15 +136,30 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec return price; } - private decimal CalculatePrice(DataExporterContext ctx, Product product, bool forAttributeCombination) + private decimal CalculatePrice( + DataExporterContext ctx, + Product product, + ProductVariantAttributeCombination combination, + IList attributeValues) { - decimal price = product.Price; + var price = product.Price; + var priceCalculationContext = ctx.ProductExportContext as PriceCalculationContext; - // price type - if (ctx.Projection.PriceType.HasValue && !forAttributeCombination) + if (combination != null) { - var priceCalculationContext = ctx.ProductExportContext as PriceCalculationContext; + // price for attribute combination + var attributesTotalPriceBase = decimal.Zero; + if (attributeValues != null) + { + attributeValues.Each(x => attributesTotalPriceBase += _priceCalculationService.Value.GetProductVariantAttributeValuePriceAdjustment(x)); + } + + price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, attributesTotalPriceBase, true, 1, null, priceCalculationContext); + } + else if (ctx.Projection.PriceType.HasValue) + { + // price for product if (ctx.Projection.PriceType.Value == PriceDisplayType.LowestPrice) { bool displayFromMessage; @@ -163,7 +178,6 @@ private decimal CalculatePrice(DataExporterContext ctx, Product product, bool fo return ConvertPrice(ctx, product, price) ?? price; } - private List GetLocalized(DataExporterContext ctx, T entity, params Expression>[] keySelectors) where T : BaseEntity, ILocalizedEntity { @@ -487,6 +501,11 @@ private dynamic ToDynamic(DataExporterContext ctx, Category category) result.Picture = null; + if (ctx.CategoryTemplates.ContainsKey(category.CategoryTemplateId)) + result._CategoryTemplateViewPath = ctx.CategoryTemplates[category.CategoryTemplateId]; + else + result._CategoryTemplateViewPath = ""; + result._Localized = GetLocalized(ctx, category, x => x.Name, x => x.FullName, @@ -547,7 +566,6 @@ private dynamic ToDynamic( product.MergeWithCombination(combination); var languageId = (ctx.Projection.LanguageId ?? 0); - var productTemplate = ctx.ProductTemplates.FirstOrDefault(x => x.Key == product.ProductTemplateId); var pictureSize = _mediaSettings.Value.ProductDetailsPictureSize; var numberOfPictures = (ctx.Projection.NumberOfPictures ?? int.MaxValue); int[] pictureIds = (combination == null ? new int[0] : combination.GetAssignedPictureIds()); @@ -563,10 +581,11 @@ private dynamic ToDynamic( var productTags = ctx.ProductExportContext.ProductTags.Load(product.Id); var specificationAttributes = ctx.ProductExportContext.ProductSpecificationAttributes.Load(product.Id); + var variantAttributes = (combination != null ? _productAttributeParser.Value.DeserializeProductVariantAttributes(combination.AttributesXml) : null); + var variantAttributeValues = (combination != null ? _productAttributeParser.Value.ParseProductVariantAttributeValues(variantAttributes, productAttributes) : null); + if (pictureIds.Length > 0) - { productPictures = productPictures.Where(x => pictureIds.Contains(x.PictureId)); - } productPictures = productPictures.Take(numberOfPictures); @@ -574,35 +593,53 @@ private dynamic ToDynamic( #region gerneral data - dynObject.Price = CalculatePrice(ctx, product, combination != null); - - if (combination != null && ctx.Projection.AttributeCombinationValueMerging == ExportAttributeValueMerging.AppendAllValuesToName) - { - var values = _productAttributeParser.Value.ParseProductVariantAttributeValues(combination.AttributesXml, productAttributes, ctx.Projection.LanguageId ?? 0); + dynObject._CategoryName = null; + dynObject._CategoryPath = null; + dynObject._AttributeCombination = null; + dynObject._AttributeCombinationValues = null; + dynObject._AttributeCombinationId = (combination == null ? 0 : combination.Id); + dynObject._DetailUrl = ctx.Store.Url.EnsureEndsWith("/") + (string)dynObject.SeName; + + if (combination == null) + dynObject._UniqueId = product.Id.ToString(); + else + dynObject._UniqueId = string.Concat(product.Id, "-", combination.Id); - dynObject.Name = ((string)dynObject.Name).Grow(string.Join(", ", values), " "); - } + dynObject.Price = CalculatePrice(ctx, product, combination, variantAttributeValues); dynObject._BasePriceInfo = product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, _priceCalculationService.Value, ctx.ContextCurrency, decimal.Zero, true); - dynObject._ProductTemplateViewPath = (productTemplate.Value == null ? "" : productTemplate.Value.ViewPath); - - var detailUrl = ctx.Store.Url.EnsureEndsWith("/") + (string)dynObject.SeName; + if (ctx.ProductTemplates.ContainsKey(product.ProductTemplateId)) + dynObject._ProductTemplateViewPath = ctx.ProductTemplates[product.ProductTemplateId]; + else + dynObject._ProductTemplateViewPath = ""; if (combination != null) { + if (ctx.Supports(ExportFeatures.UsesAttributeCombination)) + { + dynObject._AttributeCombination = variantAttributes; + dynObject._AttributeCombinationValues = variantAttributeValues; + } + + if (ctx.Projection.AttributeCombinationValueMerging == ExportAttributeValueMerging.AppendAllValuesToName) + { + var valueNames = variantAttributeValues + .Select(x => x.GetLocalized(y => y.Name, languageId, true, false)) + .ToList(); + + dynObject.Name = ((string)dynObject.Name).Grow(string.Join(", ", valueNames), " "); + } + var attributeQueryString = _productAttributeParser.Value.SerializeQueryData(combination.AttributesXml, product.Id); if (attributeQueryString.HasValue()) { - detailUrl = string.Concat(detailUrl, detailUrl.Contains("?") ? "&" : "?", "attributes=", attributeQueryString); + var url = (string)dynObject._DetailUrl; + dynObject._DetailUrl = string.Concat(url, url.Contains("?") ? "&" : "?", "attributes=", attributeQueryString); } } - dynObject._DetailUrl = detailUrl; - - dynObject._CategoryName = null; - if (ctx.Categories.Count > 0) { dynObject._CategoryPath = _categoryService.Value.GetCategoryPath( @@ -614,13 +651,8 @@ private dynamic ToDynamic( productCategories.OrderBy(x => x.DisplayOrder).FirstOrDefault() ); } - else - { - dynObject._CategoryPath = null; - } ToDeliveryTime(ctx, dynObject, product.DeliveryTimeId); - ToQuantityUnit(ctx, dynObject, product.QuantityUnitId); dynObject.ProductPictures = productPictures @@ -841,20 +873,32 @@ private dynamic ToDynamic( if (ctx.Supports(ExportFeatures.UsesSpecialPrice)) { - dynObject._SpecialPrice = null; - dynObject._RegularPrice = null; // price if a special price would not exist + dynObject._SpecialPrice = null; // special price which is valid now + dynObject._FutureSpecialPrice = null; // special price which is valid now and in future + dynObject._RegularPrice = null; // price as if a special price would not exist if (!(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) { + if (product.SpecialPrice.HasValue && product.SpecialPriceEndDateTimeUtc.HasValue) + { + var endDate = DateTime.SpecifyKind(product.SpecialPriceEndDateTimeUtc.Value, DateTimeKind.Utc); + if (endDate > DateTime.UtcNow) + { + dynObject._FutureSpecialPrice = ConvertPrice(ctx, product, product.SpecialPrice.Value); + } + } + var specialPrice = _priceCalculationService.Value.GetSpecialPrice(product); dynObject._SpecialPrice = ConvertPrice(ctx, product, specialPrice); - if (specialPrice.HasValue) + if (specialPrice.HasValue || dynObject._FutureSpecialPrice != null) { decimal tmpSpecialPrice = product.SpecialPrice.Value; product.SpecialPrice = null; - dynObject._RegularPrice = CalculatePrice(ctx, product, combination != null); + + dynObject._RegularPrice = CalculatePrice(ctx, product, combination, variantAttributeValues); + product.SpecialPrice = tmpSpecialPrice; } } @@ -895,6 +939,8 @@ private dynamic ToDynamic(DataExporterContext ctx, OrderItem orderItem) dynamic result = new DynamicEntity(orderItem); + orderItem.Product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser.Value); + result.Product = ToDynamic(ctx, orderItem.Product); return result; @@ -988,19 +1034,17 @@ private List Convert(DataExporterContext ctx, Product product) //var productValues = new Dictionary(); var dbContext = _dbContext as DbContext; - product = _dbContext.Attach(product); - - var entry = dbContext.Entry(product); - - // the returned object is not the entity and is not being tracked by the context. - // it also does not have any relationships set to other objects. - // CurrentValues only includes database (thus primitive) values. - var productClone = entry.CurrentValues.ToObject() as Product; - - _dbContext.DetachEntity(product); - foreach (var combination in combinations.Where(x => x.IsActive)) { + product = _dbContext.Attach(product); + var entry = dbContext.Entry(product); + + // the returned object is not the entity and is not being tracked by the context. + // it also does not have any relationships set to other objects. + // CurrentValues only includes database (thus primitive) values. + var productClone = entry.CurrentValues.ToObject() as Product; + _dbContext.DetachEntity(product); + var dynObject = ToDynamic(ctx, productClone, combinations, combination); result.Add(dynObject); } @@ -1029,10 +1073,13 @@ private List Convert(DataExporterContext ctx, Order order) { var result = new List(); - ctx.OrderExportContext.Addresses.Collect(order.ShippingAddressId ?? 0); + if (!ctx.IsPreview) + { + ctx.OrderExportContext.Addresses.Collect(order.ShippingAddressId.HasValue ? order.ShippingAddressId.Value : 0); + ctx.OrderExportContext.Addresses.Load(order.BillingAddressId); + } var perfLoadId = (ctx.IsPreview ? 0 : order.Id); - var addresses = ctx.OrderExportContext.Addresses.Load(ctx.IsPreview ? 0 : order.BillingAddressId); var customers = ctx.OrderExportContext.Customers.Load(order.CustomerId); var rewardPointsHistories = ctx.OrderExportContext.RewardPointsHistories.Load(ctx.IsPreview ? 0 : order.CustomerId); var orderItems = ctx.OrderExportContext.OrderItems.Load(perfLoadId); @@ -1060,20 +1107,25 @@ private List Convert(DataExporterContext ctx, Order order) .PointsBalance; } - dynObject.BillingAddress = ToDynamic(ctx, addresses.FirstOrDefault(x => x.Id == order.BillingAddressId)); + if (ctx.OrderExportContext.Addresses.ContainsKey(order.BillingAddressId)) + { + dynObject.BillingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.BillingAddressId].FirstOrDefault()); + } - if (order.ShippingAddressId.HasValue) + if (order.ShippingAddressId.HasValue && ctx.OrderExportContext.Addresses.ContainsKey(order.ShippingAddressId.Value)) { - dynObject.ShippingAddress = ToDynamic(ctx, addresses.FirstOrDefault(x => x.Id == order.ShippingAddressId.Value)); + dynObject.ShippingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.ShippingAddressId.Value].FirstOrDefault()); } dynObject.OrderItems = orderItems .Select(e => { dynamic dyn = ToDynamic(ctx, e); - var productTemplate = ctx.ProductTemplates.FirstOrDefault(x => x.Key == e.Product.ProductTemplateId); - dyn.Product._ProductTemplateViewPath = (productTemplate.Value == null ? "" : productTemplate.Value.ViewPath); + if (ctx.ProductTemplates.ContainsKey(e.Product.ProductTemplateId)) + dyn.Product._ProductTemplateViewPath = ctx.ProductTemplates[e.Product.ProductTemplateId]; + else + dyn.Product._ProductTemplateViewPath = ""; dyn.Product._BasePriceInfo = e.Product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, _priceCalculationService.Value, ctx.ContextCurrency, decimal.Zero, true); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Events/RowExportingEvent.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs similarity index 50% rename from src/Libraries/SmartStore.Services/DataExchange/Events/RowExportingEvent.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs index 7459b9ee3f..0b96526e00 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Events/RowExportingEvent.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs @@ -4,13 +4,32 @@ namespace SmartStore.Services.DataExchange.Export.Events { // TODO: Another event message must be implemented, say 'ColumnsBuildingEvent' // The consumer of this event (most likely a plugin) could push a list of specific column headers - // into global the export definition. + // into the global export definition. public class RowExportingEvent { - public dynamic Row { get; internal set; } - public ExportEntityType EntityType { get; internal set; } - public DataExportRequest ExportRequest { get; internal set; } - public IExportExecuteContext ExecuteContext { get; internal set; } + public dynamic Row + { + get; + internal set; + } + + public ExportEntityType EntityType + { + get; + internal set; + } + + public DataExportRequest ExportRequest + { + get; + internal set; + } + + public ExportExecuteContext ExecuteContext + { + get; + internal set; + } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs index 514c223624..d5f2860ff8 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs @@ -7,132 +7,173 @@ namespace SmartStore.Services.DataExchange.Export { - public interface IExportExecuteContext + public class ExportExecuteContext { + private DataExportResult _result; + private CancellationToken _cancellation; + private DataExchangeAbortion _providerAbort; + + internal ExportExecuteContext(DataExportResult result, CancellationToken cancellation, string folder) + { + _result = result; + _cancellation = cancellation; + Folder = folder; + ExtraDataUnits = new List(); + CustomProperties = new Dictionary(); + } + /// /// Provides the data to be exported /// - IExportDataSegmenterConsumer Segmenter { get; } + public IExportDataSegmenterConsumer DataSegmenter { get; set; } /// /// The store context to be used for the export /// - dynamic Store { get; } + public dynamic Store { get; internal set; } /// /// The customer context to be used for the export /// - dynamic Customer { get; } + public dynamic Customer { get; internal set; } /// /// The currency context to be used for the export /// - dynamic Currency { get; } + public dynamic Currency { get; internal set; } /// /// The language context to be used for the export /// - dynamic Language { get; } + public dynamic Language { get; internal set; } /// /// Projection data /// - ExportProjection Projection { get; } + public ExportProjection Projection { get; internal set; } /// /// To log information into the export log file /// - ILogger Log { get; } + public ILogger Log { get; internal set; } /// /// Indicates whether and how to abort the export /// - DataExchangeAbortion Abort { get; set; } + public DataExchangeAbortion Abort + { + get + { + if (_cancellation.IsCancellationRequested || IsMaxFailures) + return DataExchangeAbortion.Hard; + return _providerAbort; + } + set + { + _providerAbort = value; + } + } + + public bool IsMaxFailures + { + get { return RecordsFailed > 11; } + } /// /// Identifier of current data stream. Can be null. /// - string DataStreamId { get; set; } + public string DataStreamId { get; set; } /// /// Stream used to write data to /// - Stream DataStream { get; } + public Stream DataStream { get; internal set; } /// - /// List with extra data streams required by provider + /// List with extra data units/streams required by provider /// - List ExtraDataStreams { get; set; } - + public List ExtraDataUnits { get; private set; } /// /// The maximum allowed file name length /// - int MaxFileNameLength { get; } + public int MaxFileNameLength { get; internal set; } /// /// The name of the current export file /// - string FileName { get; } + public string FileName { get; internal set; } /// /// The path of the export content folder /// - string Folder { get; } - + public string Folder { get; private set; } /// /// Whether the profile has a public deployment into "Exchange" folder /// - bool HasPublicDeployment { get; } + public bool HasPublicDeployment { get; internal set; } /// /// The local path to the public export folder "Exchange". null if the profile has no public deployment. /// - string PublicFolderPath { get; } - - /// - /// The public URL of the export file (accessible through the internet). null if the profile has no public deployment. - /// - string PublicFileUrl { get; } - + public string PublicFolderPath { get; internal set; } /// /// Provider specific configuration data /// - object ConfigurationData { get; } + public object ConfigurationData { get; internal set; } /// /// Use this dictionary for any custom data required along the export /// - Dictionary CustomProperties { get; set; } + public Dictionary CustomProperties { get; set; } /// /// Number of successful processed records /// - int RecordsSucceeded { get; set; } + public int RecordsSucceeded { get; set; } /// /// Number of failed records /// - int RecordsFailed { get; set; } + public int RecordsFailed { get; set; } /// /// Processes an exception that occurred while exporting a record /// - /// Exception - void RecordException(Exception exc, int entityId); + /// Exception + public void RecordException(Exception exception, int entityId) + { + ++RecordsFailed; + + Log.Error("Error while processing record with id {0}: {1}".FormatInvariant(entityId, exception.ToAllMessages()), exception); + + if (IsMaxFailures) + _result.LastError = exception.ToString(); + } + + public ProgressValueSetter ProgressValueSetter { get; internal set; } /// /// Allows to set a progress message /// /// Output message - void SetProgress(string message); + public void SetProgress(string message) + { + if (ProgressValueSetter != null && message.HasValue()) + { + try + { + ProgressValueSetter.Invoke(0, 0, message); + } + catch { } + } + } } - - public class ExportExtraStreams + public class ExportDataUnit { /// /// Your Id to identify this stream within a list of streams @@ -149,93 +190,4 @@ public class ExportExtraStreams /// public string FileName { get; set; } } - - - public class ExportExecuteContext : IExportExecuteContext - { - private DataExportResult _result; - private CancellationToken _cancellation; - private DataExchangeAbortion _providerAbort; - - internal ExportExecuteContext(DataExportResult result, CancellationToken cancellation, string folder) - { - _result = result; - _cancellation = cancellation; - Folder = folder; - ExtraDataStreams = new List(); - CustomProperties = new Dictionary(); - } - - public IExportDataSegmenterConsumer Segmenter { get; set; } - - public dynamic Store { get; internal set; } - public dynamic Customer { get; internal set; } - public dynamic Currency { get; internal set; } - public dynamic Language { get; internal set; } - public ExportProjection Projection { get; internal set; } - - public ILogger Log { get; internal set; } - public ProgressValueSetter ProgressValueSetter { get; internal set; } - - public DataExchangeAbortion Abort - { - get - { - if (_cancellation.IsCancellationRequested || IsMaxFailures) - return DataExchangeAbortion.Hard; - - return _providerAbort; - } - set - { - _providerAbort = value; - } - } - - public bool IsMaxFailures - { - get { return RecordsFailed > 11; } - } - - public string DataStreamId { get; set; } - public Stream DataStream { get; internal set; } - public List ExtraDataStreams { get; set; } - - public int MaxFileNameLength { get; internal set; } - public string FileName { get; internal set; } - public string Folder { get; private set; } - - public bool HasPublicDeployment { get; internal set; } - public string PublicFolderPath { get; internal set; } - public string PublicFileUrl { get; internal set; } - - public object ConfigurationData { get; internal set; } - - public Dictionary CustomProperties { get; set; } - - public int RecordsSucceeded { get; set; } - public int RecordsFailed { get; set; } - - public void RecordException(Exception exc, int entityId) - { - ++RecordsFailed; - - Log.Error("Error while processing record with id {0}: {1}".FormatInvariant(entityId, exc.ToAllMessages()), exc); - - if (IsMaxFailures) - _result.LastError = exc.ToString(); - } - - public void SetProgress(string message) - { - if (ProgressValueSetter != null && message.HasValue()) - { - try - { - ProgressValueSetter.Invoke(0, 0, message); - } - catch { } - } - } - } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs index 3d65d673c8..0371702eec 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs @@ -4,7 +4,10 @@ using System.IO; using System.Linq; using System.Text; +using System.Web; +using SmartStore.Core; using SmartStore.Core.Domain; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Plugins; using SmartStore.Services.Localization; @@ -21,7 +24,7 @@ public static class ExportExtensions /// true provider is valid, false provider is invalid. public static bool IsValid(this Provider provider) { - return provider != null; + return provider != null && provider.Value != null; } /// @@ -46,7 +49,7 @@ public static string GetName(this Provider provider, ILocalizat /// Folder path public static string GetExportFolder(this ExportProfile profile, bool content = false, bool create = false) { - var path = CommonHelper.MapPath(string.Concat("~/App_Data/ExportProfiles/", profile.FolderName, content ? "/Content" : "")); + var path = CommonHelper.MapPath(string.Concat(profile.FolderName, content ? "/Content" : "")); if (create && !System.IO.Directory.Exists(path)) System.IO.Directory.CreateDirectory(path); @@ -71,27 +74,48 @@ public static string GetExportLogPath(this ExportProfile profile) /// ZIP file path public static string GetExportZipPath(this ExportProfile profile) { - return Path.Combine(profile.GetExportFolder(), profile.FolderName + ".zip"); + var name = (new DirectoryInfo(profile.FolderName)).Name; + if (name.IsEmpty()) + name = "ExportData"; + + return Path.Combine(profile.GetExportFolder(), name.ToValidFileName() + ".zip"); } /// /// Gets existing export files for an export profile /// /// Export profile + /// Export provider /// List of file names - public static List GetExportFiles(this ExportProfile profile) + public static IEnumerable GetExportFiles(this ExportProfile profile, Provider provider) { var exportFolder = profile.GetExportFolder(true); - if (System.IO.Directory.Exists(exportFolder)) + if (System.IO.Directory.Exists(exportFolder) && provider.Value.FileExtension.HasValue()) { - return System.IO.Directory.EnumerateFiles(exportFolder, "*", SearchOption.TopDirectoryOnly) - .OrderBy(x => x) - .ToList(); + var filter = "*.{0}".FormatInvariant(provider.Value.FileExtension.ToLower()); + + return System.IO.Directory.EnumerateFiles(exportFolder, filter, SearchOption.AllDirectories).OrderBy(x => x); } - return new List(); - } + return Enumerable.Empty(); + } + + /// + /// Get number of existing export files + /// + /// Export profile + /// Export provider + /// Number of export files + public static int GetExportFileCount(this ExportProfile profile, Provider provider) + { + var result = profile.GetExportFiles(provider).Count(); + + if (File.Exists(profile.GetExportZipPath())) + ++result; + + return result; + } /// /// Resolves the file name pattern for an export profile @@ -128,5 +152,107 @@ public static string ResolveFileNamePattern(this ExportProfile profile, Store st return result; } + + /// + /// Get path of the deployment folder + /// + /// Export deployment + /// Deployment folder path + public static string GetDeploymentFolder(this ExportDeployment deployment, bool create = false) + { + if (deployment == null) + return null; + + string path = null; + + if (deployment.DeploymentType == ExportDeploymentType.PublicFolder) + { + if (deployment.SubFolder.HasValue()) + path = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder, deployment.SubFolder); + else + path = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder); + } + else if (deployment.DeploymentType == ExportDeploymentType.FileSystem) + { + if (deployment.FileSystemPath.IsEmpty()) + return null; + + if (Path.IsPathRooted(deployment.FileSystemPath)) + { + path = deployment.FileSystemPath; + } + else + { + path = FileSystemHelper.ValidateRootPath(deployment.FileSystemPath); + path = CommonHelper.MapPath(path); + } + } + + if (create && !System.IO.Directory.Exists(path)) + System.IO.Directory.CreateDirectory(path); + + return path; + } + + /// + /// Get url of the public folder and take filtering and projection into consideration + /// + /// Export deployment + /// Common services + /// Store entity + /// Absolute URL of the public folder (always ends with /) or null + public static string GetPublicFolderUrl(this ExportDeployment deployment, ICommonServices services, Store store = null) + { + if (deployment != null && deployment.DeploymentType == ExportDeploymentType.PublicFolder) + { + if (store == null) + { + var filter = XmlHelper.Deserialize(deployment.Profile.Filtering); + var storeId = filter.StoreId; + + if (storeId == 0) + { + var projection = XmlHelper.Deserialize(deployment.Profile.Projection); + storeId = (projection.StoreId ?? 0); + } + + store = (storeId == 0 ? services.StoreContext.CurrentStore : services.StoreService.GetStoreById(storeId)); + } + + var url = string.Concat( + store.Url.EnsureEndsWith("/"), + DataExporter.PublicFolder.EnsureEndsWith("/"), + deployment.SubFolder.HasValue() ? deployment.SubFolder.EnsureEndsWith("/") : "" + ); + + return url; + } + + return null; + } + + /// + /// Get icon class for a deployment type + /// + /// Deployment type + /// Icon class + public static string GetIconClass(this ExportDeploymentType type) + { + switch (type) + { + case ExportDeploymentType.FileSystem: + return "fa-folder-open-o"; + case ExportDeploymentType.Email: + return "fa-envelope-o"; + case ExportDeploymentType.Http: + return "fa-globe"; + case ExportDeploymentType.Ftp: + return "fa-files-o"; + case ExportDeploymentType.PublicFolder: + return "fa-unlock"; + default: + return "fa-question"; + } + } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs index eb187c1657..1814ae56ae 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs @@ -144,11 +144,11 @@ public virtual ExportProfile InsertExportProfile( .Replace("/", "") .Replace("-", ""); - profile.FolderName = SeoHelper.GetSeName(cleanedSystemName, true, false) + var folderName = SeoHelper.GetSeName(cleanedSystemName, true, false) .ToValidPath() .Truncate(_dataExchangeSettings.MaxFileNameLength); - profile.FolderName = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/App_Data/ExportProfiles"), profile.FolderName); + profile.FolderName = "~/App_Data/ExportProfiles/" + FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/App_Data/ExportProfiles"), folderName); if (profileSystemName.IsEmpty() && isSystemProfile) profile.SystemName = cleanedSystemName; @@ -167,13 +167,15 @@ public virtual ExportProfile InsertExportProfile( { if (features.HasFlag(ExportFeatures.CreatesInitialPublicDeployment)) { + var subFolder = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/" + DataExporter.PublicFolder), folderName); + profile.Deployments.Add(new ExportDeployment { ProfileId = profile.Id, Enabled = true, - IsPublic = true, - DeploymentType = ExportDeploymentType.FileSystem, - Name = profile.Name + DeploymentType = ExportDeploymentType.PublicFolder, + Name = profile.Name, + SubFolder = subFolder }); UpdateExportProfile(profile); @@ -220,9 +222,7 @@ public virtual void UpdateExportProfile(ExportProfile profile) if (profile == null) throw new ArgumentNullException("profile"); - profile.FolderName = profile.FolderName - .ToValidPath() - .Truncate(_dataExchangeSettings.MaxFileNameLength); + profile.FolderName = FileSystemHelper.ValidateRootPath(profile.FolderName); _exportProfileRepository.Update(profile); @@ -342,6 +342,16 @@ public virtual ExportDeployment GetExportDeploymentById(int id) return deployment; } + public virtual void UpdateExportDeployment(ExportDeployment deployment) + { + if (deployment == null) + throw new ArgumentNullException("deployment"); + + _exportDeploymentRepository.Update(deployment); + + _eventPublisher.EntityUpdated(deployment); + } + public virtual void DeleteExportDeployment(ExportDeployment deployment) { if (deployment == null) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs index 2a2d9618f1..c77638f60b 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs @@ -28,22 +28,22 @@ public virtual ExportConfigurationInfo ConfigurationInfo get { return null; } } + public void Execute(ExportExecuteContext context) + { + Export(context); + } + /// /// Export data to a file /// /// Export execution context - protected abstract void Export(IExportExecuteContext context); - - public void Execute(IExportExecuteContext context) - { - Export(context); - } + protected abstract void Export(ExportExecuteContext context); /// /// Called once per store when export execution ended /// /// Export execution context - public virtual void OnExecuted(IExportExecuteContext context) + public virtual void OnExecuted(ExportExecuteContext context) { } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs index 6f84b5f064..68c8b78d55 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs @@ -49,6 +49,8 @@ public static XmlWriterSettings DefaultSettings } } + public ExportXmlExclude Exclude { get; set; } + public XmlWriter Writer { get { return _writer; } @@ -330,36 +332,41 @@ public void WriteCategory(dynamic category, string node) } _writer.Write("Id", entity.Id.ToString()); - _writer.Write("Name", (string)category.Name); - _writer.Write("FullName", (string)category.FullName); - _writer.Write("Description", (string)category.Description); - _writer.Write("BottomDescription", (string)category.BottomDescription); - _writer.Write("CategoryTemplateId", entity.CategoryTemplateId.ToString()); - _writer.Write("MetaKeywords", (string)category.MetaKeywords); - _writer.Write("MetaDescription", (string)category.MetaDescription); - _writer.Write("MetaTitle", (string)category.MetaTitle); - _writer.Write("SeName", (string)category.SeName); - _writer.Write("ParentCategoryId", entity.ParentCategoryId.ToString()); - _writer.Write("PictureId", entity.PictureId.HasValue ? entity.PictureId.Value.ToString() : ""); - _writer.Write("PageSize", entity.PageSize.ToString()); - _writer.Write("AllowCustomersToSelectPageSize", entity.AllowCustomersToSelectPageSize.ToString()); - _writer.Write("PageSizeOptions", entity.PageSizeOptions); - _writer.Write("PriceRanges", entity.PriceRanges); - _writer.Write("ShowOnHomePage", entity.ShowOnHomePage.ToString()); - _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); - _writer.Write("Published", entity.Published.ToString()); - _writer.Write("Deleted", entity.Deleted.ToString()); - _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); - _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); - _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); - _writer.Write("SubjectToAcl", entity.SubjectToAcl.ToString()); - _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); - _writer.Write("Alias", (string)category.Alias); - _writer.Write("DefaultViewMode", entity.DefaultViewMode); - WritePicture(category.Picture, "Picture"); + if (!Exclude.HasFlag(ExportXmlExclude.Category)) + { + _writer.Write("Name", (string)category.Name); + _writer.Write("FullName", (string)category.FullName); + _writer.Write("Description", (string)category.Description); + _writer.Write("BottomDescription", (string)category.BottomDescription); + _writer.Write("CategoryTemplateId", entity.CategoryTemplateId.ToString()); + _writer.Write("CategoryTemplateViewPath", (string)category._CategoryTemplateViewPath); + _writer.Write("MetaKeywords", (string)category.MetaKeywords); + _writer.Write("MetaDescription", (string)category.MetaDescription); + _writer.Write("MetaTitle", (string)category.MetaTitle); + _writer.Write("SeName", (string)category.SeName); + _writer.Write("ParentCategoryId", entity.ParentCategoryId.ToString()); + _writer.Write("PictureId", entity.PictureId.HasValue ? entity.PictureId.Value.ToString() : ""); + _writer.Write("PageSize", entity.PageSize.ToString()); + _writer.Write("AllowCustomersToSelectPageSize", entity.AllowCustomersToSelectPageSize.ToString()); + _writer.Write("PageSizeOptions", entity.PageSizeOptions); + _writer.Write("PriceRanges", entity.PriceRanges); + _writer.Write("ShowOnHomePage", entity.ShowOnHomePage.ToString()); + _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("Deleted", entity.Deleted.ToString()); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + _writer.Write("SubjectToAcl", entity.SubjectToAcl.ToString()); + _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); + _writer.Write("Alias", (string)category.Alias); + _writer.Write("DefaultViewMode", entity.DefaultViewMode); - WriteLocalized(category); + WritePicture(category.Picture, "Picture"); + + WriteLocalized(category); + } if (node.HasValue()) { @@ -897,4 +904,15 @@ public void WriteCustomer(dynamic customer, string node) } } } + + + /// + /// Allows to exclude XML nodes from export + /// + [Flags] + public enum ExportXmlExclude + { + None = 0, + Category = 1 + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs index 7a61a36def..b0b0c8a6bd 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs @@ -108,7 +108,13 @@ ExportProfile InsertExportProfile( ExportDeployment GetExportDeploymentById(int id); /// - /// Deleted a export deployment + /// Update an export deployment + /// + /// Export deployment + void UpdateExportDeployment(ExportDeployment deployment); + + /// + /// Deleted an export deployment /// /// Export deployment void DeleteExportDeployment(ExportDeployment deployment); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs index e5248b20ba..60be2e11a9 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs @@ -24,12 +24,12 @@ public partial interface IExportProvider : IProvider, IUserEditable /// Export data to a file /// /// Export execution context - void Execute(IExportExecuteContext context); + void Execute(ExportExecuteContext context); /// /// Called once per store when export execution ended /// /// Export execution context - void OnExecuted(IExportExecuteContext context); + void OnExecuted(ExportExecuteContext context); } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/CategoryExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/CategoryExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/CustomerExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/CustomerExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs similarity index 94% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/DataExporterContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs index f909b93678..5c6c1315dd 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Internal/DataExporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -34,7 +34,8 @@ public DataExporterContext( Stores = new Dictionary(); Languages = new Dictionary(); Countries = new Dictionary(); - ProductTemplates = new Dictionary(); + ProductTemplates = new Dictionary(); + CategoryTemplates = new Dictionary(); NewsletterSubscriptions = new HashSet(); RecordsPerStore = new Dictionary(); @@ -109,7 +110,8 @@ public bool IsFileBasedExport public Dictionary Stores { get; set; } public Dictionary Languages { get; set; } public Dictionary Countries { get; set; } - public Dictionary ProductTemplates { get; set; } + public Dictionary ProductTemplates { get; set; } + public Dictionary CategoryTemplates { get; set; } public HashSet NewsletterSubscriptions { get; set; } // data loaded once per page diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/DynamicEntity.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/DynamicEntity.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/ManufacturerExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/ManufacturerExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/OrderExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/OrderExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/ProductExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/ProductExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs index 813f29b021..14a0ac77e9 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; -using System.Linq; +using Newtonsoft.Json; namespace SmartStore.Services.DataExchange.Import { public class ColumnMap { // maps source column to property - private readonly Dictionary _map = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _map = new Dictionary(StringComparer.OrdinalIgnoreCase); private static bool IsIndexed(string name) { @@ -24,27 +24,27 @@ private static string CreateSourceName(string name, string index) return name; } - public IReadOnlyDictionary Mappings + public IReadOnlyDictionary Mappings { get { return _map; } } - public static bool ParseSourceColumn(string sourceColumn, out string columnWithoutIndex, out string index) + public static bool ParseSourceName(string sourceName, out string nameWithoutIndex, out string index) { - columnWithoutIndex = sourceColumn; + nameWithoutIndex = sourceName; index = null; var result = true; - if (sourceColumn.HasValue() && IsIndexed(sourceColumn)) + if (sourceName.HasValue() && IsIndexed(sourceName)) { - var x1 = sourceColumn.IndexOf('['); - var x2 = sourceColumn.IndexOf(']', x1); + var x1 = sourceName.IndexOf('['); + var x2 = sourceName.IndexOf(']', x1); if (x1 != -1 && x2 != -1 && x2 > x1) { - columnWithoutIndex = sourceColumn.Substring(0, x1); - index = sourceColumn.Substring(x1 + 1, x2 - x1 - 1); + nameWithoutIndex = sourceName.Substring(0, x1); + index = sourceName.Substring(x1 + 1, x2 - x1 - 1); } else { @@ -55,128 +55,93 @@ public static bool ParseSourceColumn(string sourceColumn, out string columnWitho return result; } - //public IEnumerable> GetInvalidMappings() - //{ - // var mappings = Mappings.Where(x => - // x.Value.Property.HasValue() && - // Mappings.Count(y => y.Value.Property.IsCaseInsensitiveEqual(x.Value.Property)) > 1 - // ); - - // return mappings; - //} - - public bool AddMapping(string sourceColumn, string entityProperty, string defaultValue = null) + public void AddMapping(string sourceName, string mappedName, string defaultValue = null) { - return AddMapping(sourceColumn, null, entityProperty, defaultValue); + AddMapping(sourceName, null, mappedName, defaultValue); } - public bool AddMapping(string sourceColumn, string index, string entityProperty, string defaultValue = null) + public void AddMapping(string sourceName, string index, string mappedName, string defaultValue = null) { - Guard.ArgumentNotEmpty(() => sourceColumn); - - var isAlreadyMapped = (entityProperty.HasValue() && _map.Any(x => x.Value.Property.IsCaseInsensitiveEqual(entityProperty))); + Guard.ArgumentNotEmpty(() => sourceName); + Guard.ArgumentNotEmpty(() => mappedName); - if (isAlreadyMapped) - return false; + var key = CreateSourceName(sourceName, index); - _map[CreateSourceName(sourceColumn, index)] = new ColumnMappingValue + _map[key] = new ColumnMappingItem { - Property = entityProperty, + SoureName = key, + MappedName = mappedName, Default = defaultValue }; - - return true; } /// /// Gets a mapped column value /// - /// The name of the column to get a mapped value for. - /// The mapped column value OR - if the name is unmapped - a value with the passed - public ColumnMappingValue GetMapping(string sourceColumn) - { - ColumnMappingValue result; - - if (_map.TryGetValue(sourceColumn, out result)) - { - // a) source column and property are equal or b) property should be ignored - return result; - } - - var crossPair = _map.FirstOrDefault(x => x.Value.Property.IsCaseInsensitiveEqual(sourceColumn)); - - if (crossPair.Key.HasValue()) - { - // source column and property are unequal - return new ColumnMappingValue - { - Property = crossPair.Key, - Default = (crossPair.Value != null ? crossPair.Value.Default : null) - }; - } - - // there is no mapping at all - return new ColumnMappingValue { Property = sourceColumn }; - } - - /// - /// Gets a mapped column value - /// - /// The name of the column to get a mapped value for. + /// The name of the column to get a mapped value for. /// The column index, e.g. a language code (de, en etc.) - /// The mapped column value OR - if the name is unmapped - a value with the passed [] - public ColumnMappingValue GetMapping(string sourceColumn, string index) + /// The mapped column value OR - if the name is unmapped - a value with the passed [] + public ColumnMappingItem GetMapping(string sourceName, string index) { - return GetMapping(CreateSourceName(sourceColumn, index)); + return GetMapping(CreateSourceName(sourceName, index)); } /// - /// Gets a mapped property name + /// Gets a mapped column value /// - /// The name of the column to get a mapped property name for. - /// The mapped property name OR - if the name is unmapped - the passed [] - public string GetMappedProperty(string sourceColumn) + /// The name of the column to get a mapped value for. + /// The mapped column value OR - if the name is unmapped - the value of the passed + public ColumnMappingItem GetMapping(string sourceName) { - ColumnMappingValue result; + ColumnMappingItem result; - if (_map.TryGetValue(sourceColumn, out result)) + if (_map.TryGetValue(sourceName, out result)) { - return result.Property; - } - - var crossPair = _map.FirstOrDefault(x => x.Value.Property.IsCaseInsensitiveEqual(sourceColumn)); - - if (crossPair.Key.HasValue()) - { - return crossPair.Key; + return result; } - return sourceColumn; - } - - /// - /// Gets a mapped property name - /// - /// The name of the column to get a mapped property name for. - /// The column index, e.g. a language code (de, en etc.) - /// The mapped property name OR - if the name is unmapped - the passed [] - public string GetMappedProperty(string sourceColumn, string index) - { - return GetMappedProperty(CreateSourceName(sourceColumn, index)); + return new ColumnMappingItem { SoureName = sourceName, MappedName = sourceName }; } } - public class ColumnMappingValue + [JsonObject(MemberSerialization.OptIn)] + public class ColumnMappingItem { + private bool? _ignored; + + /// + /// The source name + /// + [JsonIgnore] + public string SoureName { get; set; } + /// - /// The property name of the target entity + /// The mapped name /// - public string Property { get; set; } + [JsonProperty] + public string MappedName { get; set; } /// /// An optional default value /// + [JsonProperty] public string Default { get; set; } + + /// + /// Indicates whether to explicitly ignore this property + /// + public bool IgnoreProperty + { + get + { + if (_ignored == null) + { + _ignored = Default != null && Default == "[IGNOREPROPERTY]"; + } + + return _ignored.Value; + } + } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs index 78cbd7fa13..c46b90b82a 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs @@ -27,12 +27,12 @@ public override object ConvertFrom(CultureInfo culture, object value) { if (value is string) { - var dict = JsonConvert.DeserializeObject>((string)value); + var dict = JsonConvert.DeserializeObject>((string)value); var map = new ColumnMap(); foreach (var kvp in dict) { - map.AddMapping(kvp.Key, null, kvp.Value.Property, kvp.Value.Default); + map.AddMapping(kvp.Key, null, kvp.Value.MappedName, kvp.Value.Default); } return map; diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs index 9413254f3d..aeb4b81897 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs @@ -1,166 +1,57 @@ using System; using System.IO; -using System.Linq; using System.Text; using System.Threading; +using System.Linq; using SmartStore.Core; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Domain.Forums; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Domain.Seo; using SmartStore.Core.Email; using SmartStore.Core.Localization; using SmartStore.Core.Logging; -using SmartStore.Services.Affiliates; -using SmartStore.Services.Catalog; -using SmartStore.Services.Catalog.Importer; -using SmartStore.Services.Common; -using SmartStore.Services.Customers; -using SmartStore.Services.Customers.Importer; using SmartStore.Services.DataExchange.Csv; using SmartStore.Services.DataExchange.Import.Internal; -using SmartStore.Services.Directory; -using SmartStore.Services.Helpers; using SmartStore.Services.Localization; -using SmartStore.Services.Media; using SmartStore.Services.Messages; -using SmartStore.Services.Messages.Importer; using SmartStore.Services.Security; -using SmartStore.Services.Seo; -using SmartStore.Services.Stores; using SmartStore.Utilities; namespace SmartStore.Services.DataExchange.Import { public partial class DataImporter : IDataImporter { - #region Dependencies - private readonly ICommonServices _services; - private readonly ICustomerService _customerService; private readonly IImportProfileService _importProfileService; - - private readonly Lazy> _subscriptionRepository; - private readonly Lazy> _pictureRepository; - private readonly Lazy> _productPictureRepository; - private readonly Lazy> _productManufacturerRepository; - private readonly Lazy> _productCategoryRepository; - private readonly Lazy> _urlRecordRepository; - private readonly Lazy> _productRepository; - private readonly Lazy> _customerRepository; - private readonly Lazy> _categoryRepository; - - private readonly Lazy _languageService; - private readonly Lazy _localizedEntityService; - private readonly Lazy _pictureService; - private readonly Lazy _manufacturerService; - private readonly Lazy _categoryService; - private readonly Lazy _categoryTemplateService; - private readonly Lazy _productTemplateService; - private readonly Lazy _productService; - private readonly Lazy _urlRecordService; - private readonly Lazy _storeMappingService; - private readonly Lazy _genericAttributeService; - private readonly Lazy _affiliateService; - private readonly Lazy _countryService; - private readonly Lazy _stateProvinceService; + private readonly ILanguageService _languageService; + private readonly Func _importerFactory; private readonly Lazy _emailAccountService; private readonly Lazy _emailSender; - private readonly Lazy _fileDownloadManager; - - private readonly Lazy _seoSettings; - private readonly Lazy _customerSettings; - private readonly Lazy _dateTimeSettings; - private readonly Lazy _forumSettings; private readonly Lazy _contactDataSettings; private readonly Lazy _dataExchangeSettings; public DataImporter( ICommonServices services, - ICustomerService customerService, IImportProfileService importProfileService, - Lazy> subscriptionRepository, - Lazy> pictureRepository, - Lazy> productPictureRepository, - Lazy> productManufacturerRepository, - Lazy> productCategoryRepository, - Lazy> urlRecordRepository, - Lazy> productRepository, - Lazy> customerRepository, - Lazy> categoryRepository, - Lazy languageService, - Lazy localizedEntityService, - Lazy pictureService, - Lazy manufacturerService, - Lazy categoryService, - Lazy categoryTemplateService, - Lazy productTemplateService, - Lazy productService, - Lazy urlRecordService, - Lazy storeMappingService, - Lazy genericAttributeService, - Lazy affiliateService, - Lazy countryService, - Lazy stateProvinceService, + ILanguageService languageService, + Func importerFactory, Lazy emailAccountService, Lazy emailSender, - Lazy fileDownloadManager, - Lazy seoSettings, - Lazy customerSettings, - Lazy dateTimeSettings, - Lazy forumSettings, Lazy contactDataSettings, Lazy dataExchangeSettings) { _services = services; - _customerService = customerService; _importProfileService = importProfileService; - - _subscriptionRepository = subscriptionRepository; - _pictureRepository = pictureRepository; - _productPictureRepository = productPictureRepository; - _productManufacturerRepository = productManufacturerRepository; - _productCategoryRepository = productCategoryRepository; - _urlRecordRepository = urlRecordRepository; - _productRepository = productRepository; - _customerRepository = customerRepository; - _categoryRepository = categoryRepository; - _languageService = languageService; - _localizedEntityService = localizedEntityService; - _pictureService = pictureService; - _manufacturerService = manufacturerService; - _categoryService = categoryService; - _categoryTemplateService = categoryTemplateService; - _productTemplateService = productTemplateService; - _productService = productService; - _urlRecordService = urlRecordService; - _storeMappingService = storeMappingService; - _genericAttributeService = genericAttributeService; - _affiliateService = affiliateService; - _countryService = countryService; - _stateProvinceService = stateProvinceService; + _importerFactory = importerFactory; _emailAccountService = emailAccountService; _emailSender = emailSender; - _fileDownloadManager = fileDownloadManager; - - _seoSettings = seoSettings; - _customerSettings = customerSettings; - _dateTimeSettings = dateTimeSettings; - _forumSettings = forumSettings; _contactDataSettings = contactDataSettings; _dataExchangeSettings = dataExchangeSettings; T = NullLocalizer.Instance; } - #endregion - public Localizer T { get; set; } private bool HasPermission(DataImporterContext ctx) @@ -310,7 +201,7 @@ private void ImportCoreInner(DataImporterContext ctx, string filePath) CsvConfiguration csvConfiguration = null; var extension = Path.GetExtension(filePath); - if (extension.IsCaseInsensitiveEqual(".csv")) + if ((new string[] { ".csv", ".txt", ".tab" }).Contains(extension, StringComparer.OrdinalIgnoreCase)) { var converter = new CsvConfigurationConverter(); csvConfiguration = converter.ConvertFrom(ctx.Request.Profile.FileTypeConfiguration); @@ -365,11 +256,14 @@ private void ImportCoreOuter(DataImporterContext ctx) { ctx.Log = logger; + ctx.ExecuteContext.DataExchangeSettings = _dataExchangeSettings.Value; + ctx.ExecuteContext.Services = _services; ctx.ExecuteContext.Log = logger; - ctx.ExecuteContext.Languages = _languageService.Value.GetAllLanguages(true); + ctx.ExecuteContext.Languages = _languageService.GetAllLanguages(true); ctx.ExecuteContext.UpdateOnly = ctx.Request.Profile.UpdateOnly; ctx.ExecuteContext.KeyFieldNames = ctx.Request.Profile.KeyFieldNames.SplitSafe(","); ctx.ExecuteContext.ImportFolder = ctx.Request.Profile.GetImportFolder(); + ctx.ExecuteContext.ExtraData = XmlHelper.Deserialize(ctx.Request.Profile.ExtraData); { var mapConverter = new ColumnMapConverter(); @@ -379,76 +273,12 @@ private void ImportCoreOuter(DataImporterContext ctx) var files = ctx.Request.Profile.GetImportFiles(); if (files.Count == 0) - throw new SmartException("No files to import found."); + throw new SmartException("No files to import."); if (!HasPermission(ctx)) throw new SmartException("You do not have permission to perform the selected import."); - if (ctx.Request.Profile.EntityType == ImportEntityType.Product) - { - ctx.Importer = new ProductImporter( - _productPictureRepository.Value, - _productManufacturerRepository.Value, - _productCategoryRepository.Value, - _urlRecordRepository.Value, - _productRepository.Value, - _services, - _localizedEntityService.Value, - _pictureService.Value, - _manufacturerService.Value, - _categoryService.Value, - _productService.Value, - _urlRecordService.Value, - _productTemplateService.Value, - _storeMappingService.Value, - _fileDownloadManager.Value, - _seoSettings.Value, - _dataExchangeSettings.Value); - } - else if (ctx.Request.Profile.EntityType == ImportEntityType.Customer) - { - ctx.Importer = new CustomerImporter( - _customerRepository.Value, - _pictureRepository.Value, - _services, - _genericAttributeService.Value, - _customerService, - _pictureService.Value, - _affiliateService.Value, - _countryService.Value, - _stateProvinceService.Value, - _fileDownloadManager.Value, - _customerSettings.Value, - _dateTimeSettings.Value, - _forumSettings.Value, - _dataExchangeSettings.Value); - } - else if (ctx.Request.Profile.EntityType == ImportEntityType.NewsLetterSubscription) - { - ctx.Importer = new NewsLetterSubscriptionImporter( - _services, - _subscriptionRepository.Value); - } - else if (ctx.Request.Profile.EntityType == ImportEntityType.Category) - { - ctx.Importer = new CategoryImporter( - _categoryRepository.Value, - _urlRecordRepository.Value, - _pictureRepository.Value, - _services, - _urlRecordService.Value, - _categoryTemplateService.Value, - _storeMappingService.Value, - _pictureService.Value, - _localizedEntityService.Value, - _fileDownloadManager.Value, - _seoSettings.Value, - _dataExchangeSettings.Value); - } - else - { - throw new SmartException("Unsupported entity type {0}.".FormatInvariant(ctx.Request.Profile.EntityType.ToString())); - } + ctx.Importer = _importerFactory(ctx.Request.Profile.EntityType); files.ForEach(x => ImportCoreInner(ctx, x)); } @@ -462,7 +292,7 @@ private void ImportCoreOuter(DataImporterContext ctx) { // database context sharing problem: if there are entities in modified state left by the provider due to SaveChanges failure, // then all subsequent SaveChanges would fail too (e.g. IImportProfileService.UpdateImportProfile, IScheduledTaskService.UpdateTask...). - // so whatever it is, detach\dispose all that the tracker still has tracked. + // so whatever it is, detach\dispose all what the tracker still has tracked. _services.DbContext.DetachAll(false); } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs index 68cb99eb9c..52be0a8dfa 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs @@ -31,6 +31,8 @@ public LightweightDataTable(IList columns, IList data) _columns = new ReadOnlyCollection(columns); + TrimData(data); + var rows = data.Select(x => new LightweightDataRow(this, x)).Cast().ToList(); _rows = new ReadOnlyCollection(rows); @@ -49,6 +51,27 @@ public LightweightDataTable(IList columns, IList data) } } + private static void TrimData(IList data) + { + // When a user deletes content instead of whole rows from an excel sheet, + // our data table contains completely empty rows at the end. + // Here we get rid of them as they are absolutely useless. + for (int i = data.Count - 1; i >= 0; i--) + { + var allColumnsEmpty = data[i].All(x => x == null || x == DBNull.Value); + if (allColumnsEmpty) + { + data.RemoveAt(i); + //i--; + } + else + { + // get out here on the first occurence of a NON-empty row + break; + } + } + } + public bool HasColumn(string name) { if (name.HasValue()) @@ -137,7 +160,7 @@ public static IDataTable FromFile( if (contentLength == 0) { - throw Error.Argument("fileName", "The posted file '{0}' does not contain any data.".FormatInvariant(fileName)); // TODO Loc + throw Error.Argument("fileName", "The posted file '{0}' does not contain any data.".FormatInvariant(fileName)); } IDataReader dataReader = null; @@ -160,14 +183,14 @@ public static IDataTable FromFile( if (table.Columns.Count == 0 || table.Rows.Count == 0) { - throw Error.InvalidOperation("file", "The posted file '{0}' does not contain any columns or data rows.".FormatInvariant(fileName)); // TODO Loc + throw Error.InvalidOperation("The posted file '{0}' does not contain any columns or data rows.".FormatInvariant(fileName)); } return table; } - catch + catch (Exception ex) { - throw; + throw ex; } finally { @@ -208,6 +231,8 @@ public static IDataTable FromDataReader( var fieldCount = reader.FieldCount; + take = Math.Min(take, int.MaxValue - skip); + int i = -1; while (reader.Read()) { diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs index 9ef5baf6d5..eff63d8431 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs @@ -2,9 +2,20 @@ using System.Collections.Generic; using System.IO; using System.Net.Mime; +using System.Linq; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.IO; using SmartStore.Utilities; +using System.Linq.Expressions; +using SmartStore.Core; +using Autofac; +using SmartStore.Services.Localization; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Seo; +using SmartStore.Services.Seo; +using SmartStore.Core.Data; +using SmartStore.Services.Stores; +using SmartStore.Core.Domain.Stores; namespace SmartStore.Services.DataExchange.Import { @@ -12,31 +23,53 @@ public abstract class EntityImporterBase : IEntityImporter { private const string _imageDownloadFolder = @"Content\DownloadedImages"; - public DateTime UtcNow { get; private set; } - - public Dictionary DownloadedItems { get; private set; } + public DateTime UtcNow + { + get; + private set; + } - public string ImageDownloadFolder { get; private set; } + public Dictionary DownloadedItems + { + get; + private set; + } - public string ImageFolder { get; private set; } + public string ImageDownloadFolder + { + get; + private set; + } - public FileDownloadManagerContext DownloaderContext { get; private set; } + public string ImageFolder + { + get; + private set; + } - protected abstract void Import(IImportExecuteContext context); + public FileDownloadManagerContext DownloaderContext + { + get; + private set; + } - public void Execute(IImportExecuteContext context) + public void Execute(ImportExecuteContext context) { Import(context); } - public void Init(IImportExecuteContext context, DataExchangeSettings dataExchangeSettings) + protected abstract void Import(ImportExecuteContext context); + + protected void Initialize(ImportExecuteContext context) { UtcNow = DateTime.UtcNow; DownloadedItems = new Dictionary(); ImageDownloadFolder = Path.Combine(context.ImportFolder, _imageDownloadFolder); - if (dataExchangeSettings.ImageImportFolder.HasValue()) - ImageFolder = Path.Combine(context.ImportFolder, dataExchangeSettings.ImageImportFolder); + var settings = context.DataExchangeSettings; + + if (settings.ImageImportFolder.HasValue()) + ImageFolder = Path.Combine(context.ImportFolder, settings.ImageImportFolder); else ImageFolder = context.ImportFolder; @@ -45,10 +78,12 @@ public void Init(IImportExecuteContext context, DataExchangeSettings dataExchang DownloaderContext = new FileDownloadManagerContext { - Timeout = TimeSpan.FromMinutes(dataExchangeSettings.ImageDownloadTimeout), + Timeout = TimeSpan.FromMinutes(settings.ImageDownloadTimeout), Logger = context.Log, CancellationToken = context.CancellationToken }; + + context.Result.TotalRecords = context.DataSegmenter.TotalRows; } public FileDownloadManagerItem CreateDownloadImage(string urlOrPath, string seoName, int displayOrder) @@ -108,5 +143,160 @@ public void Succeeded(FileDownloadManagerItem item) DownloadedItems.Add(item.Url, Path.GetFileName(item.Path)); } } + + protected virtual int ProcessLocalizations( + ImportExecuteContext context, + IEnumerable> batch, + IDictionary>> localizableProperties) where TEntity : BaseEntity, ILocalizedEntity + { + Guard.ArgumentNotNull(() => context); + Guard.ArgumentNotNull(() => batch); + Guard.ArgumentNotNull(() => localizableProperties); + + // Perf: determine whether our localizable properties actually have + // counterparts in the source BEFORE import batch begins. This way we spare ourself + // to query over and over for values. + var localizedProps = (from kvp in localizableProperties + where context.DataSegmenter.GetColumnIndexes(kvp.Key).Length > 0 + select kvp.Key).ToArray(); + + if (localizedProps.Length == 0) + { + return 0; + } + + var localizedEntityService = context.Services.Resolve(); + + bool shouldSave = false; + + foreach (var row in batch) + { + foreach (var prop in localizedProps) + { + var lambda = localizableProperties[prop]; + foreach (var lang in context.Languages) + { + var code = lang.UniqueSeoCode; + string value; + + if (row.TryGetDataValue(prop /* ColumnName */, code, out value)) + { + localizedEntityService.SaveLocalizedValue(row.Entity, lambda, value, lang.Id); + shouldSave = true; + } + } + } + } + + if (shouldSave) + { + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } + + return 0; + } + + protected virtual int ProcessStoreMappings( + ImportExecuteContext context, + IEnumerable> batch) where TEntity : BaseEntity, IStoreMappingSupported + { + var storeMappingService = context.Services.Resolve(); + var storeMappingRepository = context.Services.Resolve>(); + + storeMappingRepository.AutoCommitEnabled = false; + + foreach (var row in batch) + { + var storeIds = row.GetDataValue>("StoreIds"); + if (!storeIds.IsNullOrEmpty()) + { + storeMappingService.SaveStoreMappings(row.Entity, storeIds.ToArray()); + } + } + + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } + + protected virtual int ProcessSlugs( + ImportExecuteContext context, + IEnumerable> batch, + string entityName) where TEntity : BaseEntity, ISlugSupported + { + var slugMap = new Dictionary(); + UrlRecord urlRecord = null; + + var urlRecordService = context.Services.Resolve(); + var urlRecordRepository = context.Services.Resolve>(); + var seoSettings = context.Services.Resolve(); + + Func slugLookup = ((s) => + { + return (slugMap.ContainsKey(s) ? slugMap[s] : null); + }); + + foreach (var row in batch) + { + try + { + string seName = null; + string localizedName = null; + + if (row.TryGetDataValue("SeName", out seName) || row.IsNew || row.NameChanged) + { + seName = row.Entity.ValidateSeName(seName, row.EntityDisplayName, true, urlRecordService, seoSettings, extraSlugLookup: slugLookup); + + if (row.IsNew) + { + // dont't bother validating SeName for new entities. + urlRecord = new UrlRecord + { + EntityId = row.Entity.Id, + EntityName = entityName, + Slug = seName, + LanguageId = 0, + IsActive = true, + }; + urlRecordRepository.Insert(urlRecord); + } + else + { + urlRecord = urlRecordService.SaveSlug(row.Entity, seName, 0); + } + + if (urlRecord != null) + { + // a new record was inserted to the store: keep track of it for this batch. + slugMap[seName] = urlRecord; + } + } + + // process localized SeNames + foreach (var lang in context.Languages) + { + var hasSeName = row.TryGetDataValue("SeName", lang.UniqueSeoCode, out seName); + var hasLocalizedName = row.TryGetDataValue("Name", lang.UniqueSeoCode, out localizedName); + + if (hasSeName || hasLocalizedName) + { + seName = row.Entity.ValidateSeName(seName, localizedName, false, urlRecordService, seoSettings, lang.Id, slugLookup); + urlRecord = urlRecordService.SaveSlug(row.Entity, seName, lang.Id); + if (urlRecord != null) + { + slugMap[seName] = urlRecord; + } + } + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.Message, row.GetRowInfo(), "SeName"); + } + } + + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs index a33f2a3ccd..bc64b63531 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs @@ -2,6 +2,6 @@ { public partial interface IEntityImporter { - void Execute(IImportExecuteContext context); + void Execute(ImportExecuteContext context); } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs index 7ca1197f0f..41129e24f8 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs @@ -2,18 +2,17 @@ using System.Collections.Generic; using System.Data; using System.Globalization; -using System.IO; using System.Linq; using SmartStore.Core; namespace SmartStore.Services.DataExchange.Import { - public class ImportDataSegmenter where T : BaseEntity + public class ImportDataSegmenter { private const int BATCHSIZE = 100; private readonly IDataTable _table; - private ImportRow[] _currentBatch; + private object[] _currentBatch; private readonly IPageable _pageable; private bool _bof; private CultureInfo _culture; @@ -88,14 +87,67 @@ public int BatchSize get { return BATCHSIZE; } } - public bool HasColumn(string name) + /// + /// Determines whether a specific column exists in the underlying data table. + /// + /// The name of the column to find + /// + /// If true and a column with the passed does not exist, + /// this method tests for the existence of any indexed column with the same name. + /// + /// true if the column exists, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasColumn(string name, bool withAnyIndex = false) { - return _table.HasColumn(_columnMap.GetMappedProperty(name)); + var result = HasColumn(name, null); + + if (!result && withAnyIndex) + { + // Column does not exist, but withAnyIndex is true: + // Test for existence of any indexed column. + result = GetColumnIndexes(name).Length > 0; + } + + return result; } + /// + /// Determines whether the column name[index] exists in the underlying data table. + /// + /// The name of the column to find + /// The index of the column + /// true if the column exists, false otherwise + /// + /// This method takes mapped column names into account. + /// public bool HasColumn(string name, string index) { - return _table.HasColumn(_columnMap.GetMappedProperty(name, index)); + return _table.HasColumn(_columnMap.GetMapping(name, index).MappedName); + } + + /// + /// Indicates whether to ignore the property that is mapped to columnName + /// + /// The name of the column + /// true ignore, false do not ignore + public bool IsIgnored(string columnName) + { + return IsIgnored(columnName, null); + } + + /// + /// Indicates whether to ignore the property that is mapped to columnName + /// + /// The name of the column + /// The index of the column + /// true ignore, false do not ignore + public bool IsIgnored(string columnName, string index) + { + var mapping = _columnMap.GetMapping(columnName, index); + + return mapping.IgnoreProperty; } /// @@ -167,28 +219,25 @@ public bool ReadNextBatch() return false; } - public ImportRow[] CurrentBatch + public IEnumerable> GetCurrentBatch() where T : BaseEntity { - get + if (_currentBatch == null) { - if (_currentBatch == null) + int start = _pageable.FirstItemIndex - 1; + int end = _pageable.LastItemIndex - 1; + + _currentBatch = new ImportRow[(end - start) + 1]; + + // Determine values per row + int i = 0; + for (int r = start; r <= end; r++) { - int start = _pageable.FirstItemIndex - 1; - int end = _pageable.LastItemIndex - 1; - - _currentBatch = new ImportRow[(end - start) + 1]; - - // Determine values per row - int i = 0; - for (int r = start; r <= end; r++) - { - _currentBatch[i] = new ImportRow(this, _table.Rows[r], r); - i++; - } + _currentBatch[i] = new ImportRow(this, _table.Rows[r], r); + i++; } - - return _currentBatch; } + + return _currentBatch.Cast>(); } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs index a37b9b0013..70170c0fd3 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs @@ -1,117 +1,160 @@ using System.Collections.Generic; using System.Threading; -using SmartStore.Core; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Logging; namespace SmartStore.Services.DataExchange.Import { - public interface IImportExecuteContext + public class ImportExecuteContext { + private DataExchangeAbortion _abortion; + private ProgressValueSetter _progressValueSetter; + private string _progressInfo; + + private IDataTable _dataTable; + private ImportDataSegmenter _segmenter; + + public ImportExecuteContext( + CancellationToken cancellation, + ProgressValueSetter progressValueSetter, + string progressInfo) + { + _progressValueSetter = progressValueSetter; + _progressInfo = progressInfo; + + CancellationToken = cancellation; + CustomProperties = new Dictionary(); + Result = new ImportResult(); + } + /// - /// Whether to only update existing records + /// Import settings /// - bool UpdateOnly { get; } + public DataExchangeSettings DataExchangeSettings + { + get; + internal set; + } /// - /// Name of key fields to identify existing records for updating + /// The data source (CSV, Excel etc.) /// - string[] KeyFieldNames { get; } + public IDataTable DataTable + { + get + { + return _dataTable; + } + internal set + { + _dataTable = value; + _segmenter = null; + } + } /// - /// The import folder + /// Mapping information between database and data source /// - string ImportFolder { get; } + public ColumnMap ColumnMap + { + get; + internal set; + } /// - /// Use this dictionary for any custom data required along the export + /// Whether to only update existing records /// - Dictionary CustomProperties { get; set; } + public bool UpdateOnly + { + get; + internal set; + } /// - /// All languages + /// Name of key fields to identify existing records for updating /// - IList Languages { get; } + public string[] KeyFieldNames + { + get; + internal set; + } /// - /// To log information into the import log file + /// All active languages /// - ILogger Log { get; } + public IList Languages + { + get; + internal set; + } /// - /// Cancellation token + /// To log information into the import log file /// - CancellationToken CancellationToken { get; } + public ILogger Log + { + get; + internal set; + } /// - /// Result of the import + /// Common Services /// - ImportResult Result { get; set; } + public ICommonServices Services + { + get; + internal set; + } /// - /// Indicates whether and how to abort the import + /// Cancellation token /// - DataExchangeAbortion Abort { get; set; } + public CancellationToken CancellationToken + { + get; + private set; + } /// - /// Creates a segmenter instance + /// The import folder /// - /// - /// - ImportDataSegmenter GetSegmenter() where TEntity : BaseEntity; + public string ImportFolder + { + get; + internal set; + } /// - /// Allows to set a progress message + /// Use this dictionary for any custom data required along the import /// - /// Progress value - /// /// Progress maximum - void SetProgress(int value, int maximum); - } - - - public class ImportExecuteContext : IImportExecuteContext - { - private DataExchangeAbortion _abortion; - private ProgressValueSetter _progressValueSetter; - private string _progressInfo; - - public ImportExecuteContext( - CancellationToken cancellation, - ProgressValueSetter progressValueSetter, - string progressInfo) + public Dictionary CustomProperties { - _progressValueSetter = progressValueSetter; - _progressInfo = progressInfo; - - CancellationToken = cancellation; - CustomProperties = new Dictionary(); - Result = new ImportResult(); + get; + set; } - public IDataTable DataTable { get; internal set; } - - public ColumnMap ColumnMap { get; internal set; } - - public bool UpdateOnly { get; internal set; } - - public string[] KeyFieldNames { get; internal set; } - - public IList Languages { get; internal set; } - - public ILogger Log { get; internal set; } - - public CancellationToken CancellationToken { get; private set; } - - public string ImportFolder { get; internal set; } - /// - /// Use this dictionary for any custom data required along the import + /// Result of the import /// - public Dictionary CustomProperties { get; set; } + public ImportResult Result + { + get; + set; + } - public ImportResult Result { get; set; } + /// + /// Extra import configuration data + /// + public ImportExtraData ExtraData + { + get; + internal set; + } + /// + /// Indicates whether and how to abort the import + /// public DataExchangeAbortion Abort { get @@ -135,11 +178,28 @@ public bool IsMaxFailures } } - public ImportDataSegmenter GetSegmenter() where TEntity : BaseEntity + public ImportDataSegmenter DataSegmenter { - return new ImportDataSegmenter(DataTable, ColumnMap); + get + { + if (_segmenter == null) + { + if (this.DataTable == null || this.ColumnMap == null) + { + throw new SmartException("A DataTable and a ColumnMap must be specified before accessing the DataSegmenter property."); + } + _segmenter = new ImportDataSegmenter(DataTable, ColumnMap); + } + + return _segmenter; + } } + /// + /// Allows to set a progress message + /// + /// Progress value + /// /// Progress maximum public void SetProgress(int value, int maximum) { try @@ -149,5 +209,19 @@ public void SetProgress(int value, int maximum) } catch { } } + + /// + /// Allows to set a message + /// + /// Message to display + public void SetProgress(string message) + { + try + { + if (_progressValueSetter != null && message.HasValue()) + _progressValueSetter.Invoke(0, 0, message); + } + catch { } + } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs new file mode 100644 index 0000000000..ecbe710677 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs @@ -0,0 +1,13 @@ +using System; + +namespace SmartStore.Services.DataExchange.Import +{ + [Serializable] + public class ImportExtraData + { + /// + /// Number of images per object to be imported + /// + public int? NumberOfPictures { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs index 76c5c87d32..c1e54633dc 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs @@ -48,61 +48,107 @@ public ImportProfileService( _dataExchangeSettings = dataExchangeSettings; } - private string GetLocalizedPropertyName(ImportEntityType type, string propertyName) + private string GetLocalizedPropertyName(ImportEntityType type, string property) { - if (propertyName.IsEmpty()) - return null; + if (property.IsEmpty()) + return ""; - var defaultKey = ""; - var keys = new Dictionary - { - { "Id", "Admin.Common.Entity.Fields.Id" }, - { "LimitedToStores", "Admin.Common.Store.LimitedTo" }, - { "DisplayOrder", "Common.DisplayOrder" }, - { "Deleted", "Admin.Common.Deleted" }, - { "CreatedOnUtc", "Common.CreatedOn" }, - { "UpdatedOnUtc", "Common.UpdatedOn" }, - { "HasDiscountsApplied", "Admin.Catalog.Products.Fields.HasDiscountsApplied" }, - { "DefaultViewMode", "Admin.Configuration.Settings.Catalog.DefaultViewMode" }, - { "StoreId", "Admin.Common.Store" } - }; + string key = null; + string prefixKey = null; - if (type == ImportEntityType.Product) - { - defaultKey = "Admin.Catalog.Products.Fields." + propertyName; + if (property.StartsWith("BillingAddress.")) + prefixKey = "Admin.Orders.Fields.BillingAddress"; + else if (property.StartsWith("ShippingAddress.")) + prefixKey = "Admin.Orders.Fields.ShippingAddress"; - keys.Add("ParentGroupedProductId", "Admin.Catalog.Products.Fields.AssociatedToProductName"); - } - else if (type == ImportEntityType.Category) - { - defaultKey = "Admin.Catalog.Categories.Fields." + propertyName; - } - else if (type == ImportEntityType.Customer) - { - defaultKey = "Admin.Customers.Customers.Fields." + propertyName; + #region Get resource key - keys.Add("PasswordFormatId", "Admin.Configuration.Settings.CustomerUser.DefaultPasswordFormat"); - keys.Add("LastIpAddress", "Admin.Customers.Customers.Fields.IPAddress"); - } - else if (type == ImportEntityType.NewsLetterSubscription) + switch (property) { - defaultKey = "Admin.Promotions.NewsLetterSubscriptions.Fields." + propertyName; + case "Id": + key = "Admin.Common.Entity.Fields.Id"; + break; + case "LimitedToStores": + key = "Admin.Common.Store.LimitedTo"; + break; + case "DisplayOrder": + key = "Common.DisplayOrder"; + break; + case "Deleted": + key = "Admin.Common.Deleted"; + break; + case "CreatedOnUtc": + case "BillingAddress.CreatedOnUtc": + case "ShippingAddress.CreatedOnUtc": + key = "Common.CreatedOn"; + break; + case "UpdatedOnUtc": + key = "Common.UpdatedOn"; + break; + case "HasDiscountsApplied": + key = "Admin.Catalog.Products.Fields.HasDiscountsApplied"; + break; + case "DefaultViewMode": + key = "Admin.Configuration.Settings.Catalog.DefaultViewMode"; + break; + case "StoreId": + key = "Admin.Common.Store"; + break; + case "ParentGroupedProductId": + key = "Admin.Catalog.Products.Fields.AssociatedToProductName"; + break; + case "PasswordFormatId": + key = "Admin.Configuration.Settings.CustomerUser.DefaultPasswordFormat"; + break; + case "LastIpAddress": + key = "Admin.Customers.Customers.Fields.IPAddress"; + break; + default: + switch (type) + { + case ImportEntityType.Product: + key = "Admin.Catalog.Products.Fields." + property; + break; + case ImportEntityType.Category: + key = "Admin.Catalog.Categories.Fields." + property; + break; + case ImportEntityType.Customer: + if (property.StartsWith("BillingAddress.") || property.StartsWith("ShippingAddress.")) + key = "Admin.Address.Fields." + property.Substring(property.IndexOf('.') + 1); + else + key = "Admin.Customers.Customers.Fields." + property; + break; + case ImportEntityType.NewsLetterSubscription: + key = "Admin.Promotions.NewsLetterSubscriptions.Fields." + property; + break; + } + break; } - var result = _localizationService.GetResource(keys.ContainsKey(propertyName) ? keys[propertyName] : defaultKey, 0, false, "", true); + #endregion + + if (key.IsEmpty()) + return ""; + + var result = _localizationService.GetResource(key, 0, false, "", true); if (result.IsEmpty()) { - if (defaultKey.EndsWith("Id")) - result = _localizationService.GetResource(defaultKey.Substring(0, defaultKey.Length - 2), 0, false, "", true); - else if (defaultKey.EndsWith("Utc")) - result = _localizationService.GetResource(defaultKey.Substring(0, defaultKey.Length - 3), 0, false, "", true); + if (key.EndsWith("Id")) + result = _localizationService.GetResource(key.Substring(0, key.Length - 2), 0, false, "", true); + else if (key.EndsWith("Utc")) + result = _localizationService.GetResource(key.Substring(0, key.Length - 3), 0, false, "", true); } if (result.IsEmpty()) { - Debug.WriteLine("Missing string resource mapping for {0}.{1}".FormatInvariant(type.ToString(), propertyName)); - return propertyName.SplitPascalCase(); + Debug.WriteLine("Missing string resource mapping for {0} - {1}".FormatInvariant(type.ToString(), property)); + result = property.SplitPascalCase(); + } + + if (prefixKey.HasValue()) + { + result = string.Concat(_localizationService.GetResource(prefixKey, 0, false, "", true), " - ", result); } return result; @@ -289,10 +335,17 @@ public virtual Dictionary GetImportableEntityProperties(ImportEn { ImportEntityType.NewsLetterSubscription, new string[] { } } }; + var addressSet = container.GetEntitySetByName("Addresses", true); + + var addressProperties = addressSet.ElementType.Members + .Where(x => !x.Name.IsCaseInsensitiveEqual("Id") && x.BuiltInTypeKind.HasFlag(BuiltInTypeKind.EdmProperty)) + .Select(x => x.Name) + .ToList(); + + foreach (ImportEntityType type in Enum.GetValues(typeof(ImportEntityType))) { EntitySet entitySet = null; - var dic = new Dictionary(StringComparer.OrdinalIgnoreCase); try { @@ -306,23 +359,42 @@ public virtual Dictionary GetImportableEntityProperties(ImportEn throw new SmartException("There is no entity set for ImportEntityType {0}. Note, the enum value must equal the entity name.".FormatInvariant(type.ToString())); } - foreach (var member in entitySet.ElementType.Members) + var dic = entitySet.ElementType.Members + .Where(x => !x.Name.IsCaseInsensitiveEqual("Id") && x.BuiltInTypeKind.HasFlag(BuiltInTypeKind.EdmProperty)) + .Select(x => x.Name) + .ToDictionary(x => x, x => "", StringComparer.OrdinalIgnoreCase); + + // lack of abstractness? + if ((type == ImportEntityType.Product || type == ImportEntityType.Category) && !dic.ContainsKey("SeName")) + { + dic.Add("SeName", ""); + } + + // shipping and billing address + if (type == ImportEntityType.Customer) { - if (!member.Name.IsCaseInsensitiveEqual("Id") && member.BuiltInTypeKind.HasFlag(BuiltInTypeKind.EdmProperty)) + foreach (var property in addressProperties) { - var localizedValue = GetLocalizedPropertyName(type, member.Name); + dic.Add("BillingAddress." + property, ""); + dic.Add("ShippingAddress." + property, ""); + } + } - dic.Add(member.Name, localizedValue.NaIfEmpty()); + // add localized property names + foreach (var key in dic.Keys.ToList()) + { + var localizedValue = GetLocalizedPropertyName(type, key); - if (localizableProperties[type].Contains(member.Name)) + dic[key] = localizedValue.NaIfEmpty(); + + if (localizableProperties[type].Contains(key)) + { + foreach (var language in allLanguages) { - foreach (var language in allLanguages) - { - dic.Add( - "{0}[{1}]".FormatInvariant(member.Name, language.UniqueSeoCode.EmptyNull().ToLower()), - "{0} {1}".FormatInvariant(localizedValue.NaIfEmpty(), allLanguageNames[language.UniqueSeoCode]) - ); - } + dic.Add( + "{0}[{1}]".FormatInvariant(key, language.UniqueSeoCode.EmptyNull().ToLower()), + "{0} {1}".FormatInvariant(localizedValue.NaIfEmpty(), allLanguageNames[language.UniqueSeoCode]) + ); } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs index 33279b7d7b..1f213ee9e4 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs @@ -8,38 +8,20 @@ namespace SmartStore.Services.DataExchange.Import { public class ImportRow where T : BaseEntity { + private const string ExplicitNull = "[NULL]"; + private bool _initialized = false; private T _entity; private string _entityDisplayName; private readonly int _position; private bool _isNew; + private bool _isDirty; private ImportRowInfo _rowInfo; - private readonly ImportDataSegmenter _segmenter; + private readonly ImportDataSegmenter _segmenter; private readonly IDataRow _row; - private TProp GetDefaultValue(ColumnMappingValue mapping, string columnName, TProp defaultValue, ImportResult result = null) - { - if (mapping != null && mapping.Default.HasValue()) - { - try - { - return mapping.Default.Convert(_segmenter.Culture); - } - catch (Exception exception) - { - if (result != null) - { - var msg = "Failed to convert default value '{0}'. Please specify a convertable default value. {1}"; - result.AddWarning(msg.FormatInvariant(mapping.Default, exception.Message), this.GetRowInfo(), columnName); - } - } - } - - return defaultValue; - } - - public ImportRow(ImportDataSegmenter parent, IDataRow row, int position) + public ImportRow(ImportDataSegmenter parent, IDataRow row, int position) { _segmenter = parent; _row = row; @@ -63,6 +45,27 @@ private void CheckInitialized() } } + private TProp GetDefaultValue(ColumnMappingItem mapping, TProp defaultValue, ImportResult result = null) + { + if (mapping != null && mapping.Default.HasValue()) + { + try + { + return mapping.Default.Convert(_segmenter.Culture); + } + catch (Exception exception) + { + if (result != null) + { + var msg = "Failed to convert default value '{0}'. Please specify a convertable default value. Column: {1}"; + result.AddWarning(msg.FormatInvariant(mapping.Default, exception.Message), this.GetRowInfo(), mapping.SoureName); + } + } + } + + return defaultValue; + } + public bool IsTransient { get { return _entity.Id == 0; } @@ -73,7 +76,12 @@ public bool IsNew get { return _isNew; } } - public ImportDataSegmenter Segmenter + public bool IsDirty + { + get { return _isDirty; } + } + + public ImportDataSegmenter Segmenter { get { return _segmenter; } } @@ -83,6 +91,11 @@ public T Entity get { return _entity; } } + public IDataRow DataRow + { + get { return _row; } + } + public string EntityDisplayName { get { return _entityDisplayName; } @@ -99,44 +112,147 @@ public int Position get { return _position; } } - public TProp GetDataValue(string columnName) + /// + /// Determines whether a specific column exists in the underlying data table + /// and contains a non-null, convertible value. + /// + /// The name of the column + /// + /// If true and a column with the passed does not exist, + /// this method seeks for any indexed column with the same name. + /// + /// true if the column exists and contains a value, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasDataValue(string columnName, bool withAnyIndex = false) { - return GetDataValue(columnName, null); + var result = HasDataValue(columnName, null); + + if (!result && withAnyIndex) + { + // Column does not have a value, but withAnyIndex is true: + // Test for values in any indexed column. + var indexes = _segmenter.GetColumnIndexes(columnName); + foreach (var idx in indexes) + { + result = HasDataValue(columnName, idx); + if (result) + break; + } + } + + return result; } - public TProp GetDataValue(string columnName, string index) + /// + /// Determines whether the column name[index] exists in the underlying data table + /// and contains a non-null, convertible value. + /// + /// The name of the column + /// The index of the column + /// true if the column exists and contains a value, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasDataValue(string columnName, string index) { + var mapping = _segmenter.ColumnMap.GetMapping(columnName, index); + object value; + return (_row.TryGetValue(mapping.MappedName, out value) && value != null && value != DBNull.Value); + } + + public TProp GetDataValue(string columnName, bool force = false) + { + TProp value; + TryGetDataValue(columnName, null, out value, force); + return value; + } + + public TProp GetDataValue(string columnName, string index, bool force = false) + { + TProp value; + TryGetDataValue(columnName, index, out value, force); + return value; + } + + public bool TryGetDataValue(string columnName, out TProp value, bool force = false) + { + return TryGetDataValue(columnName, null, out value, force); + } + + public bool TryGetDataValue(string columnName, string index, out TProp value, bool force = false) + { var mapping = _segmenter.ColumnMap.GetMapping(columnName, index); - if (_row.TryGetValue(mapping.Property, out value) && value != DBNull.Value) + if (!force && mapping.IgnoreProperty) { - return value.Convert(_segmenter.Culture); + value = default(TProp); + return false; } - return GetDefaultValue(mapping, columnName, default(TProp)); + object rawValue; + if (_row.TryGetValue(mapping.MappedName, out rawValue) && rawValue != null && rawValue != DBNull.Value) + { + value = rawValue.ToString().IsCaseInsensitiveEqual(ExplicitNull) + ? default(TProp) + : rawValue.Convert(_segmenter.Culture); + return true; + } + + if (IsNew) + { + // only transient/new entities should fallback to possible defaults. + value = GetDefaultValue(mapping, default(TProp)); + return true; + } + + value = default(TProp); + return false; + } + + public bool SetProperty( + ImportResult result, + Expression> prop, + TProp defaultValue = default(TProp), + Func converter = null) + { + return SetProperty( + result, + null, // columnName + prop, + defaultValue, + converter); } public bool SetProperty( ImportResult result, - T target, + string columnName, Expression> prop, TProp defaultValue = default(TProp), Func converter = null) { - // TBD: (MC) do not check for perf reason? + // TBD: (MC) do not check or validate for perf reason? //CheckInitialized(); var isPropertySet = false; var pi = prop.ExtractPropertyInfo(); var propName = pi.Name; + var target = _entity; + + columnName = columnName ?? propName; try { object value; - var mapping = _segmenter.ColumnMap.GetMapping(propName); + var mapping = _segmenter.ColumnMap.GetMapping(columnName); - if (_row.TryGetValue(mapping.Property, out value)) + if (mapping.IgnoreProperty) + { + // explicitly ignore this property + } + else if (_row.TryGetValue(mapping.MappedName, out value) && (value != null && value != DBNull.Value)) { // source contains field value. Set it. TProp converted; @@ -144,9 +260,10 @@ public bool SetProperty( { converted = converter(value, _segmenter.Culture); } - else if (value == DBNull.Value || value.ToString().IsCaseInsensitiveEqual("NULL")) + else if (value.ToString().IsCaseInsensitiveEqual(ExplicitNull)) { - converted = GetDefaultValue(mapping, propName, defaultValue, result); + // prop is "explicitly" set to null. Don't fallback to any default! + converted = default(TProp); } else { @@ -159,9 +276,13 @@ public bool SetProperty( } else { - if (IsTransient) + // source field value does not exist or is null/empty + if (IsNew) { - defaultValue = GetDefaultValue(mapping, propName, defaultValue, result); + // if entity is new and source field value is null, determine default value in this particular order: + // 2.) Default value in field mapping table + // 3.) passed default value argument + defaultValue = GetDefaultValue(mapping, defaultValue, result); // source does not contain field data or is empty... if (defaultValue != null) @@ -179,6 +300,11 @@ public bool SetProperty( result.AddWarning("Conversion failed: " + exception.Message, this.GetRowInfo(), propName); } + if (isPropertySet && !_isDirty) + { + _isDirty = true; + } + return isPropertySet; } diff --git a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs index 9b75b86e8b..ffa0fa2615 100644 --- a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs +++ b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs @@ -12,6 +12,7 @@ using SmartStore.Services.Customers; using SmartStore.Services.Common; using SmartStore.Services.Configuration; +using SmartStore.Services.Orders; namespace SmartStore.Services.Discounts { @@ -317,18 +318,18 @@ public virtual bool IsDiscountValid(Discount discount, Customer customer, string } //check date range - DateTime now = DateTime.UtcNow; - int storeId = _storeContext.CurrentStore.Id; + var now = DateTime.UtcNow; + var store = _storeContext.CurrentStore; if (discount.StartDateUtc.HasValue) { - DateTime startDate = DateTime.SpecifyKind(discount.StartDateUtc.Value, DateTimeKind.Utc); + var startDate = DateTime.SpecifyKind(discount.StartDateUtc.Value, DateTimeKind.Utc); if (startDate.CompareTo(now) > 0) return false; } if (discount.EndDateUtc.HasValue) { - DateTime endDate = DateTime.SpecifyKind(discount.EndDateUtc.Value, DateTimeKind.Utc); + var endDate = DateTime.SpecifyKind(discount.EndDateUtc.Value, DateTimeKind.Utc); if (endDate.CompareTo(now) < 0) return false; } @@ -336,33 +337,37 @@ public virtual bool IsDiscountValid(Discount discount, Customer customer, string if (!CheckDiscountLimitations(discount, customer)) return false; - // discount requirements - var requirements = discount.DiscountRequirements; + // better not to apply discounts if there are gift cards in the cart cause the customer could "earn" money through that. + if (discount.DiscountType == DiscountType.AssignedToOrderTotal || discount.DiscountType == DiscountType.AssignedToOrderSubTotal) + { + var cart = customer.ShoppingCartItems + .Filter(ShoppingCartType.ShoppingCart, store.Id) + .ToList(); + + if (cart.Any(x => x.Product.IsGiftCard)) + return false; + } + + // discount requirements + var requirements = discount.DiscountRequirements; foreach (var req in requirements) { - var requirementRule = LoadDiscountRequirementRuleBySystemName(req.DiscountRequirementRuleSystemName, storeId); + var requirementRule = LoadDiscountRequirementRuleBySystemName(req.DiscountRequirementRuleSystemName, store.Id); if (requirementRule == null) continue; - var request = new CheckDiscountRequirementRequest() + var request = new CheckDiscountRequirementRequest { DiscountRequirement = req, Customer = customer, - Store = _storeContext.CurrentStore + Store = store }; - if (!requirementRule.Value.CheckRequirement(request)) + + // TODO: cache result... CheckRequirement is very often called + if (!requirementRule.Value.CheckRequirement(request)) return false; } - // better not to apply discounts if there are gift cards in the cart cause the customer could "earn" money through that. - if (discount.DiscountType == DiscountType.AssignedToOrderTotal || discount.DiscountType == DiscountType.AssignedToOrderSubTotal) - { - var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, storeId); - - if (cart.Any(x => x.Item.Product.IsGiftCard)) - return false; - } - return true; } diff --git a/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs b/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs index a2e53aa8da..216894b2d2 100644 --- a/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs +++ b/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs @@ -13,6 +13,7 @@ namespace SmartStore { public static class NameValueCollectionExtensions { + // TODO: find place to make it public static private static string AttributeFormatedName(int productAttributeId, int attributeId, int productId = 0, int bundleItemId = 0) { if (productId == 0) @@ -25,34 +26,57 @@ public static void AddProductAttribute(this NameValueCollection collection, int { if (productAttributeId != 0 && attributeId != 0 && valueId != 0) { - string name = AttributeFormatedName(productAttributeId, attributeId, productId, bundleItemId); + var name = AttributeFormatedName(productAttributeId, attributeId, productId, bundleItemId); collection.Add(name, valueId.ToString()); } } /// - /// Converts attribute query data + /// Get selected attributes from query string /// - /// Name value collection + /// Name value collection with selected attributes + /// Query string parameters /// Attribute query data items with following structure: /// Product.Id, ProductAttribute.Id, Product_ProductAttribute_Mapping.Id, ProductVariantAttributeValue.Id, [BundleItem.Id] /// Product identifier to filter - public static void ConvertAttributeQueryData(this NameValueCollection collection, List> queryData, int productId = 0) + public static void GetSelectedAttributes(this NameValueCollection collection, NameValueCollection queryString, List> attributes, int productId = 0) { - if (collection == null || queryData == null || queryData.Count <= 0) - return; + Guard.NotNull(() => collection); - var items = queryData.Where(i => i.Count > 3); + // ambiguous parameters: let other query string parameters win over the json formatted attributes parameter + if (queryString != null && queryString.Count > 0) + { + var items = queryString.AllKeys + .Where(x => x.EmptyNull().StartsWith("product_attribute_")) + .SelectMany(queryString.GetValues, (k, v) => new { key = k.EmptyNull(), value = v.TrimSafe() }); - if (productId != 0) - items = items.Where(i => i[0] == productId); + foreach (var item in items) + { + var ids = item.key.Replace("product_attribute_", "").SplitSafe("_"); + if (ids.Count() > 3) + { + if (productId == 0 || (productId != 0 && productId == ids[0].ToInt())) + { + collection.Add(item.key, item.value); + } + } + } + } - foreach (var item in items) + if (attributes != null && attributes.Count > 0) { - var name = AttributeFormatedName(item[1], item[2], item[0], item.Count > 4 ? item[4] : 0); + var items = attributes.Where(i => i.Count > 3); + + if (productId != 0) + items = items.Where(i => i[0] == productId); - collection.Add(name, item[3].ToString()); + foreach (var item in items) + { + var name = AttributeFormatedName(item[1], item[2], item[0], item.Count > 4 ? item[4] : 0); + + collection.Add(name, item[3].ToString()); + } } } @@ -88,7 +112,7 @@ public static string CreateSelectedAttributesXml(this NameValueCollection collec var ctrlAttributes = collection[controlId]; if (ctrlAttributes.HasValue()) { - int selectedAttributeId = int.Parse(ctrlAttributes); + var selectedAttributeId = ctrlAttributes.SplitSafe(",").SafeGet(0).ToInt(); if (selectedAttributeId > 0) selectedAttributes = productAttributeParser.AddProductAttribute(selectedAttributes, attribute, selectedAttributeId.ToString()); } @@ -102,7 +126,7 @@ public static string CreateSelectedAttributesXml(this NameValueCollection collec { foreach (var item in cblAttributes.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - int selectedAttributeId = int.Parse(item); + var selectedAttributeId = item.SplitSafe(",").SafeGet(0).ToInt(); if (selectedAttributeId > 0) selectedAttributes = productAttributeParser.AddProductAttribute(selectedAttributes, attribute, selectedAttributeId.ToString()); } diff --git a/src/Libraries/SmartStore.Services/Filter/FilterService.cs b/src/Libraries/SmartStore.Services/Filter/FilterService.cs index d3e1ceeaac..507b1f2fed 100644 --- a/src/Libraries/SmartStore.Services/Filter/FilterService.cs +++ b/src/Libraries/SmartStore.Services/Filter/FilterService.cs @@ -350,6 +350,20 @@ from a in attributes return criterias; } + private void AddChildCategoryIds(List result, int categoryId) + { + var ids = _categoryService.GetAllCategoriesByParentCategoryId(categoryId).Select(x => x.Id); + + foreach (var id in ids) + { + if (!result.Contains(id)) + { + result.Add(id); + AddChildCategoryIds(result, id); + } + } + } + public virtual List Deserialize(string jsonData) { if (jsonData.HasValue()) @@ -389,9 +403,7 @@ public virtual FilterProductContext CreateFilterProductContext(string filter, in if (_catalogSettings.ShowProductsFromSubcategories) { - context.CategoryIds.AddRange( - _categoryService.GetAllCategoriesByParentCategoryId(categoryID).Select(x => x.Id) - ); + AddChildCategoryIds(context.CategoryIds, categoryID); } int languageId = _services.WorkContext.WorkingLanguage.Id; diff --git a/src/Libraries/SmartStore.Services/ICommonServices.cs b/src/Libraries/SmartStore.Services/ICommonServices.cs index ee16a33432..f9b3043bf0 100644 --- a/src/Libraries/SmartStore.Services/ICommonServices.cs +++ b/src/Libraries/SmartStore.Services/ICommonServices.cs @@ -86,4 +86,37 @@ IDateTimeHelper DateTimeHelper get; } } + + public static class ICommonServicesExtensions + { + public static TService Resolve(this ICommonServices services) + { + return services.Container.Resolve(); + } + + public static TService Resolve(this ICommonServices services, object serviceKey) + { + return services.Container.ResolveKeyed(serviceKey); + } + + public static TService ResolveNamed(this ICommonServices services, string serviceName) + { + return services.Container.ResolveNamed(serviceName); + } + + public static object Resolve(this ICommonServices services, Type serviceType) + { + return services.Resolve(null, serviceType); + } + + public static object Resolve(this ICommonServices services, object serviceKey, Type serviceType) + { + return services.Container.ResolveKeyed(serviceKey, serviceType); + } + + public static object ResolveNamed(this ICommonServices services, string serviceName, Type serviceType) + { + return services.Container.ResolveNamed(serviceName, serviceType); + } + } } diff --git a/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs b/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs index 918ad1be88..6195301c03 100644 --- a/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs +++ b/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs @@ -117,9 +117,10 @@ LocaleStringResource GetLocaleStringResourceByName(string resourceName, int lang /// Language /// XML document /// Prefix for resource key name - /// Specifies whether resource should be inserted or updated (or both) - /// Specifies whether user touched resources should also be updated - void ImportResourcesFromXml(Language language, + /// Specifies whether resource should be inserted or updated (or both) + /// Specifies whether user touched resources should also be updated + /// The number of processed (added or updated) resource entries + int ImportResourcesFromXml(Language language, XmlDocument xmlDocument, string rootKey = null, bool sourceIsPlugin = false, @@ -131,11 +132,14 @@ void ImportResourcesFromXml(Language language, /// /// codehint: sm-add /// Descriptor of the plugin - /// Load them into list rather than into database + /// Load them into the passed list rather than into database /// Specifies whether user touched resources should also be updated /// Import only files for particular languages - void ImportPluginResourcesFromXml(PluginDescriptor pluginDescriptor, - List forceToList = null, bool updateTouchedResources = true, IList filterLanguages = null); + void ImportPluginResourcesFromXml( + PluginDescriptor pluginDescriptor, + IList targetList = null, + bool updateTouchedResources = true, + IList filterLanguages = null); /// /// Flattens all nested LocaleResource child nodes into a new document diff --git a/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs index 871afe9887..070d1fbfe5 100644 --- a/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs @@ -62,12 +62,14 @@ public partial interface ILocalizedEntityService /// Key selector /// Locale value /// Language ID - void SaveLocalizedValue(T entity, + void SaveLocalizedValue( + T entity, Expression> keySelector, string localeValue, int languageId) where T : BaseEntity, ILocalizedEntity; - void SaveLocalizedValue(T entity, + void SaveLocalizedValue( + T entity, Expression> keySelector, TPropType localeValue, int languageId) where T : BaseEntity, ILocalizedEntity; diff --git a/src/Libraries/SmartStore.Services/Localization/LanguageService.cs b/src/Libraries/SmartStore.Services/Localization/LanguageService.cs index bc8e7d0999..db75218b54 100644 --- a/src/Libraries/SmartStore.Services/Localization/LanguageService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LanguageService.cs @@ -110,18 +110,19 @@ public virtual IList GetAllLanguages(bool showHidden = false, int stor { var query = _languageRepository.Table; if (!showHidden) - query = query.Where(l => l.Published); - query = query.OrderBy(l => l.DisplayOrder); + query = query.Where(x => x.Published); + query = query.OrderBy(x => x.DisplayOrder); return query.ToList(); }); - //store mapping + // store mapping if (storeId > 0) { languages = languages .Where(l => _storeMappingService.Authorize(l, storeId)) .ToList(); } + return languages; } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs index 8610072e92..c6ac869c27 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs @@ -16,6 +16,8 @@ using SmartStore.Core.Events; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; +using SmartStore.Core.Localization; +using System.Globalization; namespace SmartStore.Services.Localization { @@ -172,6 +174,7 @@ orderby lsr.ResourceName if (localeStringResource == null && logIfNotFound) _logger.Warning(string.Format("Resource string ({0}) not found. Language ID = {1}", resourceName, languageId)); + return localeStringResource; } @@ -270,6 +273,7 @@ orderby l.ResourceName string cacheKey = string.Format(LOCALESTRINGRESOURCES_ALL_KEY, languageId); var dict = _cacheManager.Get(cacheKey, () => { + // TODO: make result cacheable in distributed cache (IDictionary) var result = new ConcurrentDictionary>(8, 2000, StringComparer.CurrentCultureIgnoreCase); if (forceAll || _localizationSettings.LoadAllLocaleRecordsOnStartup) { @@ -415,13 +419,183 @@ public virtual string ExportResourcesToXml(Language language) return stringWriter.ToString(); } + public virtual void ImportPluginResourcesFromXml( + PluginDescriptor pluginDescriptor, + IList targetList = null, + bool updateTouchedResources = true, + IList filterLanguages = null) + { + var directory = new DirectoryInfo(Path.Combine(pluginDescriptor.OriginalAssemblyFile.Directory.FullName, "Localization")); + + if (!directory.Exists) + return; + + if (targetList == null && updateTouchedResources) + { + DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); + } + + var unprocessedLanguages = new List(); + + var defaultLanguageId = _languageService.GetDefaultLanguageId(); + var languages = filterLanguages ?? _languageService.GetAllLanguages(true); + + string code = null; + foreach (var language in languages) + { + code = ImportPluginResourcesForLanguage( + language, + null, + directory, + pluginDescriptor.ResourceRootKey, + targetList, + updateTouchedResources, + false); + + if (code == null) + { + unprocessedLanguages.Add(language); + } + } + + if (filterLanguages == null && unprocessedLanguages.Count > 0) + { + // There were unprocessed languages (no corresponding resource file could be found). + // In order for GetResource() to be able to gracefully fallback to the default language's resources, + // we need to import resources for the current default language.... + var processedLanguages = languages.Except(unprocessedLanguages).ToList(); + if (!processedLanguages.Any(x => x.Id == defaultLanguageId)) + { + // ...but only if no resource file could be mapped to the default language before, + // namely because in this case the following operation would be redundant. + var defaultLanguage = _languageService.GetLanguageById(_languageService.GetDefaultLanguageId()); + if (defaultLanguage != null) + { + ImportPluginResourcesForLanguage( + defaultLanguage, + "en-us", + directory, + pluginDescriptor.ResourceRootKey, + targetList, + updateTouchedResources, + true); + } + } + } + } + /// - /// Import language resources from XML file + /// Resolves a resource file for the specified language and processes the import /// /// Language - /// XML document - /// Prefix for resource key name - public virtual void ImportResourcesFromXml( + /// The culture code of the processed resource file + private string ImportPluginResourcesForLanguage( + Language language, + string fileCode, + DirectoryInfo directory, + string resourceRootKey, + IList targetList, + bool updateTouchedResources, + bool canFallBackToAnyResourceFile) + { + var fileNamePattern = "resources.{0}.xml"; + + var codeCandidates = GetResourceFileCodeCandidates( + fileCode ?? language.LanguageCulture, + directory, + canFallBackToAnyResourceFile); + + string path = null; + string code = null; + + foreach (var candidate in codeCandidates) + { + var pathCandidate = Path.Combine(directory.FullName, fileNamePattern.FormatInvariant(candidate)); + if (File.Exists(pathCandidate)) + { + code = candidate; + path = pathCandidate; + break; + } + } + + if (code != null) + { + var doc = new XmlDocument(); + + doc.Load(path); + doc = FlattenResourceFile(doc); + + if (targetList == null) + { + ImportResourcesFromXml(language, doc, resourceRootKey, true, updateTouchedResources: updateTouchedResources); + } + else + { + var nodes = doc.SelectNodes(@"//Language/LocaleResource"); + foreach (XmlNode node in nodes) + { + var valueNode = node.SelectSingleNode("Value"); + var res = new LocaleStringResource() + { + ResourceName = node.Attributes["Name"].InnerText.Trim(), + ResourceValue = (valueNode == null ? "" : valueNode.InnerText), + LanguageId = language.Id, + IsFromPlugin = true + }; + + if (res.ResourceName.HasValue()) + { + targetList.Add(res); + } + } + } + } + + return code; + } + + private IEnumerable GetResourceFileCodeCandidates(string code, DirectoryInfo directory, bool canFallBackToAnyResourceFile) + { + // exact match (de-DE) + yield return code; + + // neutral culture (de) + var ci = CultureInfo.GetCultureInfo(code); + if (ci.Parent != null && !ci.IsNeutralCulture) + { + code = ci.Parent.Name; + yield return code; + } + + var rgFileName = new Regex("^resources.(.+?).xml$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // any other region with same language (de-*) + foreach (var fi in directory.EnumerateFiles("resources.{0}-*.xml".FormatInvariant(code), SearchOption.TopDirectoryOnly)) + { + code = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(code)) + { + yield return code; + yield break; + } + } + + if (canFallBackToAnyResourceFile) + { + foreach (var fi in directory.EnumerateFiles("resources.*.xml", SearchOption.TopDirectoryOnly)) + { + code = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(code)) + { + yield return code; + yield break; + } + } + } + } + + public virtual int ImportResourcesFromXml( Language language, XmlDocument xmlDocument, string rootKey = null, @@ -429,12 +603,16 @@ public virtual void ImportResourcesFromXml( ImportModeFlags mode = ImportModeFlags.Insert | ImportModeFlags.Update, bool updateTouchedResources = false) { - using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false, forceNoTracking: true, hooksEnabled: false)) { var toAdd = new List(); var toUpdate = new List(); var nodes = xmlDocument.SelectNodes(@"//Language/LocaleResource"); + var resources = language.LocaleStringResources.ToDictionarySafe(x => x.ResourceName, StringComparer.OrdinalIgnoreCase); + + LocaleStringResource resource; + foreach (var xel in nodes.Cast()) { string name = xel.GetAttribute("Name").TrimSafe(); @@ -452,18 +630,23 @@ public virtual void ImportResourcesFromXml( name = "{0}.{1}".FormatWith(rootKey, name); } + resource = null; + // do not use "Insert"/"Update" methods because they clear cache // let's bulk insert - var resource = language.LocaleStringResources.Where(x => x.ResourceName.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); - if (resource != null) + //var resource = language.LocaleStringResources.Where(x => x.ResourceName.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (resources.TryGetValue(name, out resource)) { if (mode.HasFlag(ImportModeFlags.Update)) { if (updateTouchedResources || !resource.IsTouched.GetValueOrDefault()) { - resource.ResourceValue = value; - resource.IsTouched = null; - toUpdate.Add(resource); + if (value != resource.ResourceValue) + { + resource.ResourceValue = value; + resource.IsTouched = null; + toUpdate.Add(resource); + } } } } @@ -472,7 +655,7 @@ public virtual void ImportResourcesFromXml( if (mode.HasFlag(ImportModeFlags.Insert)) { toAdd.Add( - new LocaleStringResource() + new LocaleStringResource { LanguageId = language.Id, ResourceName = name, @@ -483,84 +666,25 @@ public virtual void ImportResourcesFromXml( } } - _lsrRepository.AutoCommitEnabled = true; - _lsrRepository.InsertRange(toAdd, 500); - toAdd.Clear(); - - _lsrRepository.UpdateRange(toUpdate); - toUpdate.Clear(); - - //clear cache - _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_PATTERN_KEY); - } - } - - /// - /// Import plugin resources from xml files in plugin's localization directory. - /// - /// Descriptor of the plugin - /// Load them into list rather than into database - /// Specifies whether user touched resources should also be updated - /// Import only files for particular languages - public virtual void ImportPluginResourcesFromXml(PluginDescriptor pluginDescriptor, - List forceToList = null, bool updateTouchedResources = true, IList filterLanguages = null) - { - string pluginDir = pluginDescriptor.OriginalAssemblyFile.Directory.FullName; - string localizationDir = Path.Combine(pluginDir, "Localization"); + //_lsrRepository.AutoCommitEnabled = true; - if (!System.IO.Directory.Exists(localizationDir)) - return; + if (toAdd.Any() || toUpdate.Any()) + { + _lsrRepository.InsertRange(toAdd); + toAdd.Clear(); - if (forceToList == null && updateTouchedResources) - DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); + _lsrRepository.UpdateRange(toUpdate); + toUpdate.Clear(); - var languages = _languageService.GetAllLanguages(true); - var doc = new XmlDocument(); + int num = _lsrRepository.Context.SaveChanges(); - foreach (var filePath in System.IO.Directory.EnumerateFiles(localizationDir, "*.xml")) - { - Match match = Regex.Match(Path.GetFileName(filePath), Regex.Escape("resources.") + "(.*?)" + Regex.Escape(".xml")); - string languageCode = match.Groups[1].Value; + // clear cache + _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_PATTERN_KEY); - Language language = languages.Where(l => l.LanguageCulture.IsCaseInsensitiveEqual(languageCode)).FirstOrDefault(); - if (language != null) - { - language = _languageService.GetLanguageById(language.Id); + return num; } - if (languageCode.HasValue() && language != null) - { - if (filterLanguages != null && !filterLanguages.Any(x => x.Id == language.Id)) - { - continue; - } - - doc.Load(filePath); - doc = FlattenResourceFile(doc); - - if (forceToList == null) - { - ImportResourcesFromXml(language, doc, pluginDescriptor.ResourceRootKey, true, updateTouchedResources: updateTouchedResources); - } - else - { - var nodes = doc.SelectNodes(@"//Language/LocaleResource"); - foreach (XmlNode node in nodes) - { - var valueNode = node.SelectSingleNode("Value"); - var res = new LocaleStringResource() - { - ResourceName = node.Attributes["Name"].InnerText.Trim(), - ResourceValue = (valueNode == null ? "" : valueNode.InnerText), - LanguageId = language.Id, - IsFromPlugin = true - }; - - if (res.ResourceName.HasValue()) - forceToList.Add(res); - } - } - } + return 0; } } diff --git a/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs b/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs index fb152d8a1f..8387de3969 100644 --- a/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs +++ b/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs @@ -119,9 +119,9 @@ public virtual IPagedList GetAllLogs(DateTime? fromUtc, DateTime? toUtc, st var query = _logRepository.Table; if (fromUtc.HasValue) - query = query.Where(l => fromUtc.Value <= l.CreatedOnUtc); + query = query.Where(l => fromUtc.Value <= l.CreatedOnUtc || fromUtc.Value <= l.UpdatedOnUtc); if (toUtc.HasValue) - query = query.Where(l => toUtc.Value >= l.CreatedOnUtc); + query = query.Where(l => toUtc.Value >= l.CreatedOnUtc || toUtc.Value >= l.UpdatedOnUtc); if (logLevel.HasValue) { int logLevelId = (int)logLevel.Value; @@ -129,7 +129,8 @@ public virtual IPagedList GetAllLogs(DateTime? fromUtc, DateTime? toUtc, st } if (!String.IsNullOrEmpty(message)) query = query.Where(l => l.ShortMessage.Contains(message) || l.FullMessage.Contains(message)); - query = query.OrderByDescending(l => l.CreatedOnUtc); + + query = query.OrderByDescending(l => l.UpdatedOnUtc).ThenByDescending(l => l.CreatedOnUtc); if (minFrequency > 0) query = query.Where(l => l.Frequency >= minFrequency); diff --git a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs index 4392ca231a..b67c04fbe5 100644 --- a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs +++ b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs @@ -43,7 +43,7 @@ public void Execute(TaskExecutionContext ctx) { using (var scope = new DbContextScope(autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) { - var pictures = _pictureRepository.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToList(); + var pictures = _pictureRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToList(); foreach (var picture in pictures) { _pictureService.DeletePicture(picture); diff --git a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs index 70ee52c637..979051d1d6 100644 --- a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs +++ b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using SmartStore.Core.Data; using SmartStore.Core.Domain.DataExchange; @@ -37,25 +36,23 @@ public static string[] DefaultKeyFields } } - public void Execute(IImportExecuteContext context) + public void Execute(ImportExecuteContext context) { var utcNow = DateTime.UtcNow; var currentStoreId = _services.StoreContext.CurrentStore.Id; - var toAdd = new List(); - var toUpdate = new List(); - using (var scope = new DbContextScope(ctx: _services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) { - var segmenter = context.GetSegmenter(); + var segmenter = context.DataSegmenter; context.Result.TotalRecords = segmenter.TotalRows; while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) { - var batch = segmenter.CurrentBatch; + var batch = segmenter.GetCurrentBatch(); - _subscriptionRepository.Context.DetachAll(false); + // Perf: detach all entities + _subscriptionRepository.Context.DetachEntities(false); context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); @@ -63,12 +60,22 @@ public void Execute(IImportExecuteContext context) { try { + var active = true; var email = row.GetDataValue("Email"); - var active = row.GetDataValue("Active"); var storeId = row.GetDataValue("StoreId"); if (storeId == 0) + { storeId = currentStoreId; + } + + if (row.HasDataValue("Active") && row.TryGetDataValue("Active", out active)) + { + } + else + { + active = true; // default + } if (email.IsEmpty()) { @@ -122,32 +129,25 @@ public void Execute(IImportExecuteContext context) StoreId = storeId }; - toAdd.Add(subscription); - ++context.Result.NewRecords; + _subscriptionRepository.Insert(subscription); + context.Result.NewRecords++; } else { subscription.Active = active; - toUpdate.Add(subscription); - ++context.Result.ModifiedRecords; + _subscriptionRepository.Update(subscription); + context.Result.ModifiedRecords++; } - - // insert new subscribers - _subscriptionRepository.AutoCommitEnabled = true; - _subscriptionRepository.InsertRange(toAdd, 500); - toAdd.Clear(); - - // update modified subscribers - _subscriptionRepository.UpdateRange(toUpdate); - toUpdate.Clear(); } catch (Exception exception) { context.Result.AddError(exception.ToAllMessages(), row.GetRowInfo()); } - } - } + } // for + + _subscriptionRepository.Context.SaveChanges(); + } // while } } } diff --git a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs b/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs index 6a86297fe7..d0ff917259 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs @@ -927,15 +927,24 @@ public virtual void AddReturnRequestTokens(IList tokens, ReturnRequest re public virtual void AddGiftCardTokens(IList tokens, GiftCard giftCard) { - var order = giftCard.PurchasedWithOrderItem.Order; - var remainingAmount = _currencyService.ConvertCurrency(giftCard.GetGiftCardRemainingAmount(), order.CurrencyRate); + var order = (giftCard.PurchasedWithOrderItem != null ? giftCard.PurchasedWithOrderItem.Order : null); + + if (order != null) + { + var remainingAmount = _currencyService.ConvertCurrency(giftCard.GetGiftCardRemainingAmount(), order.CurrencyRate); + + tokens.Add(new Token("GiftCard.RemainingAmount", _priceFormatter.FormatPrice(remainingAmount, true, false))); + } + else + { + tokens.Add(new Token("GiftCard.RemainingAmount", "")); + } tokens.Add(new Token("GiftCard.SenderName", giftCard.SenderName)); tokens.Add(new Token("GiftCard.SenderEmail", giftCard.SenderEmail)); tokens.Add(new Token("GiftCard.RecipientName", giftCard.RecipientName)); tokens.Add(new Token("GiftCard.RecipientEmail", giftCard.RecipientEmail)); tokens.Add(new Token("GiftCard.Amount", _priceFormatter.FormatPrice(giftCard.Amount, true, false))); - tokens.Add(new Token("GiftCard.RemainingAmount", _priceFormatter.FormatPrice(remainingAmount, true, false))); tokens.Add(new Token("GiftCard.CouponCode", giftCard.GiftCardCouponCode)); var giftCardMesage = !String.IsNullOrWhiteSpace(giftCard.Message) ? diff --git a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs index d9a340c451..7e266a98b1 100644 --- a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs @@ -4,18 +4,19 @@ using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; namespace SmartStore.Services.Orders { - /// - /// Checkout attribute service - /// - public partial class CheckoutAttributeService : ICheckoutAttributeService + /// + /// Checkout attribute service + /// + public partial class CheckoutAttributeService : ICheckoutAttributeService { #region Constants - private const string CHECKOUTATTRIBUTES_ALL_KEY = "SmartStore.checkoutattribute.all-{0}"; + private const string CHECKOUTATTRIBUTES_ALL_KEY = "SmartStore.checkoutattribute.all-{0}-{1}"; private const string CHECKOUTATTRIBUTEVALUES_ALL_KEY = "SmartStore.checkoutattributevalue.all-{0}"; private const string CHECKOUTATTRIBUTES_PATTERN_KEY = "SmartStore.checkoutattribute."; private const string CHECKOUTATTRIBUTEVALUES_PATTERN_KEY = "SmartStore.checkoutattributevalue."; @@ -28,7 +29,8 @@ public partial class CheckoutAttributeService : ICheckoutAttributeService private readonly IRepository _checkoutAttributeRepository; private readonly IRepository _checkoutAttributeValueRepository; - private readonly IEventPublisher _eventPublisher; + private readonly IRepository _storeMappingRepository; + private readonly IEventPublisher _eventPublisher; private readonly ICacheManager _cacheManager; #endregion @@ -45,25 +47,31 @@ public partial class CheckoutAttributeService : ICheckoutAttributeService public CheckoutAttributeService(ICacheManager cacheManager, IRepository checkoutAttributeRepository, IRepository checkoutAttributeValueRepository, - IEventPublisher eventPublisher) + IRepository storeMappingRepository, + IEventPublisher eventPublisher) { _cacheManager = cacheManager; _checkoutAttributeRepository = checkoutAttributeRepository; _checkoutAttributeValueRepository = checkoutAttributeValueRepository; + _storeMappingRepository = storeMappingRepository; _eventPublisher = eventPublisher; - } - #endregion + this.QuerySettings = DbQuerySettings.Default; + } - #region Methods + #endregion - #region Checkout attributes + public DbQuerySettings QuerySettings { get; set; } - /// - /// Deletes a checkout attribute - /// - /// Checkout attribute - public virtual void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute) + #region Methods + + #region Checkout attributes + + /// + /// Deletes a checkout attribute + /// + /// Checkout attribute + public virtual void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute) { if (checkoutAttribute == null) throw new ArgumentNullException("checkoutAttribute"); @@ -77,26 +85,56 @@ public virtual void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute) _eventPublisher.EntityDeleted(checkoutAttribute); } - /// - /// Gets all checkout attributes - /// - /// Checkout attribute collection - public virtual IList GetAllCheckoutAttributes(bool showHidden = false) + /// + /// Gets checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attributes query + public virtual IQueryable GetCheckoutAttributes(int storeId = 0, bool showHidden = false) + { + var query = _checkoutAttributeRepository.Table; + + if (!showHidden) + query = query.Where(x => x.IsActive); + + if (storeId > 0 && !QuerySettings.IgnoreMultiStore) + { + query = + from x in query + join sm in _storeMappingRepository.Table on new { c1 = x.Id, c2 = "CheckoutAttribute" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into x_sm + from sm in x_sm.DefaultIfEmpty() + where !x.LimitedToStores || storeId == sm.StoreId + select x; + + query = + from x in query + group x by x.Id into grp + orderby grp.Key + select grp.FirstOrDefault(); + } + + query = query.OrderBy(x => x.DisplayOrder); + + return query; + } + + /// + /// Gets all checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attribute collection + public virtual IList GetAllCheckoutAttributes(int storeId = 0, bool showHidden = false) { - string key = CHECKOUTATTRIBUTES_ALL_KEY.FormatInvariant(showHidden); + string key = CHECKOUTATTRIBUTES_ALL_KEY.FormatInvariant(storeId, showHidden); return _cacheManager.Get(key, () => { - var query = _checkoutAttributeRepository.Table; + var query = GetCheckoutAttributes(storeId, showHidden); - if (!showHidden) - query = query.Where(x => x.IsActive); - - query = query.OrderBy(x => x.DisplayOrder); - - var checkoutAttributes = query.ToList(); - return checkoutAttributes; - }); + return query.ToList(); + }); } /// diff --git a/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs b/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs index 2ceb9c3396..9a520ca28b 100644 --- a/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs @@ -1,12 +1,13 @@ using System.Collections.Generic; +using System.Linq; using SmartStore.Core.Domain.Orders; namespace SmartStore.Services.Orders { - /// - /// Checkout attribute service - /// - public partial interface ICheckoutAttributeService + /// + /// Checkout attribute service + /// + public partial interface ICheckoutAttributeService { #region Checkout attributes @@ -16,11 +17,21 @@ public partial interface ICheckoutAttributeService /// Checkout attribute void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute); - /// - /// Gets all checkout attributes - /// - /// Checkout attribute collection - IList GetAllCheckoutAttributes(bool showHidden = false); + /// + /// Gets checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attributes query + IQueryable GetCheckoutAttributes(int storeId = 0, bool showHidden = false); + + /// + /// Gets all checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attribute collection + IList GetAllCheckoutAttributes(int storeId = 0, bool showHidden = false); /// /// Gets a checkout attribute diff --git a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs index 7c633a920f..0993597da4 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs @@ -242,12 +242,8 @@ protected void AwardRewardPoints(Order order, decimal? amount = null) if (order.RewardPointsWereAdded) return; - // Truncate increases the risk of inaccuracy of rounding - //int points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); - - // why are points awarded for OrderTotal? wouldn't be OrderSubtotalInclTax better? - - int points = (int)Math.Round((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); + // Trucate same as Floor for positive amounts + int points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); if (points == 0) return; @@ -1139,7 +1135,7 @@ public virtual PlaceOrderResult PlaceOrder( var bundleItemSubTotal = _taxService.GetProductPrice(childItem.Item.Product, _priceCalculationService.GetSubTotal(childItem, true), out taxRate); var attributesInfo = _productAttributeFormatter.FormatAttributes(childItem.Item.Product, childItem.Item.AttributesXml, order.Customer, - renderPrices: false, allowHyperlinks: false); + renderPrices: false, allowHyperlinks: true); childItem.BundleItemData.ToOrderData(listBundleData, bundleItemSubTotal, childItem.Item.AttributesXml, attributesInfo); } diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs index 259b323910..27f0331be4 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs @@ -879,7 +879,7 @@ public virtual IList GetShoppingCartWarnings(IList !x.IsIdle); - _hasActiveAcl = query.Any(); + _hasActiveAcl = _aclRecordRepository.TableUntracked.Any(x => !x.IsIdle); } return _hasActiveAcl.Value; } diff --git a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs index 67337bbb66..29c29d13ef 100644 --- a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs +++ b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs @@ -243,7 +243,7 @@ public static string ValidateSeName(this T entity, } } - // validate and alter sename if it could be interpreted as SEO code + // validate and alter SeName if it could be interpreted as SEO code if (LocalizationHelper.IsValidCultureCode(seName)) { if (seName.Length == 2) diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index fa1dde780b..38fe4a7bda 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -68,7 +68,7 @@ public virtual IList GetUrlRecordsByIds(int[] urlRecordIds) if (urlRecordIds == null || urlRecordIds.Length == 0) return new List(); - var urlRecords = _urlRecordRepository + var urlRecords = _urlRecordRepository.Table .Where(x => urlRecordIds.Contains(x.Id)) .ToList(); diff --git a/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs b/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs index 51f0a22966..4f6db7fa9c 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs @@ -317,11 +317,11 @@ public virtual decimal GetShoppingCartTotalWeight(IList(SystemCustomerAttributeNames.CheckoutAttributes, _genericAttributeService); @@ -332,6 +332,7 @@ public virtual decimal GetShoppingCartTotalWeight(IList ..\..\packages\ncrontab.2.0.0\lib\net20\NCrontab.dll - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True False @@ -174,23 +174,26 @@ + + - - - - - + + + + + - + + @@ -204,19 +207,19 @@ - + - - - + + + - + - - + + diff --git a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs index 04ff4206ff..c2ccb86f17 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs @@ -141,12 +141,12 @@ public virtual void SaveStoreMappings(T entity, int[] selectedStoreIds) where { if (selectedStoreIds != null && selectedStoreIds.Contains(store.Id)) { - if (existingStoreMappings.Where(sm => sm.StoreId == store.Id).Count() == 0) + if (!existingStoreMappings.Any(x => x.StoreId == store.Id)) InsertStoreMapping(entity, store.Id); } else { - var storeMappingToDelete = existingStoreMappings.Where(sm => sm.StoreId == store.Id).FirstOrDefault(); + var storeMappingToDelete = existingStoreMappings.FirstOrDefault(x => x.StoreId == store.Id); if (storeMappingToDelete != null) DeleteStoreMapping(storeMappingToDelete); } @@ -185,7 +185,7 @@ public virtual void InsertStoreMapping(T entity, int storeId) where T : BaseE int entityId = entity.Id; string entityName = typeof(T).Name; - var storeMapping = new StoreMapping() + var storeMapping = new StoreMapping { EntityId = entityId, EntityName = entityName, diff --git a/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs index 25243b0ad5..c35b4b7128 100644 --- a/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs +++ b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs @@ -59,14 +59,16 @@ public static string GetFriendlyDescription(string expression) Use24HourTimeFormat = Thread.CurrentThread.CurrentUICulture.DateTimeFormat.AMDesignator.IsEmpty() }; - try - { - return ExpressionDescriptor.GetDescription(expression, options); - } - catch + if (expression.HasValue()) { - return null; + try + { + return ExpressionDescriptor.GetDescription(expression, options); + } + catch { } } + + return "?"; } } diff --git a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs index e5b6db97de..3329fbf3a7 100644 --- a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs +++ b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs @@ -17,24 +17,35 @@ namespace SmartStore.Services.Tasks public class InitializeSchedulerFilter : IAuthorizationFilter { private readonly static object s_lock = new object(); + private static int s_errCount; private static bool s_initializing = false; public void OnAuthorization(AuthorizationContext filterContext) { - lock (s_lock) + if (filterContext == null || filterContext.HttpContext == null) + return; + + var request = filterContext.HttpContext.Request; + if (request == null) + return; + + if (filterContext.IsChildAction) + return; + + lock (s_lock) { if (!s_initializing) { s_initializing = true; - var logger = EngineContext.Current.Resolve(); + ILogger logger = EngineContext.Current.Resolve(); + ITaskScheduler taskScheduler = EngineContext.Current.Resolve(); try { var taskService = EngineContext.Current.Resolve(); var storeService = EngineContext.Current.Resolve(); var eventPublisher = EngineContext.Current.Resolve(); - var taskScheduler = EngineContext.Current.Resolve(); var tasks = taskService.GetAllTasks(true); taskService.CalculateFutureSchedules(tasks, true /* isAppStart */); @@ -59,11 +70,23 @@ public void OnAuthorization(AuthorizationContext filterContext) } catch (Exception ex) { + s_errCount++; + s_initializing = false; logger.Error("Error while initializing Task Scheduler", ex); } finally { - GlobalFilters.Filters.Remove(this); + var tooManyFailures = s_errCount >= 10; + + if (tooManyFailures || (taskScheduler != null && taskScheduler.IsActive)) + { + GlobalFilters.Filters.Remove(this); + } + + if (tooManyFailures && logger != null) + { + logger.Warning("Stopped trying to initialize the Task Scheduler: too many failed attempts in succession (10+). Maybe uncommenting the setting 'sm:TaskSchedulerBaseUrl' in web.config solves the problem?"); + } } } } diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs index fc7ce73e42..8fd93c2956 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs @@ -1,21 +1,22 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using Autofac; +using SmartStore.Core; using SmartStore.Core.Async; using SmartStore.Core.Data; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Tasks; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Services.Customers; -using SmartStore.Core; -using SmartStore.Core.Domain.Customers; -using System.Diagnostics; -using System.Collections.Generic; namespace SmartStore.Services.Tasks { - public class TaskExecutor : ITaskExecutor + public class TaskExecutor : ITaskExecutor { private readonly IScheduleTaskService _scheduledTaskService; private readonly IDbContext _dbContext; @@ -42,11 +43,13 @@ public TaskExecutor( this._taskResolver = taskResolver; Logger = NullLogger.Instance; - } + T = NullLocalizer.Instance; + } public ILogger Logger { get; set; } + public Localizer T { get; set; } - public void Execute( + public void Execute( ScheduleTask task, IDictionary taskParameters = null, bool throwOnError = false) @@ -126,12 +129,17 @@ public void Execute( instance.Execute(ctx); } - catch (Exception ex) + catch (Exception exception) { faulted = true; - canceled = ex is OperationCanceledException; - Logger.Error(string.Format("Error while running scheduled task '{0}'. {1}", task.Name, ex.Message), ex); - lastError = ex.Message.Truncate(995, "..."); + canceled = exception is OperationCanceledException; + lastError = exception.Message.Truncate(995, "..."); + + if (canceled) + Logger.Warning(T("Admin.System.ScheduleTasks.Cancellation", task.Name), exception); + else + Logger.Error(string.Concat(T("Admin.System.ScheduleTasks.RunningError", task.Name), ": ", exception.Message), exception); + if (throwOnError) { throw; diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs index bd2734c136..0e055029a3 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs @@ -13,6 +13,7 @@ using SmartStore.Collections; using SmartStore.Core.Infrastructure; using SmartStore.Core.Caching; +using SmartStore.Core; namespace SmartStore.Services.Tasks { @@ -48,7 +49,7 @@ public string BaseUrl { CheckUrl(value); _baseUrl = value.TrimEnd('/', '\\'); - } + } } public void Start() @@ -135,7 +136,7 @@ public void RunSingleTask(int scheduleTaskId, IDictionary taskPa query = qs.ToString(); } - CallEndpoint("{0}/Execute/{1}{2}".FormatInvariant(_baseUrl, scheduleTaskId, query)); + CallEndpoint(new Uri("{0}/Execute/{1}{2}".FormatInvariant(_baseUrl, scheduleTaskId, query))); } private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) @@ -153,7 +154,7 @@ private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) _intervalFixed = true; } - CallEndpoint(_baseUrl + "/Sweep"); + CallEndpoint(new Uri(_baseUrl + "/Sweep")); } } finally @@ -162,25 +163,25 @@ private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) } } - protected internal virtual void CallEndpoint(string url) + protected internal virtual void CallEndpoint(Uri uri) { if (_shuttingDown) return; - - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = "SmartStore.NET"; - req.Method = "POST"; + + var req = WebHelper.CreateHttpRequestForSafeLocalCall(uri); + req.Method = "POST"; req.ContentType = "text/plain"; req.ContentLength = 0; + req.Timeout = 10000; // 10 sec. string authToken = CreateAuthToken(); req.Headers.Add("X-AUTH-TOKEN", authToken); - req.GetResponseAsync().ContinueWith(t => - { + req.GetResponseAsync().ContinueWith(t => + { if (t.IsFaulted) { - HandleException(t.Exception, url); + HandleException(t.Exception, uri); _errCount++; if (_errCount >= 10) { @@ -204,32 +205,39 @@ protected internal virtual void CallEndpoint(string url) response.Dispose(); } - }); - } + }); + } - private void HandleException(AggregateException exception, string url) + private void HandleException(AggregateException exception, Uri uri) { using (var logger = new TraceLogger()) { - string msg = "Error while calling TaskScheduler endpoint '{0}'.".FormatInvariant(url); + string msg = "Error while calling TaskScheduler endpoint '{0}'.".FormatInvariant(uri.OriginalString); var wex = exception.InnerExceptions.OfType().FirstOrDefault(); if (wex == null) { - logger.Error(msg, exception); + logger.Error(msg, exception.InnerException); + } + else if (wex.Response == null) + { + logger.Error(msg, wex); } else { using (var response = wex.Response as HttpWebResponse) { - msg += " HTTP {0}, {1}".FormatCurrent((int)response.StatusCode, response.StatusDescription); + if (response != null) + { + msg += " HTTP {0}, {1}".FormatCurrent((int)response.StatusCode, response.StatusDescription); + } logger.Error(msg); } } } } - private void CheckUrl(string url) + private void CheckUrl(string url) { if (!url.IsWebUrl()) { diff --git a/src/Libraries/SmartStore.Services/packages.config b/src/Libraries/SmartStore.Services/packages.config index 7f5b29df78..d078b22719 100644 --- a/src/Libraries/SmartStore.Services/packages.config +++ b/src/Libraries/SmartStore.Services/packages.config @@ -13,7 +13,7 @@ - + diff --git a/src/Plugins/SmartStore.AmazonPay/Description.txt b/src/Plugins/SmartStore.AmazonPay/Description.txt index 3e203915e6..8237c4a7ad 100644 --- a/src/Plugins/SmartStore.AmazonPay/Description.txt +++ b/src/Plugins/SmartStore.AmazonPay/Description.txt @@ -1,6 +1,6 @@ FriendlyName: Pay with Amazon SystemName: SmartStore.AmazonPay -Version: 2.5.0 +Version: 2.6.0 Group: Payment MinAppVersion: 2.5.0 Author: SmartStore AG diff --git a/src/Plugins/SmartStore.AmazonPay/web.config b/src/Plugins/SmartStore.AmazonPay/web.config index 0e57dde0c1..3fd5b09a23 100644 --- a/src/Plugins/SmartStore.AmazonPay/web.config +++ b/src/Plugins/SmartStore.AmazonPay/web.config @@ -1,131 +1,131 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.Clickatell/Description.txt b/src/Plugins/SmartStore.Clickatell/Description.txt index d5e561a2fc..177b509843 100644 --- a/src/Plugins/SmartStore.Clickatell/Description.txt +++ b/src/Plugins/SmartStore.Clickatell/Description.txt @@ -1,7 +1,7 @@ FriendlyName: Clickatell SMS Provider SystemName: SmartStore.Clickatell Group: Mobile -Version: 2.5.0 +Version: 2.6.0 MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.Clickatell.dll diff --git a/src/Plugins/SmartStore.Clickatell/web.config b/src/Plugins/SmartStore.Clickatell/web.config index 3e1b370f5d..ba87d0f098 100644 --- a/src/Plugins/SmartStore.Clickatell/web.config +++ b/src/Plugins/SmartStore.Clickatell/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs index 91f0a57419..4e4cb6d96d 100644 --- a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs @@ -1,12 +1,5 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Web.Mvc; -using System.Reflection; using Autofac; -using Autofac.Core; using Autofac.Integration.Mvc; -using SmartStore.Core.Plugins; using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; using SmartStore.Core.Infrastructure.DependencyManagement; @@ -16,13 +9,13 @@ namespace SmartStore.DevTools { - public class DependencyRegistrar : IDependencyRegistrar + public class DependencyRegistrar : IDependencyRegistrar { public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { builder.RegisterType().As().InstancePerRequest(); - if (isActiveModule) + if (isActiveModule && DataSettings.DatabaseIsInstalled()) { // intercept ALL public store controller actions builder.RegisterType().AsActionFilterFor(); diff --git a/src/Plugins/SmartStore.DevTools/Description.txt b/src/Plugins/SmartStore.DevTools/Description.txt index fff9df57de..b169d8b16a 100644 --- a/src/Plugins/SmartStore.DevTools/Description.txt +++ b/src/Plugins/SmartStore.DevTools/Description.txt @@ -1,7 +1,7 @@ FriendlyName: SmartStore.NET Developer Tools (MiniProfiler and other goodies) SystemName: SmartStore.DevTools Group: Developer -Version: 2.5.0 +Version: 2.6.0 MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.DevTools.dll diff --git a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs index e1c5693558..c2e585d0eb 100644 --- a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs +++ b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs @@ -1,5 +1,6 @@ using System; using System.Web; +using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; using StackExchange.Profiling; @@ -43,6 +44,11 @@ private static bool ShouldProfile(HttpApplication app) if (app.Context == null || app.Context.Request == null) return false; + if (!DataSettings.DatabaseIsInstalled()) + { + return false; + } + var url = app.Context.Request.AppRelativeCurrentExecutionFilePath; if (url.StartsWith("~/admin", StringComparison.InvariantCultureIgnoreCase) || url.StartsWith("~/mini-profiler", StringComparison.InvariantCultureIgnoreCase) || url.StartsWith("~/bundles", StringComparison.InvariantCultureIgnoreCase)) { diff --git a/src/Plugins/SmartStore.DevTools/Web.config b/src/Plugins/SmartStore.DevTools/Web.config index e1ac213bd8..b076d239ac 100644 --- a/src/Plugins/SmartStore.DevTools/Web.config +++ b/src/Plugins/SmartStore.DevTools/Web.config @@ -1,135 +1,135 @@ - + -
+
- - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + diff --git a/src/Plugins/SmartStore.DiscountRules/Description.txt b/src/Plugins/SmartStore.DiscountRules/Description.txt index 2a17b4fcd9..f4bb29d728 100644 --- a/src/Plugins/SmartStore.DiscountRules/Description.txt +++ b/src/Plugins/SmartStore.DiscountRules/Description.txt @@ -2,7 +2,7 @@ Description: Contains common discount requirement rule providers like "Billing country is", "Customer role is", "Had spent amount" etc. Group: Marketing SystemName: SmartStore.DiscountRules -Version: 2.5.0 +Version: 2.6.0 MinAppVersion: 2.5.0 DisplayOrder: 0 FileName: SmartStore.DiscountRules.dll diff --git a/src/Plugins/SmartStore.DiscountRules/Providers/HadSpentAmountRule.cs b/src/Plugins/SmartStore.DiscountRules/Providers/HadSpentAmountRule.cs index b67d2a7efd..69d7f2614b 100644 --- a/src/Plugins/SmartStore.DiscountRules/Providers/HadSpentAmountRule.cs +++ b/src/Plugins/SmartStore.DiscountRules/Providers/HadSpentAmountRule.cs @@ -1,15 +1,16 @@ using System; +using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Logging; using SmartStore.Core.Plugins; -using SmartStore.Services.Discounts; +using SmartStore.DiscountRules.Settings; +using SmartStore.Services.Catalog; using SmartStore.Services.Customers; +using SmartStore.Services.Discounts; using SmartStore.Services.Orders; -using SmartStore.Services.Catalog; using SmartStore.Services.Tax; -using SmartStore.Core.Localization; -using SmartStore.DiscountRules.Settings; -using Newtonsoft.Json; namespace SmartStore.DiscountRules { @@ -21,15 +22,18 @@ public partial class HadSpentAmountRule : DiscountRequirementRuleBase private readonly IOrderService _orderService; private readonly IPriceCalculationService _priceCalculationService; private readonly ITaxService _taxService; + private readonly ILogger _logger; public HadSpentAmountRule( IOrderService orderService, IPriceCalculationService priceCalculationService, - ITaxService taxService) + ITaxService taxService, + ILogger logger) { - this._orderService = orderService; - this._priceCalculationService = priceCalculationService; - this._taxService = taxService; + _orderService = orderService; + _priceCalculationService = priceCalculationService; + _taxService = taxService; + _logger = logger; } public override bool CheckRequirement(CheckDiscountRequirementRequest request) @@ -88,14 +92,34 @@ private bool CheckTotalHistoryRequirement(CheckDiscountRequirementRequest reques private bool CheckCurrentSubTotalRequirement(CheckDiscountRequirementRequest request) { - var cartItems = request.Customer.GetCartItems(ShoppingCartType.ShoppingCart, request.Store.Id); + var spentAmount = decimal.Zero; - decimal spentAmount = decimal.Zero; - decimal taxRate = decimal.Zero; - foreach (var sci in cartItems) + try { - // includeDiscounts == true produces a stack overflow! - spentAmount += sci.Item.Quantity * _taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetUnitPrice(sci, false), out taxRate); + var taxRate = decimal.Zero; + var cartItems = request.Customer.GetCartItems(ShoppingCartType.ShoppingCart, request.Store.Id); + + foreach (var cartItem in cartItems) + { + var product = cartItem.Item.Product; + Dictionary mergedValuesClone = null; + + // we must reapply merged values because CheckCurrentSubTotalRequirement uses price calculation and is called by it itself. + // this can cause wrong discount calculation if the cart contains a product several times. + if (product.MergedDataValues != null) + mergedValuesClone = new Dictionary(product.MergedDataValues); + + // includeDiscounts == true produces a stack overflow! + spentAmount += cartItem.Item.Quantity * _taxService.GetProductPrice(product, _priceCalculationService.GetUnitPrice(cartItem, false), out taxRate); + + if (mergedValuesClone != null) + product.MergedDataValues = new Dictionary(mergedValuesClone); + } + } + catch (Exception exception) + { + _logger.Error(exception); + return false; } return spentAmount >= request.DiscountRequirement.SpentAmount; diff --git a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj index 813caac6a0..dc162de8da 100644 --- a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj +++ b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj @@ -85,8 +85,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/Plugins/SmartStore.DiscountRules/packages.config b/src/Plugins/SmartStore.DiscountRules/packages.config index 669284b1aa..0325f33297 100644 --- a/src/Plugins/SmartStore.DiscountRules/packages.config +++ b/src/Plugins/SmartStore.DiscountRules/packages.config @@ -4,5 +4,5 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.DiscountRules/web.config b/src/Plugins/SmartStore.DiscountRules/web.config index 3e1b370f5d..ba87d0f098 100644 --- a/src/Plugins/SmartStore.DiscountRules/web.config +++ b/src/Plugins/SmartStore.DiscountRules/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs index abc3a709bd..5a99884991 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs @@ -138,10 +138,10 @@ private ActionResult LoginInternal(string returnUrl, bool verifyResponse) if (result.Result != null) return result.Result; - return HttpContext.Request.IsAuthenticated ? - new RedirectResult(!string.IsNullOrEmpty(returnUrl) ? returnUrl : "~/") : + return HttpContext.Request.IsAuthenticated ? + RedirectToReferrer(returnUrl, "~/") : new RedirectResult(Url.LogOn(returnUrl)); - } + } public ActionResult Login(string returnUrl) { diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs new file mode 100644 index 0000000000..5f0960101b --- /dev/null +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Data; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Web; +using System.Web.Script.Serialization; +using DotNetOpenAuth.AspNet; +using DotNetOpenAuth.AspNet.Clients; +using Newtonsoft.Json; + +namespace SmartStore.FacebookAuth.Core +{ + /// + /// + internal class FacebookOAuth2Client : OAuth2Client + { + /// + /// The authorization endpoint. + /// + private const string AuthorizationEndpoint = "https://www.facebook.com/v2.8/dialog/oauth"; + /// + /// The token endpoint. + /// + private const string TokenEndpoint = "https://graph.facebook.com/v2.8/oauth/access_token"; + /// + /// The user info endpoint. + /// + private const string UserInfoEndpoint = "https://graph.facebook.com/v2.8/me"; + /// + /// The app id. + /// + private readonly string _appId; + /// + /// The app secret. + /// + private readonly string _appSecret; + + /// + /// The requested scopes. + /// + private readonly string[] _requestedScopes; + + + /// + /// Creates a new Facebook OAuth2 client, requesting the default "email" scope. + /// + /// The Facebook App Id + /// The Facebook App Secret + public FacebookOAuth2Client(string appId, string appSecret) + : this(appId, appSecret, new[] { "email" }) { } + + /// + /// Creates a new Facebook OAuth2 client. + /// + /// The Facebook App Id + /// The Facebook App Secret + /// One or more requested scopes, passed without the base URI. + public FacebookOAuth2Client(string appId, string appSecret, params string[] requestedScopes) + : base("facebook") + { + if (string.IsNullOrWhiteSpace(appId)) + throw new ArgumentNullException("appId"); + + if (string.IsNullOrWhiteSpace(appSecret)) + throw new ArgumentNullException("appSecret"); + + if (requestedScopes == null) + throw new ArgumentNullException("requestedScopes"); + + if (requestedScopes.Length == 0) + throw new ArgumentException("One or more scopes must be requested.", "requestedScopes"); + + _appId = appId; + _appSecret = appSecret; + _requestedScopes = requestedScopes; + } + + public override void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + string redirectUrl = this.GetServiceLoginUrl(returnUrl).AbsoluteUri; + context.Response.Redirect(redirectUrl, endResponse: true); + } + + public new AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + throw new NoNullAllowedException(); + } + + public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl) + { + string code = context.Request.QueryString["code"]; + if (string.IsNullOrEmpty(code)) + { + return AuthenticationResult.Failed; + } + + string accessToken = this.QueryAccessToken(returnPageUrl, code); + if (accessToken == null) + { + return AuthenticationResult.Failed; + } + + IDictionary userData = this.GetUserData(accessToken); + if (userData == null) + { + return AuthenticationResult.Failed; + } + + string id = userData["id"]; + string name; + + // Some oAuth providers do not return value for the 'username' attribute. + // In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id' + if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name)) + { + name = id; + } + + // add the access token to the user data dictionary just in case page developers want to use it + userData["accesstoken"] = accessToken; + + return new AuthenticationResult( + isSuccessful: true, provider: this.ProviderName, providerUserId: id, userName: name, extraData: userData); + } + + protected override Uri GetServiceLoginUrl(Uri returnUrl) + { + var state = string.IsNullOrEmpty(returnUrl.Query) ? string.Empty : returnUrl.Query.Substring(1); + + return BuildUri(AuthorizationEndpoint, new NameValueCollection + { + { "client_id", _appId }, + { "scope", string.Join(" ", _requestedScopes) }, + { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, + { "state", state }, + }); + } + + protected override IDictionary GetUserData(string accessToken) + { + var uri = BuildUri(UserInfoEndpoint, new NameValueCollection { { "access_token", accessToken } }); + + var webRequest = (HttpWebRequest)WebRequest.Create(uri); + + using (var webResponse = webRequest.GetResponse()) + using (var stream = webResponse.GetResponseStream()) + { + if (stream == null) + return null; + + using (var textReader = new StreamReader(stream)) + { + var json = textReader.ReadToEnd(); + var extraData = JsonConvert.DeserializeObject>(json); + var data = extraData.ToDictionary(x => x.Key, x => x.Value.ToString()); + + data.Add("picture", string.Format("https://graph.facebook.com/{0}/picture", data["id"])); + + return data; + } + } + } + + public string QueryAccessTokenByCode(Uri returnUrl, string authorizationCode) + { + return this.QueryAccessToken(returnUrl, authorizationCode); + } + + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) + { + var uri = BuildUri(TokenEndpoint, new NameValueCollection + { + { "code", authorizationCode }, + { "client_id", _appId }, + { "client_secret", _appSecret }, + { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, + }); + + var webRequest = (HttpWebRequest)WebRequest.Create(uri); + string accessToken = null; + HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse(); + + // handle response from FB + // this will not be a url with params like the first request to get the 'code' + Encoding rEncoding = Encoding.GetEncoding(response.CharacterSet); + + using (StreamReader sr = new StreamReader(response.GetResponseStream(), rEncoding)) + { + var serializer = new JavaScriptSerializer(); + var jsonObject = serializer.DeserializeObject(sr.ReadToEnd()); + var jConvert = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(jsonObject)); + + Dictionary desirializedJsonObject = JsonConvert.DeserializeObject>(jConvert.ToString()); + accessToken = desirializedJsonObject["access_token"].ToString(); + } + return accessToken; + } + + private static Uri BuildUri(string baseUri, NameValueCollection queryParameters) + { + var keyValuePairs = queryParameters.AllKeys.Select(k => HttpUtility.UrlEncode(k) + "=" + HttpUtility.UrlEncode(queryParameters[k])); + var qs = String.Join("&", keyValuePairs); + + var builder = new UriBuilder(baseUri) { Query = qs }; + return builder.Uri; + } + + /// + /// Facebook works best when return data be packed into a "state" parameter. + /// This should be called before verifying the request, so that the url is rewritten to support this. + /// + public static void RewriteRequest() + { + var ctx = HttpContext.Current; + + var stateString = HttpUtility.UrlDecode(ctx.Request.QueryString["state"]); + if (stateString == null || !stateString.Contains("__provider__=facebook")) + return; + + var q = HttpUtility.ParseQueryString(stateString); + q.Add(ctx.Request.QueryString); + q.Remove("state"); + + ctx.RewritePath(ctx.Request.Path + "?" + q); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs index 5f89fe62cc..378599e6d5 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs @@ -27,7 +27,7 @@ public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer private readonly HttpContextBase _httpContext; private readonly ICommonServices _services; - private FacebookClient _facebookApplication; + private FacebookOAuth2Client _facebookApplication; #endregion @@ -50,7 +50,7 @@ public FacebookProviderAuthorizer(IExternalAuthorizer authorizer, #region Utilities - private FacebookClient FacebookApplication + private FacebookOAuth2Client FacebookApplication { get { @@ -58,7 +58,7 @@ private FacebookClient FacebookApplication { var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); - _facebookApplication = new FacebookClient(settings.ClientKeyIdentifier, settings.ClientSecret); + _facebookApplication = new FacebookOAuth2Client(settings.ClientKeyIdentifier, settings.ClientSecret); } return _facebookApplication; @@ -93,9 +93,12 @@ private AuthorizeState VerifyAuthentication(string returnUrl) } var state = new AuthorizeState(returnUrl, OpenAuthenticationStatus.Error); - var error = authResult.Error != null ? authResult.Error.Message : "Unknown error"; - state.AddError(error); - return state; + + state.AddError(authResult.Error != null + ? authResult.Error.Message + : _services.Localization.GetResource("Admin.Common.UnknownError")); + + return state; } private string GetEmailFromFacebook(string accessToken) diff --git a/src/Plugins/SmartStore.FacebookAuth/Description.txt b/src/Plugins/SmartStore.FacebookAuth/Description.txt index 6e028c02d4..d60a7a363b 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Description.txt +++ b/src/Plugins/SmartStore.FacebookAuth/Description.txt @@ -1,7 +1,7 @@ FriendlyName: Facebook SystemName: SmartStore.FacebookAuth Group: Security -Version: 2.5.0 +Version: 2.6.0.1 MinAppVersion: 2.5.0 DisplayOrder: 5 FileName: SmartStore.FacebookAuth.dll diff --git a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj index 734ef16423..85992eb451 100644 --- a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj +++ b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj @@ -121,8 +121,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -179,6 +180,7 @@ Properties\AssemblyVersionInfo.cs + diff --git a/src/Plugins/SmartStore.FacebookAuth/packages.config b/src/Plugins/SmartStore.FacebookAuth/packages.config index 7d02692d69..2cd39833bf 100644 --- a/src/Plugins/SmartStore.FacebookAuth/packages.config +++ b/src/Plugins/SmartStore.FacebookAuth/packages.config @@ -15,5 +15,5 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/web.config b/src/Plugins/SmartStore.FacebookAuth/web.config index 6b304e839a..68523a15bc 100644 --- a/src/Plugins/SmartStore.FacebookAuth/web.config +++ b/src/Plugins/SmartStore.FacebookAuth/web.config @@ -1,120 +1,120 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt index 908e329118..59bf21b018 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt +++ b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt @@ -1,7 +1,7 @@ FriendlyName: Google Analytics SystemName: SmartStore.GoogleAnalytics Group: Analytics -Version: 2.5.0 +Version: 2.6.0 MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.GoogleAnalytics.dll diff --git a/src/Plugins/SmartStore.GoogleAnalytics/web.config b/src/Plugins/SmartStore.GoogleAnalytics/web.config index 32347fc005..c0f82db177 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/web.config +++ b/src/Plugins/SmartStore.GoogleAnalytics/web.config @@ -1,115 +1,115 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs index b6c3684093..2dbc226f57 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs @@ -7,17 +7,13 @@ public class AdminMenu : AdminMenuProvider { protected override void BuildMenuCore(TreeNode pluginsNode) { - var root = pluginsNode.SelectNode(x => x.Value.Id == "promotion-feeds"); - if (root == null) - return; - var menuItem = new MenuItem().ToBuilder() .Text("Google Merchant Center") .ResKey("Plugins.FriendlyName.SmartStore.GoogleMerchantCenter") .Action("ConfigurePlugin", "Plugin", new { systemName = GoogleMerchantCenterFeedPlugin.SystemName, area = "Admin" }) .ToItem(); - root.Append(menuItem); + pluginsNode.Prepend(menuItem); } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt index d30ed161f9..6f343b6b69 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt @@ -1,7 +1,7 @@ FriendlyName: Google Merchant Center (GMC) feed SystemName: SmartStore.GoogleMerchantCenter Group: Marketing -Version: 2.5.0 +Version: 2.6.0.1 MinAppVersion: 2.5.0 Author: SmartStore AG DisplayOrder: 1 diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs index 3a5cc536f6..3edd31e361 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -153,7 +153,7 @@ public override string FileExtension get { return "XML"; } } - protected override void Export(IExportExecuteContext context) + protected override void Export(ExportExecuteContext context) { dynamic currency = context.Currency; string measureWeightSystemKey = ""; @@ -177,9 +177,9 @@ protected override void Export(IExportExecuteContext context) writer.WriteElementString("link", "http://base.google.com/base/"); writer.WriteElementString("description", "Information about products"); - while (context.Abort == DataExchangeAbortion.None && context.Segmenter.ReadNextSegment()) + while (context.Abort == DataExchangeAbortion.None && context.DataSegmenter.ReadNextSegment()) { - var segment = context.Segmenter.CurrentSegment; + var segment = context.DataSegmenter.CurrentSegment; int[] productIds = segment.Select(x => (int)((dynamic)x).Id).ToArray(); var googleProducts = _googleFeedService.GetGoogleProductRecords(productIds); @@ -202,14 +202,18 @@ protected override void Export(IExportExecuteContext context) string category = (gmc == null ? null : gmc.Taxonomy); string productType = product._CategoryPath; string mainImageUrl = product._MainPictureUrl; - var specialPrice = product._SpecialPrice as decimal?; var price = (decimal)product.Price; + var uniqueId = (string)product._UniqueId; string brand = product._Brand; string gtin = product.Gtin; string mpn = product.ManufacturerPartNumber; string condition = "new"; string availability = "in stock"; + var specialPrice = product._FutureSpecialPrice as decimal?; + if (!specialPrice.HasValue) + specialPrice = product._SpecialPrice; + if (category.IsEmpty()) category = config.DefaultGoogleCategory; @@ -244,7 +248,7 @@ protected override void Export(IExportExecuteContext context) } } - writer.WriteElementString("g", "id", _googleNamespace, entity.Id.ToString()); + writer.WriteElementString("g", "id", _googleNamespace, uniqueId); writer.WriteStartElement("title"); writer.WriteCData(((string)product.Name).Truncate(70).RemoveInvalidXmlChars()); @@ -295,13 +299,17 @@ protected override void Export(IExportExecuteContext context) writer.WriteElementString("g", "availability_date", _googleNamespace, availabilityDate); } - if (config.SpecialPrice && specialPrice.HasValue && entity.SpecialPriceStartDateTimeUtc.HasValue && entity.SpecialPriceEndDateTimeUtc.HasValue) + if (config.SpecialPrice && specialPrice.HasValue) { - var specialPriceDate = "{0}/{1}".FormatInvariant( - entity.SpecialPriceStartDateTimeUtc.Value.ToString(dateFormat), entity.SpecialPriceEndDateTimeUtc.Value.ToString(dateFormat)); + writer.WriteElementString("g", "sale_price", _googleNamespace, specialPrice.Value.FormatInvariant() + " " + (string)currency.CurrencyCode); + + if (entity.SpecialPriceStartDateTimeUtc.HasValue && entity.SpecialPriceEndDateTimeUtc.HasValue) + { + var specialPriceDate = "{0}/{1}".FormatInvariant( + entity.SpecialPriceStartDateTimeUtc.Value.ToString(dateFormat), entity.SpecialPriceEndDateTimeUtc.Value.ToString(dateFormat)); - writer.WriteElementString("g", "sale_price", _googleNamespace, price.FormatInvariant() + " " + (string)currency.CurrencyCode); - writer.WriteElementString("g", "sale_price_effective_date", _googleNamespace, specialPriceDate); + writer.WriteElementString("g", "sale_price_effective_date", _googleNamespace, specialPriceDate); + } price = (product._RegularPrice as decimal?) ?? price; } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj index f027f7dd39..f743627997 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj @@ -102,8 +102,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md b/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md index 65f8637800..2c1f7000cf 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md @@ -1,5 +1,14 @@ #Release Notes +##Google Merchant Center (GMC) 2.6.0.1 + ###Bugfixes + * Id should be unique when exporting attribute combinations as products + * No special price exported when the special price period was not specified + +##Google Merchant Center (GMC) 2.5.0.1 +###Bugfixes +* GMC feed does not generate the sale price if the sale price is set for a future date + ##Google Merchant Center (GMC) 2.2.0.5 ###New Features * Supports GMC fields: multipack, bundle, adult, energy efficiency class and custom label (0 to 4) diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config b/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config index 4966c1bd2c..3527aa9a5c 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config @@ -8,5 +8,5 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/web.config b/src/Plugins/SmartStore.GoogleMerchantCenter/web.config index 3e1b370f5d..ba87d0f098 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/web.config +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.OfflinePayment/Description.txt b/src/Plugins/SmartStore.OfflinePayment/Description.txt index 0b09221978..db6f0241f9 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Description.txt +++ b/src/Plugins/SmartStore.OfflinePayment/Description.txt @@ -2,7 +2,7 @@ Description: Contains common offline payment methods like Direct Debit, Invoice, Prepayment etc. Group: Payment SystemName: SmartStore.OfflinePayment -Version: 2.5.0 +Version: 2.6.0 MinAppVersion: 2.5.0 DisplayOrder: 0 FileName: SmartStore.OfflinePayment.dll diff --git a/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj b/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj index 2f5089d229..9083d62e99 100644 --- a/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj +++ b/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj @@ -91,8 +91,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/Plugins/SmartStore.OfflinePayment/packages.config b/src/Plugins/SmartStore.OfflinePayment/packages.config index ff044a8c8f..5669aa5ceb 100644 --- a/src/Plugins/SmartStore.OfflinePayment/packages.config +++ b/src/Plugins/SmartStore.OfflinePayment/packages.config @@ -6,5 +6,5 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/web.config b/src/Plugins/SmartStore.OfflinePayment/web.config index 3e1b370f5d..ba87d0f098 100644 --- a/src/Plugins/SmartStore.OfflinePayment/web.config +++ b/src/Plugins/SmartStore.OfflinePayment/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.PayPal/Content/favicon.png b/src/Plugins/SmartStore.PayPal/Content/favicon.png new file mode 100644 index 0000000000..aa29944a07 Binary files /dev/null and b/src/Plugins/SmartStore.PayPal/Content/favicon.png differ diff --git a/src/Plugins/SmartStore.PayPal/Content/images/logo200x53.png b/src/Plugins/SmartStore.PayPal/Content/images/logo200x53.png deleted file mode 100644 index a23fcb7a0b..0000000000 Binary files a/src/Plugins/SmartStore.PayPal/Content/images/logo200x53.png and /dev/null differ diff --git a/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css b/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css index 39ce66293f..06e549454c 100644 --- a/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css +++ b/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css @@ -11,13 +11,6 @@ #paypal-checkout #paypal-express-button { cursor: pointer; } -.configure-paypal-direct .logo, -.configure-paypal-express .logo, -.configure-paypal-standard .logo -.paypal-standard-public .logo{ - width: 200px; - height: 53px; -} .paypal-standard-public { margin-bottom: 10px; } diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs index 203c28693d..06d2c94600 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs @@ -3,7 +3,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Net; using System.Text; using System.Web; using System.Web.Mvc; @@ -86,37 +85,6 @@ protected PaymentStatus GetPaymentStatus(string paymentStatus, string pendingRea return result; } - protected Dictionary GetSecurityProtocols() - { - var dic = new Dictionary(); - - foreach (SecurityProtocolType protocol in Enum.GetValues(typeof(SecurityProtocolType))) - { - string friendlyName = null; - switch (protocol) - { - case SecurityProtocolType.Ssl3: - friendlyName = "SSL 3.0"; - break; - case SecurityProtocolType.Tls: - friendlyName = "TLS 1.0"; - break; - case SecurityProtocolType.Tls11: - friendlyName = "TLS 1.1"; - break; - case SecurityProtocolType.Tls12: - friendlyName = "TLS 1.2"; - break; - default: - friendlyName = protocol.ToString().ToUpper(); - break; - } - - dic.Add(protocol, friendlyName); - } - return dic; - } - protected bool VerifyIPN(PayPalSettingsBase settings, string formString, out Dictionary values) { // settings: multistore context not possible here. we need the custom value to determine what store it is. diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs index 9460321d80..abdf2df189 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs @@ -55,7 +55,7 @@ public ActionResult Configure() model.TransactModeValues = TransactModeValues(settings.TransactMode); - model.AvailableSecurityProtocols = GetSecurityProtocols() + model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols() .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) .ToList(); diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs index dc0271f69d..776d1a210a 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs @@ -4,10 +4,8 @@ using System.Net; using System.Text; using System.Web.Mvc; -using System.Web.Routing; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Discounts; -using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Logging; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; @@ -112,7 +110,7 @@ public ActionResult Configure() model.TransactModeValues = TransactModeValues(settings.TransactMode); - model.AvailableSecurityProtocols = GetSecurityProtocols() + model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols() .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) .ToList(); diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs new file mode 100644 index 0000000000..7a835f3d19 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs @@ -0,0 +1,408 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web; +using System.Web.Mvc; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Html; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Models; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.PayPal.Validators; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Customers; +using SmartStore.Services.Directory; +using SmartStore.Services.Localization; +using SmartStore.Services.Payments; +using SmartStore.Services.Tax; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Plugins; +using SmartStore.Web.Framework.Security; +using SmartStore.Web.Framework.Settings; + +namespace SmartStore.PayPal.Controllers +{ + public class PayPalPlusController : PayPalRestApiControllerBase + { + private readonly HttpContextBase _httpContext; + private readonly PluginMediator _pluginMediator; + private readonly IGenericAttributeService _genericAttributeService; + private readonly IPaymentService _paymentService; + private readonly ITaxService _taxService; + private readonly ICurrencyService _currencyService; + private readonly IPriceFormatter _priceFormatter; + + public PayPalPlusController( + HttpContextBase httpContext, + PluginMediator pluginMediator, + IPayPalService payPalService, + IGenericAttributeService genericAttributeService, + IPaymentService paymentService, + ITaxService taxService, + ICurrencyService currencyService, + IPriceFormatter priceFormatter) : base( + PayPalPlusProvider.SystemName, + payPalService) + { + _httpContext = httpContext; + _pluginMediator = pluginMediator; + _genericAttributeService = genericAttributeService; + _paymentService = paymentService; + _taxService = taxService; + _currencyService = currencyService; + _priceFormatter = priceFormatter; + } + + private string GetPaymentMethodName(Provider provider) + { + if (provider != null) + { + var name = _pluginMediator.GetLocalizedFriendlyName(provider.Metadata); + + if (name.IsEmpty()) + name = provider.Metadata.FriendlyName; + + if (name.IsEmpty()) + name = provider.Metadata.SystemName; + + return name; + } + return ""; + } + + private string GetPaymentFee(Provider provider, List cart) + { + var paymentMethodAdditionalFee = provider.Value.GetAdditionalHandlingFee(cart); + var rateBase = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, Services.WorkContext.CurrentCustomer); + var rate = _currencyService.ConvertFromPrimaryStoreCurrency(rateBase, Services.WorkContext.WorkingCurrency); + + if (rate != decimal.Zero) + { + return _priceFormatter.FormatPaymentMethodAdditionalFee(rate, true); + } + return ""; + } + + private PayPalPlusCheckoutModel.ThirdPartyPaymentMethod GetThirdPartyPaymentMethodModel( + Provider provider, + PayPalPlusPaymentSettings settings, + Store store) + { + var model = new PayPalPlusCheckoutModel.ThirdPartyPaymentMethod(); + model.MethodName = GetPaymentMethodName(provider); + model.RedirectUrl = Url.Action("CheckoutReturn", "PayPalPlus", new { area = Plugin.SystemName, systemName = provider.Metadata.SystemName }, store.SslEnabled ? "https" : "http"); + + try + { + if (settings.DisplayPaymentMethodDescription) + { + // not the short description, the full description is intended for frontend + var paymentMethod = _paymentService.GetPaymentMethodBySystemName(provider.Metadata.SystemName); + if (paymentMethod != null) + { + var description = paymentMethod.GetLocalized(x => x.FullDescription); + if (description.HasValue()) + { + description = HtmlUtils.ConvertHtmlToPlainText(description); + description = HtmlUtils.StripTags(HttpUtility.HtmlDecode(description)); + + if (description.HasValue()) + model.Description = description.EncodeJsString(); + } + } + } + } + catch { } + + try + { + if (settings.DisplayPaymentMethodLogo && provider.Metadata.PluginDescriptor != null && store.SslEnabled) + { + var brandImageUrl = _pluginMediator.GetBrandImageUrl(provider.Metadata); + if (brandImageUrl.HasValue()) + { + if (brandImageUrl.StartsWith("~")) + brandImageUrl = brandImageUrl.Substring(1); + + var uri = new UriBuilder(Uri.UriSchemeHttps, Request.Url.Host, -1, brandImageUrl); + model.ImageUrl = uri.ToString(); + } + } + } + catch { } + + return model; + } + + [NonAction] + public override IList ValidatePaymentForm(FormCollection form) + { + var warnings = new List(); + return warnings; + } + + [NonAction] + public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) + { + var paymentInfo = new ProcessPaymentRequest(); + return paymentInfo; + } + + [AdminAuthorize, ChildActionOnly] + public ActionResult Configure() + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + + var model = new PayPalPlusConfigurationModel + { + ConfigGroups = T("Plugins.SmartStore.PayPal.ConfigGroups").Text.SplitSafe(";") + }; + + model.AvailableSecurityProtocols = PayPal.Services.PayPalService.GetSecurityProtocols() + .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) + .ToList(); + + // it's better to also offer inactive methods here but filter them out in frontend + var methods = _paymentService.LoadAllPaymentMethods(storeScope); + + model.AvailableThirdPartyPaymentMethods = methods + .Where(x => + x.Metadata.PluginDescriptor.SystemName != Plugin.SystemName && + !x.Value.RequiresInteraction && + (x.Metadata.PluginDescriptor.SystemName == "SmartStore.OfflinePayment" || x.Value.PaymentMethodType == PaymentMethodType.Redirection)) + .Select(x => new SelectListItem { Value = x.Metadata.SystemName, Text = GetPaymentMethodName(x) }) + .ToList(); + + + model.Copy(settings, true); + + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); + + return View(model); + } + + [HttpPost, AdminAuthorize, ChildActionOnly] + public ActionResult Configure(PayPalPlusConfigurationModel model, FormCollection form) + { + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + var oldClientId = settings.ClientId; + var oldSecret = settings.Secret; + var oldProfileId = settings.ExperienceProfileId; + + var validator = new PayPalPlusConfigValidator(Services.Localization, x => + { + return storeScope == 0 || storeDependingSettingHelper.IsOverrideChecked(settings, x, form); + }); + + validator.Validate(model, ModelState); + + if (!ModelState.IsValid) + return Configure(); + + ModelState.Clear(); + + model.Copy(settings, false); + + // credentials changed: reset profile and webhook id to avoid errors + if (!oldClientId.IsCaseInsensitiveEqual(settings.ClientId) || !oldSecret.IsCaseInsensitiveEqual(settings.Secret)) + { + if (oldProfileId.IsCaseInsensitiveEqual(settings.ExperienceProfileId)) + settings.ExperienceProfileId = null; + + settings.WebhookId = null; + } + + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + + Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); + + Services.Settings.ClearCache(); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + + return Configure(); + } + + public ActionResult PaymentInfo() + { + return new EmptyResult(); + } + + public ActionResult PaymentWall() + { + var sb = new StringBuilder(); + var store = Services.StoreContext.CurrentStore; + var customer = Services.WorkContext.CurrentCustomer; + var language = Services.WorkContext.WorkingLanguage; + var settings = Services.Settings.LoadSetting(store.Id); + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + + var pppMethod = _paymentService.GetPaymentMethodBySystemName(PayPalPlusProvider.SystemName); + var pppProvider = _paymentService.LoadPaymentMethodBySystemName(PayPalPlusProvider.SystemName, false, store.Id); + + var methods = _paymentService.LoadActivePaymentMethods(customer, cart, store.Id, null, false); + var session = _httpContext.GetPayPalSessionData(); + + var model = new PayPalPlusCheckoutModel(); + model.ThirdPartyPaymentMethods = new List(); + model.UseSandbox = settings.UseSandbox; + model.LanguageCulture = (language.LanguageCulture ?? "de_DE").Replace("-", "_"); + model.PayPalPlusPseudoMessageFlag = TempData["PayPalPlusPseudoMessageFlag"] as string; + model.PayPalFee = GetPaymentFee(pppProvider, cart); + model.HasAnyFees = model.PayPalFee.HasValue(); + + if (pppMethod != null) + { + model.FullDescription = pppMethod.GetLocalized(x => x.FullDescription, language.Id); + } + + if (customer.BillingAddress != null && customer.BillingAddress.Country != null) + { + model.BillingAddressCountryCode = customer.BillingAddress.Country.TwoLetterIsoCode; + } + + foreach (var systemName in settings.ThirdPartyPaymentMethods) + { + var provider = methods.FirstOrDefault(x => x.Metadata.SystemName == systemName); + if (provider != null) + { + var methodModel = GetThirdPartyPaymentMethodModel(provider, settings, store); + model.ThirdPartyPaymentMethods.Add(methodModel); + + var fee = GetPaymentFee(provider, cart); + if (fee.HasValue()) + model.HasAnyFees = true; + if (sb.Length > 0) + sb.Append(", "); + sb.AppendFormat("['{0}','{1}']", methodModel.MethodName.Replace("'", ""), fee); + } + } + + model.ThirdPartyFees = sb.ToString(); + + // we must create a new paypal payment each time the payment wall is rendered because otherwise patch payment can fail + // with "Item amount must add up to specified amount subtotal (or total if amount details not specified)". + session.PaymentId = null; + session.ApprovalUrl = null; + + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + var protocol = (store.SslEnabled ? "https" : "http"); + var returnUrl = Url.Action("CheckoutReturn", "PayPalPlus", new { area = Plugin.SystemName }, protocol); + var cancelUrl = Url.Action("CheckoutCancel", "PayPalPlus", new { area = Plugin.SystemName }, protocol); + + result = PayPalService.CreatePayment(settings, session, cart, PayPalPlusProvider.SystemName, returnUrl, cancelUrl); + if (result.Success && result.Json != null) + { + foreach (var link in result.Json.links) + { + if (((string)link.rel).IsCaseInsensitiveEqual("approval_url")) + { + session.PaymentId = result.Id; + session.ApprovalUrl = link.href; + break; + } + } + } + else + { + model.ErrorMessage = result.ErrorMessage; + } + } + else + { + model.ErrorMessage = result.ErrorMessage; + } + + model.ApprovalUrl = session.ApprovalUrl; + + return View(model); + } + + [HttpPost] + public ActionResult PatchShipping() + { + var store = Services.StoreContext.CurrentStore; + var customer = Services.WorkContext.CurrentCustomer; + var settings = Services.Settings.LoadSetting(store.Id); + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + var session = HttpContext.GetPayPalSessionData(); + + var apiResult = PayPalService.PatchShipping(settings, session, cart, PayPalPlusProvider.SystemName); + + return new JsonResult { Data = new { success = apiResult.Success, error = apiResult.ErrorMessage } }; + } + + public ActionResult CheckoutCompleted() + { + var instruct = _httpContext.Session[PayPalPlusProvider.CheckoutCompletedKey] as string; + + if (instruct.HasValue()) + { + return Content(instruct); + } + + return new EmptyResult(); + } + + [ValidateInput(false)] + public ActionResult CheckoutReturn(string systemName, string paymentId, string PayerID) + { + // Request.QueryString: + // paymentId: PAY-0TC88803RP094490KK4KM6AI, token: EC-5P379249AL999154U, PayerID: 5L9K773HHJLPN + + var customer = Services.WorkContext.CurrentCustomer; + var store = Services.StoreContext.CurrentStore; + var settings = Services.Settings.LoadSetting(store.Id); + var session = _httpContext.GetPayPalSessionData(); + + if (systemName.IsEmpty()) + systemName = PayPalPlusProvider.SystemName; + + if (paymentId.HasValue() && session.PaymentId.IsEmpty()) + session.PaymentId = paymentId; + + session.PayerId = PayerID; + + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, systemName, store.Id); + + var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; + if (paymentRequest == null) + { + _httpContext.Session["OrderPaymentInfo"] = new ProcessPaymentRequest + { + PaymentMethodSystemName = systemName + }; + } + + return RedirectToAction("Confirm", "Checkout", new { area = "" }); + } + + [ValidateInput(false)] + public ActionResult CheckoutCancel() + { + // Request.QueryString: + // token: EC-6JM38216F6718012L, ppp_msg: 1 + + // undocumented + var pseudoMessageFlag = Request.QueryString["ppp_msg"] as string; + + if (pseudoMessageFlag.HasValue()) + { + TempData["PayPalPlusPseudoMessageFlag"] = pseudoMessageFlag; + } + + // back to where he came from + return RedirectToAction("PaymentMethod", "Checkout", new { area = "" }); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs new file mode 100644 index 0000000000..8d5818e5d5 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs @@ -0,0 +1,177 @@ +using System; +using System.IO; +using System.Net; +using System.Web.Mvc; +using SmartStore.Core.Configuration; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; + +namespace SmartStore.PayPal.Controllers +{ + public abstract class PayPalRestApiControllerBase : PaymentControllerBase where TSetting : PayPalApiSettingsBase, ISettings, new() + { + public PayPalRestApiControllerBase( + string systemName, + IPayPalService payPalService) + { + SystemName = systemName; + PayPalService = payPalService; + } + + private string GetControllerName() + { + return GetType().Name.Replace("Controller", ""); + } + + protected string SystemName { get; private set; } + protected IPayPalService PayPalService { get; private set; } + + [AdminAuthorize] + public ActionResult UpsertExperienceProfile() + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + + var store = Services.StoreService.GetStoreById(storeScope == 0 ? Services.StoreContext.CurrentStore.Id : storeScope); + var session = new PayPalSessionData(); + + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + result = PayPalService.UpsertCheckoutExperience(settings, session, store); + if (result.Success && result.Id.HasValue()) + { + settings.ExperienceProfileId = result.Id; + Services.Settings.SaveSetting(settings, x => x.ExperienceProfileId, storeScope, false); + Services.Settings.ClearCache(); + } + } + + if (result.Success) + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + else + NotifyError(result.ErrorMessage); + + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + } + + [AdminAuthorize] + public ActionResult DeleteExperienceProfile() + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + var session = new PayPalSessionData(); + + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + result = PayPalService.DeleteCheckoutExperience(settings, session); + if (result.Success) + { + settings.ExperienceProfileId = null; + Services.Settings.SaveSetting(settings, x => x.ExperienceProfileId, storeScope, false); + Services.Settings.ClearCache(); + } + } + + if (result.Success) + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + else + NotifyError(result.ErrorMessage); + + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + } + + [AdminAuthorize] + public ActionResult CreateWebhook() + { + var settings = Services.Settings.LoadSetting(); + var session = new PayPalSessionData(); + + if (settings.WebhookId.HasValue()) + { + var unused = PayPalService.DeleteWebhook(settings, session); + + Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false); + } + + var url = Url.Action("Webhook", GetControllerName(), new { area = Plugin.SystemName }, "https"); + + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + result = PayPalService.CreateWebhook(settings, session, url); + if (result.Success) + { + settings.WebhookId = result.Id; + Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false); + } + } + + Services.Settings.ClearCache(); + + if (result.Success) + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + else + NotifyError(result.ErrorMessage); + + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + } + + [AdminAuthorize] + public ActionResult DeleteWebhook() + { + var settings = Services.Settings.LoadSetting(); + var session = new PayPalSessionData(); + + if (settings.WebhookId.HasValue()) + { + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + result = PayPalService.DeleteWebhook(settings, session); + if (result.Success) + { + settings.WebhookId = null; + Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false); + Services.Settings.ClearCache(); + } + } + + if (result.Success) + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + else + NotifyError(result.ErrorMessage); + } + + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + } + + [ValidateInput(false)] + public ActionResult Webhook() + { + HttpStatusCode result = HttpStatusCode.OK; + + try + { + string json = null; + using (var reader = new StreamReader(Request.InputStream)) + { + json = reader.ReadToEnd(); + } + + var settings = Services.Settings.LoadSetting(); + + result = PayPalService.ProcessWebhook(settings, Request.Headers, json, PayPalPlusProvider.SystemName); + } + catch (Exception exception) + { + PayPalService.LogError(exception, isWarning: true); + } + + return new HttpStatusCodeResult(result); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs index b03f8ef7d4..e007d86dfc 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs @@ -39,7 +39,7 @@ public ActionResult Configure() model.Copy(settings, true); - model.AvailableSecurityProtocols = GetSecurityProtocols() + model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols() .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) .ToList(); diff --git a/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs b/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs index d05fa4025a..95e7e8f1c3 100644 --- a/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs @@ -3,6 +3,7 @@ using SmartStore.Core.Infrastructure; using SmartStore.Core.Infrastructure.DependencyManagement; using SmartStore.PayPal.Filters; +using SmartStore.PayPal.Services; using SmartStore.Web.Controllers; namespace SmartStore.PayPal @@ -11,10 +12,21 @@ public class DependencyRegistrar : IDependencyRegistrar { public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { + builder.RegisterType().As().InstancePerRequest(); + if (isActiveModule) { builder.RegisterType().AsActionFilterFor(x => x.PaymentMethod()).InstancePerRequest(); + builder.RegisterType().AsActionFilterFor(x => x.FlyoutShoppingCart()).InstancePerRequest(); + + builder.RegisterType() + .AsActionFilterFor(x => x.PaymentMethod()) + .InstancePerRequest(); + + builder.RegisterType() + .AsActionFilterFor(x => x.Completed()) + .InstancePerRequest(); } } diff --git a/src/Plugins/SmartStore.PayPal/Description.txt b/src/Plugins/SmartStore.PayPal/Description.txt index ef3a69a027..b96ae172bb 100644 --- a/src/Plugins/SmartStore.PayPal/Description.txt +++ b/src/Plugins/SmartStore.PayPal/Description.txt @@ -1,8 +1,8 @@ FriendlyName: PayPal -Description: Provides the PayPal payment methods PayPal Express, PayPal Standard and PayPal Direct. +Description: Provides the PayPal payment methods PayPal Standard, PayPal Direct, PayPal Express and PayPal PLUS. SystemName: SmartStore.PayPal Group: Payment -Version: 2.5.0.1 +Version: 2.6.0.5 MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.PayPal.dll diff --git a/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs b/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs index 6903865152..352d70d951 100644 --- a/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs +++ b/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs @@ -1,9 +1,12 @@ using System.Net; +using System.Web; +using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; +using SmartStore.Services.Orders; namespace SmartStore.PayPal { - public static class MiscExtensions + internal static class MiscExtensions { public static string GetPayPalUrl(this PayPalSettingsBase settings) { @@ -22,5 +25,16 @@ public static HttpWebRequest GetPayPalWebRequest(this PayPalSettingsBase setting var request = (HttpWebRequest)WebRequest.Create(GetPayPalUrl(settings)); return request; } + + public static PayPalSessionData GetPayPalSessionData(this HttpContextBase httpContext, CheckoutState state = null) + { + if (state == null) + state = httpContext.GetCheckoutState(); + + if (!state.CustomProperties.ContainsKey(PayPalPlusProvider.SystemName)) + state.CustomProperties.Add(PayPalPlusProvider.SystemName, new PayPalSessionData()); + + return state.CustomProperties[PayPalPlusProvider.SystemName] as PayPalSessionData; + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs new file mode 100644 index 0000000000..7c47079830 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs @@ -0,0 +1,49 @@ +using System; +using System.Web.Mvc; +using System.Web.Routing; +using SmartStore.Core.Domain.Customers; +using SmartStore.Services; +using SmartStore.Services.Common; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal.Filters +{ + public class PayPalPlusCheckoutFilter : IActionFilter + { + private readonly ICommonServices _services; + private readonly IPaymentService _paymentService; + private readonly Lazy _genericAttributeService; + + public PayPalPlusCheckoutFilter( + ICommonServices services, + IPaymentService paymentService, + Lazy genericAttributeService) + { + _services = services; + _paymentService = paymentService; + _genericAttributeService = genericAttributeService; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) + return; + + var store = _services.StoreContext.CurrentStore; + + if (!_paymentService.IsPaymentMethodActive(PayPalPlusProvider.SystemName, store.Id)) + return; + + _genericAttributeService.Value.SaveAttribute(_services.WorkContext.CurrentCustomer, SystemCustomerAttributeNames.SelectedPaymentMethod, PayPalPlusProvider.SystemName, store.Id); + + var routeValues = new RouteValueDictionary(new { action = "PaymentWall", controller = "PayPalPlus" }); + + filterContext.Result = new RedirectToRouteResult("SmartStore.PayPalPlus", routeValues); + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusWidgetZoneFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusWidgetZoneFilter.cs new file mode 100644 index 0000000000..cbe12c7aaf --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusWidgetZoneFilter.cs @@ -0,0 +1,58 @@ +using System; +using System.Web; +using System.Web.Mvc; +using SmartStore.Web.Framework.UI; + +namespace SmartStore.PayPal.Filters +{ + public class PayPalPlusWidgetZoneFilter : IActionFilter, IResultFilter + { + private readonly Lazy _httpContext; + private readonly Lazy _widgetProvider; + + public PayPalPlusWidgetZoneFilter( + Lazy httpContext, + Lazy widgetProvider) + { + _httpContext = httpContext; + _widgetProvider = widgetProvider; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { + if (filterContext.IsChildAction) + return; + + // should only run on a full view rendering result + var result = filterContext.Result as ViewResultBase; + if (result == null) + return; + + var controller = filterContext.RouteData.Values["controller"] as string; + var action = filterContext.RouteData.Values["action"] as string; + + if (action.IsCaseInsensitiveEqual("Completed") && controller.IsCaseInsensitiveEqual("Checkout")) + { + var instruct = _httpContext.Value.Session[PayPalPlusProvider.CheckoutCompletedKey] as string; + + if (instruct.HasValue()) + { + _widgetProvider.Value.RegisterAction("checkout_completed_top", "CheckoutCompleted", "PayPalPlus", new { area = Plugin.SystemName }); + _widgetProvider.Value.RegisterAction("mobile_checkout_completed_top", "CheckoutCompleted", "PayPalPlus", new { area = Plugin.SystemName }); + } + } + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + } + } +} diff --git a/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml b/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml index 6087bfe6cd..0568ab1572 100644 --- a/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml @@ -4,9 +4,71 @@ PayPal Zahlungsarten - Stellt die PayPal-Zahlungsarten PayPal Express, PayPal Standard und PayPal Direct zur Verfügung. + Stellt die PayPal-Zahlungsarten PayPal Standard, PayPal Direct, PayPal Express und PayPal PLUS zur Verfügung. + + API Zugang;Datenaustausch;Sonstiges + + + {0}]]> + + + Mitteilung;Ereignis;Ereignis-ID;Status;Betrag;Zahlungs-ID + + + Sonstiges + + + {0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden. + + + Bitte überweisen Sie den fälligen Betrag auf folgendes Konto. + + + Referenz;Bankleitzahl;Bank;BIC;IBAN;Kontoinhaber;Kontonummer;Betrag;Zahlung fällig am;Details + + + Mitteilung wurde ignoriert, da {0} Aufträge mit der Payment-ID {1} gefunden wurden. + + + Währung {0} entspricht nicht der Leitwährung des Shops {1}. + + + Keine Weiterleitungs-URL von PayPal erhalten. + + + + Client-ID + + + Legt die API Client-ID fest. + + + Secret + + + Legt die API Secret fest. + + + Bitte geben Sie Ihre API Zugangsdaten ein. + + + Experience Profil-ID + + + Legt die Experience Profil-ID fest. Das Profil beinhaltet globale Daten wie z.B. Shop-Name und -Logo. Ein Profil braucht nur einmalig angelegt werden. Über den Button können Sie ein neues Profil erstellen oder ein vorhandenes aktualisieren. + + + Experience Profil-ID + + + Webhook-ID + + + Legt die Webhook-ID fest. Über einen Webhook sendet PayPal Statusänderungen von Zahlungen an Ihren Shop, wodurch der Zahlungsstatus Ihrer Aufträge automatisch aktualisert werden kann. + + @@ -78,10 +140,19 @@ Teilerstattung von {0} + + Zahlartgebühren + + + Rabatt + + + Angewendeter Geschenkgutschein + - - + + PayPal Express @@ -304,9 +375,6 @@ Versandgebühren - - Zahlungsgebühren - Umsatzsteuer @@ -327,5 +395,59 @@ Zahlungsgebühr: {10}]]> - + + + + PayPal PLUS + + + + + + PayPal PLUS bietet die Möglichkeit per PayPal, Kreditkarte, Lastschriftverfahren und per Rechnung zu bezahlen. +Die Bezahlung auf Rechnung steht möglicherweise nicht allen Händlern zur Verfügung. PayPal PLUS unterstützt nur den Transaktionstyp der sofortigen Buchung und ist derzeit nur in Deutschland verfügbar.

+

PayPal PLUS ersetzt die Zahlartenliste im Kassenbereich durch die PayPal Payment Wall. Sie können der Payment Wall jedoch bis zu 5 weitere Offline- bzw. Weiterleitungszahlarten hinzufügen.

+

So richten Sie PayPal PLUS ein: +

    +
  1. Legen Sie unter My Apps & Credentials eine neue REST API Anwendung an. +Unter Manage your applications finden Sie weitere Informationen zum Thema PayPal Anwendungen (Englisch).
  2. +
  3. Klicken Sie auf den Namen der Anwendung und tragen Sie Client-ID und Secret weiter unten auf dieser Seite ein. Speichern.
  4. +
  5. Optional: Klicken Sie unten bei Experience Profil-ID auf Hinzufügen, um ein Profil zu erstellen. Ihre Shop-Daten (Name, Logo etc.) werden Käufern dadurch beim Bezahlen auf den PayPal-Seiten angezeigt.
  6. +
  7. Optional: Klicken Sie unten bei Webhook-ID auf Hinzufügen, um einen Webhook zu erstellen. Über einen Webhook empfängt der Shop PayPal-Mitteilungen und aktualisiert u.U. den Zahlungsstatus Ihrer Aufträge.
  8. +

]]> +
+
+ + Wir können Ihnen leider keine PayPal PLUS Zahlungsart anbieten. Bitte versuchen Sie er später erneut. + + + Entschuldigung, das hat gerade nicht funktioniert. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode. + + + .]]> + + + Weitere Zahlarten + + + Sie können bis zu 5 weitere Zahlarten festlegen. Diese werden zusammen mit den PayPal PLUS Zahlarten im Kassenbereich angeboten. + + + Es können maximal 5 weitere Zahlarten festgelegt werden. + + + Logo der Zahlarten anzeigen + + + Legt fest, ob für die weiteren Zahlarten deren Logo angezeigt werden soll. SSL erforderlich. + + + Beschreibung der Zahlarten anzeigen + + + Legt fest, ob für die weiteren Zahlarten deren Beschreibung angezeigt werden soll. + +
+
+ \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml b/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml index 45d036a94a..700b806aa7 100644 --- a/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml @@ -4,7 +4,69 @@ PayPal Payment Methods - Provides the PayPal payment methods PayPal Express, PayPal Standard and PayPal Direct. + Provides the PayPal payment methods PayPal Standard, PayPal Direct, PayPal Express and PayPal PLUS. + + + + API access;Data exchange;Miscellaneous + + + {0}]]> + + + Message;Event;Event-ID;State;Amount;Payment-ID + + + Miscellaneous + + + The execution of the payment has been declined by PayPal. + + + {0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden. + + + Please transfer the due amount to the following bank account. + + + Reference;Bank routing number;Bank;BIC;IBAN;Account holder;Account number;Amount;Payment due date;Details + + + Message has been ignored because {0} orders with payment ID {1} found. + + + Currency {0} does not equal primary store currency {1}. + + + No redirect URL received from PayPal. + + + + Client ID + + + Specifies the API client ID. + + + Secret + + + Specifies the API secret. + + + Please enter your API credentials. + + + Experience profile ID + + + Specifies the experience profile ID. The profile contains global data like shop name and logo. A profile needs to be created only once. Using the button, you can create a new profile or update an existing one. + + + Webhook ID + + + Specifies the webhook ID. PayPal sends payment status changes via a webhook to your shop, whereby the payment status of your orders can be automatically synced. @@ -78,6 +140,15 @@ Partial refund of {0} + + Payment method fee + + + Discount + + + Giftcard Applied + @@ -304,9 +375,6 @@ Shipping fee - - Payment method fee - Sales tax @@ -328,4 +396,58 @@ Payment fee: {10}]]> + + + PayPal PLUS + + + + + + PayPal PLUS is a solution where PayPal offers PayPal, Credit Card, Direct Debit (ELV) and pay upon invoice as individual payment options on the payment selection page. +Pay upon invoice may not be available for all merchants. PayPal PLUS only supports the transaction type of instant settlement and is only available in Germany at the moment.

+

PayPal PLUS replaces the list of payment methods in checkout by the PayPal payment wall. You can add up to 5 offline or redirection payment methods to the payment wall.

+

How to set up PayPal PLUS: +

    +
  1. Create a new REST API application under My Apps & Credentials. +Go to Manage your applications to get more information about PayPal applications.
  2. +
  3. Click the name of the application and enter Client-ID and Secret below on this page. Save.
  4. +
  5. Optional: Click Add next to Experience profile ID to create a new profile. Your shop data (name, logo etc.) can now be displayed to customers when paying on PayPal pages.
  6. +
  7. Optional: Click Add next to Webhook-ID to create a webhook. Through a webhook the shop receives PayPal messages und possibly updates the payment state of your orders.
  8. +

]]> +
+
+ + Unfortunately we cannot offer you a PayPal PLUS payment method. Please try again later. + + + I'm sorry, that just didn't work. Please try again or select another payment method. + + + .]]> + + + More payment methods + + + You can set up to 5 more payment methods. These are offered along with the PayPal PLUS payment methods during the checkout process. + + + Maximum of 5 third party payment methods can be specified. + + + Display logo of other payment methods + + + Specifies whether to display the logo for other payment methods. SSL required. + + + Display description of other payment methods + + + Specifies whether to display the description for other payment methods. + +
+
+ \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs index ed2526fbc4..f81f6c2d7f 100644 --- a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs +++ b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs @@ -13,7 +13,7 @@ public abstract class ApiConfigurationModel: ModelBase { public string[] ConfigGroups { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPal.UseSandbox")] + [SmartResourceDisplayName("Plugins.Payments.PayPal.UseSandbox")] public bool UseSandbox { get; set; } [SmartResourceDisplayName("Plugins.Payments.PayPal.IpnChangesPaymentStatus")] @@ -37,6 +37,18 @@ public abstract class ApiConfigurationModel: ModelBase [SmartResourceDisplayName("Plugins.Payments.PayPal.Signature")] public string Signature { get; set; } + [SmartResourceDisplayName("Plugins.SmartStore.PayPal.ClientId")] + public string ClientId { get; set; } + + [SmartResourceDisplayName("Plugins.SmartStore.PayPal.Secret")] + public string Secret { get; set; } + + [SmartResourceDisplayName("Plugins.SmartStore.PayPal.ExperienceProfileId")] + public string ExperienceProfileId { get; set; } + + [SmartResourceDisplayName("Plugins.SmartStore.PayPal.WebhookId")] + public string WebhookId { get; set; } + [SmartResourceDisplayName("Plugins.Payments.PayPal.AdditionalFee")] public decimal AdditionalFee { get; set; } @@ -130,4 +142,55 @@ public void Copy(PayPalExpressPaymentSettings settings, bool fromSettings) } } } + + + public class PayPalPlusConfigurationModel : ApiConfigurationModel + { + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.ThirdPartyPaymentMethods")] + public List ThirdPartyPaymentMethods { get; set; } + public List AvailableThirdPartyPaymentMethods { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.DisplayPaymentMethodLogo")] + public bool DisplayPaymentMethodLogo { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.DisplayPaymentMethodDescription")] + public bool DisplayPaymentMethodDescription { get; set; } + + + public void Copy(PayPalPlusPaymentSettings settings, bool fromSettings) + { + if (fromSettings) + { + SecurityProtocol = settings.SecurityProtocol; + UseSandbox = settings.UseSandbox; + TransactMode = (int)Settings.TransactMode.AuthorizeAndCapture; + AdditionalFee = settings.AdditionalFee; + AdditionalFeePercentage = settings.AdditionalFeePercentage; + + ClientId = settings.ClientId; + Secret = settings.Secret; + ExperienceProfileId = settings.ExperienceProfileId; + WebhookId = settings.WebhookId; + ThirdPartyPaymentMethods = settings.ThirdPartyPaymentMethods; + DisplayPaymentMethodLogo = settings.DisplayPaymentMethodLogo; + DisplayPaymentMethodDescription = settings.DisplayPaymentMethodDescription; + } + else + { + settings.SecurityProtocol = SecurityProtocol; + settings.UseSandbox = UseSandbox; + settings.TransactMode = Settings.TransactMode.AuthorizeAndCapture; + settings.AdditionalFee = AdditionalFee; + settings.AdditionalFeePercentage = AdditionalFeePercentage; + + settings.ClientId = ClientId; + settings.Secret = Secret; + settings.ExperienceProfileId = ExperienceProfileId; + settings.WebhookId = WebhookId; + settings.ThirdPartyPaymentMethods = ThirdPartyPaymentMethods; + settings.DisplayPaymentMethodLogo = DisplayPaymentMethodLogo; + settings.DisplayPaymentMethodDescription = DisplayPaymentMethodDescription; + } + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Models/PayPalPlusCheckoutModel.cs b/src/Plugins/SmartStore.PayPal/Models/PayPalPlusCheckoutModel.cs new file mode 100644 index 0000000000..280d76ec24 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Models/PayPalPlusCheckoutModel.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.PayPal.Models +{ + public class PayPalPlusCheckoutModel : ModelBase + { + public bool UseSandbox { get; set; } + public string BillingAddressCountryCode { get; set; } + public string LanguageCulture { get; set; } + public string ApprovalUrl { get; set; } + public string ErrorMessage { get; set; } + public string PayPalPlusPseudoMessageFlag { get; set; } + public string FullDescription { get; set; } + public string PayPalFee { get; set; } + public string ThirdPartyFees { get; set; } + public bool HasAnyFees { get; set; } + + public List ThirdPartyPaymentMethods { get; set; } + + public class ThirdPartyPaymentMethod + { + public string RedirectUrl { get; set; } + public string MethodName { get; set; } + public string ImageUrl { get; set; } + public string Description { get; set; } + public string PaymentFee { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Plugin.cs b/src/Plugins/SmartStore.PayPal/Plugin.cs index 0883d41784..472a7f2680 100644 --- a/src/Plugins/SmartStore.PayPal/Plugin.cs +++ b/src/Plugins/SmartStore.PayPal/Plugin.cs @@ -1,4 +1,6 @@ -using SmartStore.Core.Plugins; +using System; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Services.Configuration; using SmartStore.Services.Localization; @@ -9,13 +11,16 @@ public class Plugin : BasePlugin { private readonly ISettingService _settingService; private readonly ILocalizationService _localizationService; + private readonly Lazy _payPalService; public Plugin( ISettingService settingService, - ILocalizationService localizationService) + ILocalizationService localizationService, + Lazy payPalService) { _settingService = settingService; _localizationService = localizationService; + _payPalService = payPalService; } public static string SystemName @@ -28,6 +33,7 @@ public override void Install() _settingService.SaveSetting(new PayPalExpressPaymentSettings()); _settingService.SaveSetting(new PayPalDirectPaymentSettings()); _settingService.SaveSetting(new PayPalStandardPaymentSettings()); + _settingService.SaveSetting(new PayPalPlusPaymentSettings()); _localizationService.ImportPluginResourcesFromXml(this.PluginDescriptor); @@ -36,9 +42,30 @@ public override void Install() public override void Uninstall() { + try + { + var settings = _settingService.LoadSetting(); + if (settings.WebhookId.HasValue()) + { + var session = new PayPalSessionData(); + var result = _payPalService.Value.EnsureAccessToken(session, settings); + + if (result.Success) + result = _payPalService.Value.DeleteWebhook(settings, session); + + if (!result.Success) + _payPalService.Value.LogError(null, result.ErrorMessage); + } + } + catch (Exception exception) + { + _payPalService.Value.LogError(exception); + } + _settingService.DeleteSetting(); _settingService.DeleteSetting(); _settingService.DeleteSetting(); + _settingService.DeleteSetting(); _localizationService.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs index d8bab5bc43..98c4e8e223 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs @@ -8,11 +8,11 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Tax; using SmartStore.Core.Infrastructure; using SmartStore.Core.Plugins; using SmartStore.PayPal.Controllers; using SmartStore.PayPal.PayPalSvc; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Services.Catalog; using SmartStore.Services.Common; @@ -21,6 +21,7 @@ using SmartStore.Services.Orders; using SmartStore.Services.Payments; using SmartStore.Services.Shipping; +using SmartStore.Services.Tax; namespace SmartStore.PayPal { @@ -31,7 +32,8 @@ public partial class PayPalExpressProvider : PayPalProviderBase cart) { - var result = new SetExpressCheckoutResponseType(); + var result = new SetExpressCheckoutResponseType(); var store = Services.StoreService.GetStoreById(processPaymentRequest.StoreId); + var customer = Services.WorkContext.CurrentCustomer; var settings = Services.Settings.LoadSetting(processPaymentRequest.StoreId); var payPalCurrency = GetApiCurrency(store.PrimaryStoreCurrency); - - var req = new SetExpressCheckoutReq + var excludingTax = (Services.WorkContext.GetTaxDisplayTypeFor(customer, store.Id) == TaxDisplayType.ExcludingTax); + + var req = new SetExpressCheckoutReq { SetExpressCheckoutRequest = new SetExpressCheckoutRequestType { @@ -181,45 +187,53 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq NoShipping = settings.NoShipmentAddress.ToString() }; - // populate cart - decimal itemTotal = decimal.Zero; + // populate cart + var taxRate = decimal.Zero; + var unitPriceTaxRate = decimal.Zero; + var itemTotal = decimal.Zero; var cartItems = new List(); - foreach (OrganizedShoppingCartItem item in cart) + + foreach (var item in cart) { - decimal shoppingCartUnitPriceWithDiscountBase = _priceCalculationService.GetUnitPrice(item, true); - decimal shoppingCartUnitPriceWithDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(shoppingCartUnitPriceWithDiscountBase, Services.WorkContext.WorkingCurrency); - decimal priceIncludingTier = shoppingCartUnitPriceWithDiscount; + var product = item.Item.Product; + var unitPrice = _priceCalculationService.GetUnitPrice(item, true); + var shoppingCartUnitPriceWithDiscount = excludingTax + ? _taxService.GetProductPrice(product, unitPrice, false, customer, out taxRate) + : _taxService.GetProductPrice(product, unitPrice, true, customer, out unitPriceTaxRate); - cartItems.Add(new PaymentDetailsItemType() + cartItems.Add(new PaymentDetailsItemType { - Name = item.Item.Product.Name, - Number = item.Item.Product.Sku, + Name = product.Name, + Number = product.Sku, Quantity = item.Item.Quantity.ToString(), - Amount = new BasicAmountType() // this is the per item cost + // this is the per item cost + Amount = new BasicAmountType { currencyID = payPalCurrency, - Value = (priceIncludingTier).ToString("N", new CultureInfo("en-us")) + Value = shoppingCartUnitPriceWithDiscount.FormatInvariant() } }); - itemTotal += (item.Item.Quantity * priceIncludingTier); + + itemTotal += (item.Item.Quantity * shoppingCartUnitPriceWithDiscount); }; // additional handling fee var additionalHandlingFee = GetAdditionalHandlingFee(cart); - cartItems.Add(new PaymentDetailsItemType() + cartItems.Add(new PaymentDetailsItemType { - Name = "Zahlartgebhren", + Name = T("Plugins.Payments.PayPal.PaymentMethodFee").Text, Quantity = "1", Amount = new BasicAmountType() { currencyID = payPalCurrency, - Value = (additionalHandlingFee).ToString("N", new CultureInfo("en-us")) + Value = additionalHandlingFee.FormatInvariant() } }); + itemTotal += GetAdditionalHandlingFee(cart); //shipping - decimal shippingTotal = decimal.Zero; + var shippingTotal = decimal.Zero; if (cart.RequiresShipping()) { decimal? shoppingCartShippingBase = OrderTotalCalculationService.GetShoppingCartShippingTotal(cart); @@ -254,28 +268,23 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq //decimal shoppingCartTax = _currencyService.ConvertFromPrimaryStoreCurrency(shoppingCartTaxBase, CommonServices.WorkContext.WorkingCurrency); // discount - decimal discount = -processPaymentRequest.Discount; - + var discount = -processPaymentRequest.Discount; if (discount != 0) { - cartItems.Add(new PaymentDetailsItemType() + cartItems.Add(new PaymentDetailsItemType { - Name = "Threadrock Discount", + Name = T("Plugins.Payments.PayPal.ThreadrockDiscount").Text, Quantity = "1", - Amount = new BasicAmountType() // this is the total discount + Amount = new BasicAmountType // this is the total discount { currencyID = payPalCurrency, - Value = discount.ToString("N", new CultureInfo("en-us")) + Value = discount.FormatInvariant() } }); itemTotal += discount; } - // get customer - int customerId = Convert.ToInt32(Services.WorkContext.CurrentCustomer.Id.ToString()); - var customer = _customerService.GetCustomerById(customerId); - if (!cart.IsRecurring()) { //we don't apply gift cards for recurring products @@ -295,14 +304,14 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq decimal amountToSubtract = -amountCanBeUsed; - cartItems.Add(new PaymentDetailsItemType() + cartItems.Add(new PaymentDetailsItemType { - Name = "Giftcard Applied", + Name = T("Plugins.Payments.PayPal.GiftcardApplied").Text, Quantity = "1", - Amount = new BasicAmountType() + Amount = new BasicAmountType { currencyID = payPalCurrency, - Value = amountToSubtract.ToString("N", new CultureInfo("en-us")) + Value = amountToSubtract.FormatInvariant() } }); @@ -318,12 +327,12 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq { ItemTotal = new BasicAmountType { - Value = Math.Round(itemTotal, 2).ToString("N", new CultureInfo("en-us")), + Value = Math.Round(itemTotal, 2).FormatInvariant(), currencyID = payPalCurrency }, ShippingTotal = new BasicAmountType { - Value = Math.Round(shippingTotal, 2).ToString("N", new CultureInfo("en-us")), + Value = Math.Round(shippingTotal, 2).FormatInvariant(), currencyID = payPalCurrency }, //TaxTotal = new BasicAmountType @@ -333,7 +342,7 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq //}, OrderTotal = new BasicAmountType { - Value = Math.Round(itemTotal + shippingTotal, 2).ToString("N", new CultureInfo("en-us")), + Value = Math.Round(itemTotal + shippingTotal, 2).FormatInvariant(), currencyID = payPalCurrency }, Custom = processPaymentRequest.OrderGuid.ToString(), @@ -405,7 +414,7 @@ public ProcessPaymentRequest SetCheckoutDetails(ProcessPaymentRequest processPay if (billingCountry != null) billingCountryId = billingCountry.Id; - var billingAddress = customer.Addresses.ToList().FindAddress( + var billingAddress = customer.Addresses.FindAddress( billingFirstName, billingLastName, billingPhoneNumber, billingEmail, string.Empty, string.Empty, billingAddress1, billingAddress2, billingCity, billingStateProvinceId, billingZipPostalCode, billingCountryId); @@ -462,7 +471,7 @@ public ProcessPaymentRequest SetCheckoutDetails(ProcessPaymentRequest processPay if (shippingCountry != null) shippingCountryId = shippingCountry.Id; - var shippingAddress = customer.Addresses.ToList().FindAddress( + var shippingAddress = customer.Addresses.FindAddress( shippingFirstName, shippingLastName, shippingPhoneNumber, shippingEmail, string.Empty, string.Empty, shippingAddress1, shippingAddress2, shippingCity, @@ -576,4 +585,13 @@ public DoExpressCheckoutPaymentResponseType DoExpressCheckoutPayment(ProcessPaym return result; } } + + + public class PayPalProcessPaymentRequest : ProcessPaymentRequest + { + /// + /// Gets or sets an order Discount Amount + /// + public decimal Discount { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalPlusProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalPlusProvider.cs new file mode 100644 index 0000000000..45e251d2d0 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalPlusProvider.cs @@ -0,0 +1,32 @@ +using System; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Controllers; +using SmartStore.PayPal.Settings; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal +{ + [SystemName("Payments.PayPalPlus")] + [FriendlyName("PayPal PLUS")] + [DisplayOrder(1)] + public partial class PayPalPlusProvider : PayPalRestApiProviderBase + { + public static string SystemName + { + get { return "Payments.PayPalPlus"; } + } + + public override PaymentMethodType PaymentMethodType + { + get + { + return PaymentMethodType.StandardAndRedirection; + } + } + + public override Type GetControllerType() + { + return typeof(PayPalPlusController); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs new file mode 100644 index 0000000000..2330c9d336 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Web; +using System.Web.Routing; +using SmartStore.Core.Configuration; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Logging; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.Services; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal +{ + public abstract class PayPalRestApiProviderBase : PaymentMethodBase, IConfigurable where TSetting : PayPalApiSettingsBase, ISettings, new() + { + protected PayPalRestApiProviderBase() + { + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + public HttpContextBase HttpContext { get; set; } + public ICommonServices Services { get; set; } + public IOrderService OrderService { get; set; } + public IOrderTotalCalculationService OrderTotalCalculationService { get; set; } + public IPayPalService PayPalService { get; set; } + + protected string GetControllerName() + { + return GetControllerType().Name.EmptyNull().Replace("Controller", ""); + } + + public static string CheckoutCompletedKey + { + get { return "PayPalCheckoutCompleted"; } + } + + public override bool SupportCapture + { + get { return true; } + } + + public override bool SupportPartiallyRefund + { + get { return true; } + } + + public override bool SupportRefund + { + get { return true; } + } + + public override bool SupportVoid + { + get { return true; } + } + + public override decimal GetAdditionalHandlingFee(IList cart) + { + var result = decimal.Zero; + try + { + var settings = Services.Settings.LoadSetting(); + + result = this.CalculateAdditionalFee(OrderTotalCalculationService, cart, settings.AdditionalFee, settings.AdditionalFeePercentage); + } + catch (Exception) + { + } + return result; + } + + public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = new ProcessPaymentResult + { + NewPaymentStatus = PaymentStatus.Pending + }; + + HttpContext.Session.SafeRemove(CheckoutCompletedKey); + + var settings = Services.Settings.LoadSetting(processPaymentRequest.StoreId); + var session = HttpContext.GetPayPalSessionData(); + + processPaymentRequest.OrderGuid = session.OrderGuid; + + var apiResult = PayPalService.ExecutePayment(settings, session); + + if (apiResult.Success && apiResult.Json != null) + { + var state = (string)apiResult.Json.state; + string reasonCode = null; + dynamic relatedObject = null; + + if (!state.IsCaseInsensitiveEqual("failed")) + { + // the payment id is required to find the order during webhook message processing + result.AuthorizationTransactionCode = apiResult.Id; + + // intent: "sale" for immediate payment, "authorize" for pre-authorized payments and "order" for an order. + // info required cause API has different endpoints for different intents. + var intent = (string)apiResult.Json.intent; + + if (intent.IsCaseInsensitiveEqual("sale")) + { + relatedObject = apiResult.Json.transactions[0].related_resources[0].sale; + + session.PaymentInstruction = PayPalService.ParsePaymentInstruction(apiResult.Json.payment_instruction) as PayPalPaymentInstruction; + } + else + { + relatedObject = apiResult.Json.transactions[0].related_resources[0].authorization; + } + + if (relatedObject != null) + { + state = (string)relatedObject.state; + reasonCode = (string)relatedObject.reason_code; + + // see PayPalService.Refund() + result.AuthorizationTransactionResult = "{0} ({1})".FormatInvariant(state.NaIfEmpty(), intent.NaIfEmpty()); + result.AuthorizationTransactionId = (string)relatedObject.id; + + result.NewPaymentStatus = PayPalService.GetPaymentStatus(state, reasonCode, PaymentStatus.Authorized); + + if (result.NewPaymentStatus == PaymentStatus.Paid) + { + result.CaptureTransactionResult = result.AuthorizationTransactionResult; + result.CaptureTransactionId = result.AuthorizationTransactionId; + } + } + } + else + { + var failureReason = (string)apiResult.Json.failure_reason; + + result.Errors.Add(T("Plugins.SmartStore.PayPal.PaymentExecuteFailed").Text.Grow(failureReason, " ")); + } + } + + if (!apiResult.Success) + result.Errors.Add(apiResult.ErrorMessage); + + return result; + } + + public override void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) + { + var instruction = PayPalService.CreatePaymentInstruction(HttpContext.GetPayPalSessionData().PaymentInstruction); + + if (instruction.HasValue()) + { + HttpContext.Session[CheckoutCompletedKey] = instruction; + + OrderService.AddOrderNote(postProcessPaymentRequest.Order, instruction, true); + } + } + + public override CapturePaymentResult Capture(CapturePaymentRequest capturePaymentRequest) + { + var result = new CapturePaymentResult + { + NewPaymentStatus = capturePaymentRequest.Order.PaymentStatus + }; + + var settings = Services.Settings.LoadSetting(capturePaymentRequest.Order.StoreId); + var session = new PayPalSessionData(); + + var apiResult = PayPalService.EnsureAccessToken(session, settings); + if (apiResult.Success) + { + apiResult = PayPalService.Capture(settings, session, capturePaymentRequest); + + if (apiResult.Success) + result.NewPaymentStatus = PaymentStatus.Paid; + } + + if (!apiResult.Success) + result.Errors.Add(apiResult.ErrorMessage); + + return result; + } + + public override RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequest) + { + var result = new RefundPaymentResult + { + NewPaymentStatus = refundPaymentRequest.Order.PaymentStatus + }; + + var settings = Services.Settings.LoadSetting(refundPaymentRequest.Order.StoreId); + var session = new PayPalSessionData(); + + var apiResult = PayPalService.EnsureAccessToken(session, settings); + if (apiResult.Success) + { + apiResult = PayPalService.Refund(settings, session, refundPaymentRequest); + if (apiResult.Success) + { + if (refundPaymentRequest.IsPartialRefund) + result.NewPaymentStatus = PaymentStatus.PartiallyRefunded; + else + result.NewPaymentStatus = PaymentStatus.Refunded; + } + } + + if (!apiResult.Success) + result.Errors.Add(apiResult.ErrorMessage); + + return result; + } + + public override VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) + { + var result = new VoidPaymentResult + { + NewPaymentStatus = voidPaymentRequest.Order.PaymentStatus + }; + + var settings = Services.Settings.LoadSetting(voidPaymentRequest.Order.StoreId); + var session = new PayPalSessionData(); + + var apiResult = PayPalService.EnsureAccessToken(session, settings); + if (apiResult.Success) + { + apiResult = PayPalService.Void(settings, session, voidPaymentRequest); + if (apiResult.Success) + { + result.NewPaymentStatus = PaymentStatus.Voided; + } + } + + if (!apiResult.Success) + result.Errors.Add(apiResult.ErrorMessage); + + return result; + } + + public override void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "Configure"; + controllerName = GetControllerName(); + routeValues = new RouteValueDictionary { { "area", "SmartStore.PayPal" } }; + } + + public override void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "PaymentInfo"; + controllerName = GetControllerName(); + routeValues = new RouteValueDictionary { { "area", "SmartStore.PayPal" } }; + } + } +} + diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs index 3e287d0c74..4632649153 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs @@ -14,7 +14,6 @@ using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.PayPal.Controllers; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Services; using SmartStore.Services.Localization; @@ -23,9 +22,6 @@ namespace SmartStore.PayPal { - /// - /// PayPalStandard provider - /// [SystemName("Payments.PayPalStandard")] [FriendlyName("PayPal Standard")] [DisplayOrder(2)] @@ -450,7 +446,7 @@ public List GetLineItems(PostProcessPaymentRequest postProcessPa var item = new PayPalLineItem { Type = PayPalItemType.PaymentFee, - Name = T("Plugins.Payments.PayPalStandard.PaymentMethodFee").Text, + Name = T("Plugins.Payments.PayPal.PaymentMethodFee").Text, Quantity = 1, Amount = order.PaymentMethodAdditionalFeeExclTax }; @@ -573,4 +569,46 @@ public override void GetPaymentInfoRoute(out string actionName, out string contr routeValues = new RouteValueDictionary() { { "area", "SmartStore.PayPal" } }; } } + + + public class PayPalLineItem : ICloneable + { + public PayPalItemType Type { get; set; } + public string Name { get; set; } + public int Quantity { get; set; } + public decimal Amount { get; set; } + + public decimal AmountRounded + { + get + { + return Math.Round(Amount, 2); + } + } + + public PayPalLineItem Clone() + { + var item = new PayPalLineItem + { + Type = this.Type, + Name = this.Name, + Quantity = this.Quantity, + Amount = this.Amount + }; + return item; + } + + object ICloneable.Clone() + { + return this.Clone(); + } + } + + public enum PayPalItemType + { + CartItem = 0, + Shipping, + PaymentFee, + Tax + } } diff --git a/src/Plugins/SmartStore.PayPal/RouteProvider.cs b/src/Plugins/SmartStore.PayPal/RouteProvider.cs index bebfbdb8ec..74e44ce093 100644 --- a/src/Plugins/SmartStore.PayPal/RouteProvider.cs +++ b/src/Plugins/SmartStore.PayPal/RouteProvider.cs @@ -29,8 +29,17 @@ public void RegisterRoutes(RouteCollection routes) ) .DataTokens["area"] = "SmartStore.PayPal"; - //Legacay Routes - routes.MapRoute("SmartStore.PayPalExpress.IPN", + routes.MapRoute("SmartStore.PayPalPlus", + "Plugins/SmartStore.PayPal/{controller}/{action}", + new { controller = "PayPalPlus", action = "Index" }, + new[] { "SmartStore.PayPal.Controllers" } + ) + .DataTokens["area"] = Plugin.SystemName; + + + + //Legacay Routes + routes.MapRoute("SmartStore.PayPalExpress.IPN", "Plugins/PaymentPayPalExpress/IPNHandler", new { controller = "PayPalExpress", action = "IPNHandler" }, new[] { "SmartStore.PayPal.Controllers" } diff --git a/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs b/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs new file mode 100644 index 0000000000..2f4b94c1c9 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Domain.Stores; +using SmartStore.PayPal.Settings; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal.Services +{ + public interface IPayPalService + { + void AddOrderNote(PayPalSettingsBase settings, Order order, string anyString, bool isIpn = false); + + void LogError(Exception exception, string shortMessage = null, string fullMessage = null, bool notify = false, IList errors = null, bool isWarning = false); + + PayPalPaymentInstruction ParsePaymentInstruction(dynamic json); + + string CreatePaymentInstruction(PayPalPaymentInstruction instruct); + + PaymentStatus GetPaymentStatus(string state, string reasonCode, PaymentStatus defaultStatus); + + PayPalResponse CallApi(string method, string path, string accessToken, PayPalApiSettingsBase settings, string data); + + PayPalResponse EnsureAccessToken(PayPalSessionData session, PayPalApiSettingsBase settings); + + PayPalResponse GetPayment(PayPalApiSettingsBase settings, PayPalSessionData session); + + PayPalResponse CreatePayment( + PayPalApiSettingsBase settings, + PayPalSessionData session, + List cart, + string providerSystemName, + string returnUrl, + string cancelUrl); + + PayPalResponse PatchShipping( + PayPalApiSettingsBase settings, + PayPalSessionData session, + List cart, + string providerSystemName); + + PayPalResponse ExecutePayment(PayPalApiSettingsBase settings, PayPalSessionData session); + + PayPalResponse Refund(PayPalApiSettingsBase settings, PayPalSessionData session, RefundPaymentRequest request); + + PayPalResponse Capture(PayPalApiSettingsBase settings, PayPalSessionData session, CapturePaymentRequest request); + + PayPalResponse Void(PayPalApiSettingsBase settings, PayPalSessionData session, VoidPaymentRequest request); + + PayPalResponse UpsertCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session, Store store); + + PayPalResponse DeleteCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session); + + PayPalResponse CreateWebhook(PayPalApiSettingsBase settings, PayPalSessionData session, string url); + + PayPalResponse DeleteWebhook(PayPalApiSettingsBase settings, PayPalSessionData session); + + HttpStatusCode ProcessWebhook( + PayPalApiSettingsBase settings, + NameValueCollection headers, + string rawJson, + string providerSystemName); + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs new file mode 100644 index 0000000000..d5bba1b72c --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs @@ -0,0 +1,27 @@ + +namespace SmartStore.PayPal.Services +{ + public enum PayPalPaymentInstructionItem + { + Reference = 0, + BankRoutingNumber, + Bank, + Bic, + Iban, + AccountHolder, + AccountNumber, + Amount, + PaymentDueDate, + Details + } + + public enum PayPalMessage + { + Message = 0, + Event, + EventId, + State, + Amount, + PaymentId + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalLineItem.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalLineItem.cs deleted file mode 100644 index 9c68e538fe..0000000000 --- a/src/Plugins/SmartStore.PayPal/Services/PayPalLineItem.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace SmartStore.PayPal.Services -{ - public class PayPalLineItem : ICloneable - { - public PayPalItemType Type { get; set; } - public string Name { get; set; } - public int Quantity { get; set; } - public decimal Amount { get; set; } - - public decimal AmountRounded - { - get - { - return Math.Round(Amount, 2); - } - } - - public PayPalLineItem Clone() - { - var item = new PayPalLineItem - { - Type = this.Type, - Name = this.Name, - Quantity = this.Quantity, - Amount = this.Amount - }; - return item; - } - - object ICloneable.Clone() - { - return this.Clone(); - } - } - - - public enum PayPalItemType - { - CartItem = 0, - Shipping, - PaymentFee, - Tax - } -} diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalProcessPaymentRequest.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalProcessPaymentRequest.cs deleted file mode 100644 index ff64026abf..0000000000 --- a/src/Plugins/SmartStore.PayPal/Services/PayPalProcessPaymentRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using SmartStore.Services.Payments; - -namespace SmartStore.PayPal.Services -{ - public class PayPalProcessPaymentRequest : ProcessPaymentRequest - { - /// - /// Gets or sets an order Discount Amount - /// - public decimal Discount { get; set; } - } -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs new file mode 100644 index 0000000000..62e53f01d5 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs @@ -0,0 +1,1177 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Web; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Discounts; +using SmartStore.Core.Domain.Logging; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; +using SmartStore.PayPal.Settings; +using SmartStore.Services; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Customers; +using SmartStore.Services.Directory; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; +using SmartStore.Services.Tax; + +namespace SmartStore.PayPal.Services +{ + public class PayPalService : IPayPalService + { + private readonly Lazy> _orderRepository; + private readonly ICommonServices _services; + private readonly IOrderService _orderService; + private readonly IOrderProcessingService _orderProcessingService; + private readonly IOrderTotalCalculationService _orderTotalCalculationService; + private readonly IPaymentService _paymentService; + private readonly IPriceCalculationService _priceCalculationService; + private readonly ITaxService _taxService; + private readonly ICurrencyService _currencyService; + private readonly Lazy _pictureService; + private readonly Lazy _companyInfoSettings; + + public PayPalService( + Lazy> orderRepository, + ICommonServices services, + IOrderService orderService, + IOrderProcessingService orderProcessingService, + IOrderTotalCalculationService orderTotalCalculationService, + IPaymentService paymentService, + IPriceCalculationService priceCalculationService, + ITaxService taxService, + ICurrencyService currencyService, + Lazy pictureService, + Lazy companyInfoSettings) + { + _orderRepository = orderRepository; + _services = services; + _orderService = orderService; + _orderProcessingService = orderProcessingService; + _orderTotalCalculationService = orderTotalCalculationService; + _paymentService = paymentService; + _priceCalculationService = priceCalculationService; + _taxService = taxService; + _currencyService = currencyService; + _pictureService = pictureService; + _companyInfoSettings = companyInfoSettings; + + T = NullLocalizer.Instance; + Logger = NullLogger.Instance; + } + + public Localizer T { get; set; } + public ILogger Logger { get; set; } + + private Dictionary CreateAddress(Address addr, bool addRecipientName) + { + var dic = new Dictionary(); + + dic.Add("line1", addr.Address1.Truncate(100)); + + if (addr.Address2.HasValue()) + { + dic.Add("line2", addr.Address2.Truncate(100)); + } + + dic.Add("city", addr.City.Truncate(50)); + + if (addr.CountryId != 0 && addr.Country != null) + { + dic.Add("country_code", addr.Country.TwoLetterIsoCode); + } + + dic.Add("postal_code", addr.ZipPostalCode.Truncate(20)); + + if (addr.StateProvinceId != 0 && addr.StateProvince != null) + { + dic.Add("state", addr.StateProvince.Abbreviation.Truncate(100)); + } + + if (addRecipientName) + { + dic.Add("recipient_name", addr.GetFullName().Truncate(50)); + } + + return dic; + } + + private Dictionary CreateAmount( + Store store, + Customer customer, + List cart, + string providerSystemName, + List> items) + { + var amount = new Dictionary(); + var amountDetails = new Dictionary(); + var language = _services.WorkContext.WorkingLanguage; + var currency = _services.WorkContext.WorkingCurrency; + var currencyCode = store.PrimaryStoreCurrency.CurrencyCode; + var includingTax = (_services.WorkContext.GetTaxDisplayTypeFor(customer, store.Id) == TaxDisplayType.IncludingTax); + + Discount orderAppliedDiscount; + List appliedGiftCards; + int redeemedRewardPoints = 0; + decimal redeemedRewardPointsAmount; + decimal orderDiscountInclTax; + decimal totalOrderItems = decimal.Zero; + var taxTotal = decimal.Zero; + + var shipping = Math.Round(_orderTotalCalculationService.GetShoppingCartShippingTotal(cart) ?? decimal.Zero, 2); + + var additionalHandlingFee = _paymentService.GetAdditionalHandlingFee(cart, providerSystemName); + var paymentFeeBase = _taxService.GetPaymentMethodAdditionalFee(additionalHandlingFee, customer); + var paymentFee = Math.Round(_currencyService.ConvertFromPrimaryStoreCurrency(paymentFeeBase, currency), 2); + + var total = Math.Round(_orderTotalCalculationService.GetShoppingCartTotal(cart, out orderDiscountInclTax, out orderAppliedDiscount, out appliedGiftCards, + out redeemedRewardPoints, out redeemedRewardPointsAmount) ?? decimal.Zero, 2); + + // line items + foreach (var item in cart) + { + decimal unitPriceTaxRate = decimal.Zero; + decimal unitPrice = _priceCalculationService.GetUnitPrice(item, true); + decimal productPrice = _taxService.GetProductPrice(item.Item.Product, unitPrice, includingTax, customer, out unitPriceTaxRate); + + if (items != null && productPrice != decimal.Zero) + { + var line = new Dictionary(); + line.Add("quantity", item.Item.Quantity); + line.Add("name", item.Item.Product.GetLocalized(x => x.Name, language.Id, true, false).Truncate(127)); + line.Add("price", productPrice.FormatInvariant()); + line.Add("currency", currencyCode); + line.Add("sku", item.Item.Product.Sku.Truncate(50)); + items.Add(line); + } + + totalOrderItems += (Math.Round(productPrice, 2) * item.Item.Quantity); + } + + if (items != null && paymentFee != decimal.Zero) + { + var line = new Dictionary(); + line.Add("quantity", "1"); + line.Add("name", T("Order.PaymentMethodAdditionalFee").Text.Truncate(127)); + line.Add("price", paymentFee.FormatInvariant()); + line.Add("currency", currencyCode); + items.Add(line); + + totalOrderItems += Math.Round(paymentFee, 2); + } + + if (!includingTax) + { + // "To avoid rounding errors we recommend not submitting tax amounts on line item basis. + // Calculated tax amounts for the entire shopping basket may be submitted in the amount objects. + // In this case the item amounts will be treated as amounts excluding tax. + // In a B2C scenario, where taxes are included, no taxes should be submitted to PayPal." + + SortedDictionary taxRates = null; + taxTotal = Math.Round(_orderTotalCalculationService.GetTaxTotal(cart, out taxRates), 2); + + amountDetails.Add("tax", taxTotal.FormatInvariant()); + } + + var itemsPlusMisc = (totalOrderItems + taxTotal + shipping); + + if (total != itemsPlusMisc) + { + var otherAmount = Math.Round(total - itemsPlusMisc, 2); + totalOrderItems += otherAmount; + + if (items != null && otherAmount != decimal.Zero) + { + // e.g. discount applied to cart total + var line = new Dictionary(); + line.Add("quantity", "1"); + line.Add("name", T("Plugins.SmartStore.PayPal.Other").Text.Truncate(127)); + line.Add("price", otherAmount.FormatInvariant()); + line.Add("currency", currencyCode); + items.Add(line); + } + } + + // fill amount object + amountDetails.Add("shipping", shipping.FormatInvariant()); + amountDetails.Add("subtotal", totalOrderItems.FormatInvariant()); + + //if (paymentFee != decimal.Zero) + //{ + // amountDetails.Add("handling_fee", paymentFee.FormatInvariant()); + //} + + amount.Add("total", total.FormatInvariant()); + amount.Add("currency", currencyCode); + amount.Add("details", amountDetails); + + return amount; + } + + private string ToInfoString(dynamic json) + { + var sb = new StringBuilder(); + + try + { + string[] strings = T("Plugins.SmartStore.PayPal.MessageStrings").Text.SplitSafe(";"); + var message = (string)json.summary; + var eventType = (string)json.event_type; + var eventId = (string)json.id; + string state = null; + string amount = null; + string paymentId = null; + + if (json.resource != null) + { + state = (string)json.resource.state; + paymentId = (string)json.resource.parent_payment; + + if (json.resource.amount != null) + amount = string.Concat((string)json.resource.amount.total, " ", (string)json.resource.amount.currency); + } + + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.Message), message.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.Event), eventType.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.EventId), eventId.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.PaymentId), paymentId.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.State), state.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.Amount), amount.NaIfEmpty())); + } + catch { } + + return sb.ToString(); + } + + public static string GetApiUrl(bool sandbox) + { + return sandbox ? "https://api.sandbox.paypal.com" : "https://api.paypal.com"; + } + + public static Dictionary GetSecurityProtocols() + { + var dic = new Dictionary(); + + foreach (SecurityProtocolType protocol in Enum.GetValues(typeof(SecurityProtocolType))) + { + string friendlyName = null; + switch (protocol) + { + case SecurityProtocolType.Ssl3: + friendlyName = "SSL 3.0"; + break; + case SecurityProtocolType.Tls: + friendlyName = "TLS 1.0"; + break; + case SecurityProtocolType.Tls11: + friendlyName = "TLS 1.1"; + break; + case SecurityProtocolType.Tls12: + friendlyName = "TLS 1.2"; + break; + default: + friendlyName = protocol.ToString().ToUpper(); + break; + } + + dic.Add(protocol, friendlyName); + } + return dic; + } + + public void AddOrderNote(PayPalSettingsBase settings, Order order, string anyString, bool isIpn = false) + { + try + { + if (order == null || anyString.IsEmpty() || (settings != null && !settings.AddOrderNotes)) + return; + + string[] orderNoteStrings = T("Plugins.SmartStore.PayPal.OrderNoteStrings").Text.SplitSafe(";"); + var faviconUrl = "{0}Plugins/{1}/Content/favicon.png".FormatInvariant(_services.WebHelper.GetStoreLocation(false), Plugin.SystemName); + + var sb = new StringBuilder(); + sb.AppendFormat("", faviconUrl); + + var note = orderNoteStrings.SafeGet(0).FormatInvariant(anyString); + + sb.AppendFormat("{0}", note); + + if (isIpn) + order.HasNewPaymentNotification = true; + + _orderService.AddOrderNote(order, sb.ToString()); + } + catch { } + } + + public void LogError(Exception exception, string shortMessage = null, string fullMessage = null, bool notify = false, IList errors = null, bool isWarning = false) + { + try + { + if (exception != null) + { + shortMessage = exception.ToAllMessages(); + fullMessage = exception.ToString(); + } + + if (shortMessage.HasValue()) + { + shortMessage = "PayPal. " + shortMessage; + Logger.InsertLog(isWarning ? LogLevel.Warning : LogLevel.Error, shortMessage, fullMessage.EmptyNull()); + + if (notify) + { + if (isWarning) + _services.Notifier.Warning(new LocalizedString(shortMessage)); + else + _services.Notifier.Error(new LocalizedString(shortMessage)); + } + } + } + catch (Exception) { } + + if (errors != null && shortMessage.HasValue()) + { + errors.Add(shortMessage); + } + } + + public PayPalPaymentInstruction ParsePaymentInstruction(dynamic json) + { + if (json == null) + return null; + + DateTime dt; + var result = new PayPalPaymentInstruction(); + + try + { + result.ReferenceNumber = (string)json.reference_number; + result.Type = (string)json.instruction_type; + result.Note = (string)json.note; + + if (DateTime.TryParse((string)json.payment_due_date, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt)) + { + result.DueDate = dt; + } + + if (json.amount != null) + { + result.AmountCurrencyCode = (string)json.amount.currency; + result.Amount = decimal.Parse((string)json.amount.value, CultureInfo.InvariantCulture); + } + + var rbi = json.recipient_banking_instruction; + + if (rbi != null) + { + result.RecipientBanking = new PayPalPaymentInstruction.RecipientBankingInstruction(); + result.RecipientBanking.BankName = (string)rbi.bank_name; + result.RecipientBanking.AccountHolderName = (string)rbi.account_holder_name; + result.RecipientBanking.AccountNumber = (string)rbi.account_number; + result.RecipientBanking.RoutingNumber = (string)rbi.routing_number; + result.RecipientBanking.Iban = (string)rbi.international_bank_account_number; + result.RecipientBanking.Bic = (string)rbi.bank_identifier_code; + } + + if (json.links != null) + { + result.Link = (string)json.links[0].href; + } + } + catch { } + + return result; + } + + public string CreatePaymentInstruction(PayPalPaymentInstruction instruct) + { + if (instruct == null || instruct.RecipientBanking == null) + return null; + + if (!instruct.IsManualBankTransfer && !instruct.IsPayUponInvoice) + return null; + + var sb = new StringBuilder("
"); + var paragraphTemplate = "
{0}
"; + var rowTemplate = "{0}: {1}
"; + var instructStrings = T("Plugins.SmartStore.PayPal.PaymentInstructionStrings").Text.SplitSafe(";"); + + if (instruct.IsManualBankTransfer) + { + sb.AppendFormat(paragraphTemplate, T("Plugins.SmartStore.PayPal.ManualBankTransferNote")); + + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Reference), instruct.ReferenceNumber); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.AccountNumber), instruct.RecipientBanking.AccountNumber); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.AccountHolder), instruct.RecipientBanking.AccountHolderName); + + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Bank), instruct.RecipientBanking.BankName); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Iban), instruct.RecipientBanking.Iban); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Bic), instruct.RecipientBanking.Bic); + } + else if (instruct.IsPayUponInvoice) + { + string amount = null; + var culture = new CultureInfo(_services.WorkContext.WorkingLanguage.LanguageCulture ?? "de-DE"); + + try + { + var currency = _currencyService.GetCurrencyByCode(instruct.AmountCurrencyCode); + var format = (currency != null && currency.CustomFormatting.HasValue() ? currency.CustomFormatting : "C"); + + amount = instruct.Amount.ToString(format, culture); + } + catch { } + + if (amount.IsEmpty()) + { + amount = string.Concat(instruct.Amount.ToString("N"), " ", instruct.AmountCurrencyCode); + } + + var intro = T("Plugins.SmartStore.PayPal.PayUponInvoiceLegalNote", _companyInfoSettings.Value.CompanyName.NaIfEmpty()); + + // /v1/payments/payment//payment-instruction not working anymore. Serves 401 unauthorized. + //if (instruct.Link.HasValue()) + //{ + // intro = "{0} {2}.".FormatInvariant(intro, instruct.Link, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Details)); + //} + + sb.AppendFormat(paragraphTemplate, intro); + + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Bank), instruct.RecipientBanking.BankName); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.AccountHolder), instruct.RecipientBanking.AccountHolderName); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Iban), instruct.RecipientBanking.Iban); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Bic), instruct.RecipientBanking.Bic); + sb.Append("
"); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Amount), amount); + if (instruct.DueDate.HasValue) + { + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.PaymentDueDate), instruct.DueDate.Value.ToString("d", culture)); + } + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Reference), instruct.ReferenceNumber); + } + + sb.Append("
"); + + return sb.ToString(); + } + + public PaymentStatus GetPaymentStatus(string state, string reasonCode, PaymentStatus defaultStatus) + { + var result = defaultStatus; + + if (state == null) + state = string.Empty; + + if (reasonCode == null) + reasonCode = string.Empty; + + switch (state.ToLowerInvariant()) + { + case "authorized": + result = PaymentStatus.Authorized; + break; + case "pending": + switch (reasonCode.ToLowerInvariant()) + { + case "authorization": + result = PaymentStatus.Authorized; + break; + default: + result = PaymentStatus.Pending; + break; + } + break; + case "completed": + case "captured": + case "partially_captured": + case "canceled_reversal": + result = PaymentStatus.Paid; + break; + case "denied": + case "expired": + case "failed": + case "voided": + result = PaymentStatus.Voided; + break; + case "reversed": + case "refunded": + result = PaymentStatus.Refunded; + break; + case "partially_refunded": + result = PaymentStatus.PartiallyRefunded; + break; + } + + return result; + } + + public PayPalResponse CallApi(string method, string path, string accessToken, PayPalApiSettingsBase settings, string data) + { + var isJson = (data.HasValue() && (data.StartsWith("{") || data.StartsWith("["))); + var encoding = (isJson ? Encoding.UTF8 : Encoding.ASCII); + var result = new PayPalResponse(); + HttpWebResponse webResponse = null; + + var url = GetApiUrl(settings.UseSandbox) + path.EnsureStartsWith("/"); + + if (method.IsCaseInsensitiveEqual("GET") && data.HasValue()) + url = url.EnsureEndsWith("?") + data; + + if (settings.SecurityProtocol.HasValue) + ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; + + var request = (HttpWebRequest)WebRequest.Create(url); + request.Method = method; + request.Accept = "application/json"; + request.ContentType = (isJson ? "application/json" : "application/x-www-form-urlencoded"); + + try + { + if (HttpContext.Current != null && HttpContext.Current.Request != null) + request.UserAgent = HttpContext.Current.Request.UserAgent; + else + request.UserAgent = Plugin.SystemName; + } + catch { } + + if (path.EmptyNull().EndsWith("/token")) + { + // see https://github.com/paypal/sdk-core-dotnet/blob/master/Source/SDK/OAuthTokenCredential.cs + byte[] credentials = Encoding.UTF8.GetBytes("{0}:{1}".FormatInvariant(settings.ClientId, settings.Secret)); + + request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(credentials)); + } + else + { + request.Headers["Authorization"] = "Bearer " + accessToken.EmptyNull(); + } + + request.Headers["PayPal-Partner-Attribution-Id"] = "SmartStoreAG_Cart_PayPalPlus"; + + if (data.HasValue() && (method.IsCaseInsensitiveEqual("POST") || method.IsCaseInsensitiveEqual("PUT") || method.IsCaseInsensitiveEqual("PATCH"))) + { + byte[] bytes = encoding.GetBytes(data); + + request.ContentLength = bytes.Length; + + using (var stream = request.GetRequestStream()) + { + stream.Write(bytes, 0, bytes.Length); + } + } + else + { + request.ContentLength = 0; + } + + try + { + webResponse = request.GetResponse() as HttpWebResponse; + result.Success = ((int)webResponse.StatusCode < 400); + } + catch (WebException wexc) + { + result.Success = false; + result.ErrorMessage = wexc.ToString(); + webResponse = wexc.Response as HttpWebResponse; + } + catch (Exception exception) + { + result.Success = false; + result.ErrorMessage = exception.ToString(); + LogError(exception); + } + + try + { + if (webResponse != null) + { + using (var reader = new StreamReader(webResponse.GetResponseStream(), Encoding.UTF8)) + { + var rawResponse = reader.ReadToEnd(); + if (rawResponse.HasValue()) + { + try + { + if (rawResponse.StartsWith("[")) + result.Json = JArray.Parse(rawResponse); + else + result.Json = JObject.Parse(rawResponse); + + if (result.Json != null) + { + if (!result.Success) + { + var name = (string)result.Json.name; + var message = (string)result.Json.message; + + if (name.IsEmpty()) + name = (string)result.Json.error; + + if (message.IsEmpty()) + message = (string)result.Json.error_description; + + result.ErrorMessage = "{0} ({1}).".FormatInvariant(message.NaIfEmpty(), name.NaIfEmpty()); + } + } + } + catch + { + if (!result.Success) + result.ErrorMessage = rawResponse; + } + } + } + + if (!result.Success) + { + if (result.ErrorMessage.IsEmpty()) + result.ErrorMessage = webResponse.StatusDescription; + + LogError(null, result.ErrorMessage, string.Concat(data.NaIfEmpty(), "\r\n\r\n", result.Json == null ? "" : result.Json.ToString()), false); + } + } + } + catch (Exception exception) + { + LogError(exception); + } + finally + { + if (webResponse != null) + { + webResponse.Close(); + webResponse.Dispose(); + } + } + + return result; + } + + public PayPalResponse EnsureAccessToken(PayPalSessionData session, PayPalApiSettingsBase settings) + { + if (session.AccessToken.IsEmpty() || DateTime.UtcNow >= session.TokenExpiration) + { + var result = CallApi("POST", "/v1/oauth2/token", null, settings, "grant_type=client_credentials"); + + if (result.Success) + { + session.AccessToken = (string)result.Json.access_token; + + var expireSeconds = ((string)result.Json.expires_in).ToInt(5 * 60); + + session.TokenExpiration = DateTime.UtcNow.AddSeconds(expireSeconds); + } + else + { + return result; + } + } + + return new PayPalResponse + { + Success = session.AccessToken.HasValue() + }; + } + + public PayPalResponse GetPayment(PayPalApiSettingsBase settings, PayPalSessionData session) + { + var result = CallApi("GET", "/v1/payments/payment/" + session.PaymentId, session.AccessToken, settings, null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse CreatePayment( + PayPalApiSettingsBase settings, + PayPalSessionData session, + List cart, + string providerSystemName, + string returnUrl, + string cancelUrl) + { + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + + //var dateOfBirth = customer.GetAttribute(SystemCustomerAttributeNames.DateOfBirth); + + var data = new Dictionary(); + var redirectUrls = new Dictionary(); + var payer = new Dictionary(); + //var payerInfo = new Dictionary(); + var transaction = new Dictionary(); + var items = new List>(); + var itemList = new Dictionary(); + + // "PayPal PLUS only supports transaction type “Sale” (instant settlement)" + if (providerSystemName == PayPalPlusProvider.SystemName) + data.Add("intent", "sale"); + else + data.Add("intent", settings.TransactMode == TransactMode.AuthorizeAndCapture ? "sale" : "authorize"); + + if (settings.ExperienceProfileId.HasValue()) + data.Add("experience_profile_id", settings.ExperienceProfileId); + + // redirect urls + if (returnUrl.HasValue()) + redirectUrls.Add("return_url", returnUrl); + + if (cancelUrl.HasValue()) + redirectUrls.Add("cancel_url", cancelUrl); + + if (redirectUrls.Any()) + data.Add("redirect_urls", redirectUrls); + + // payer, payer_info + // paypal review: do not transmit + //if (dateOfBirth.HasValue) + //{ + // payerInfo.Add("birth_date", dateOfBirth.Value.ToString("yyyy-MM-dd")); + //} + //if (customer.BillingAddress != null) + //{ + // payerInfo.Add("billing_address", CreateAddress(customer.BillingAddress, false)); + //} + + payer.Add("payment_method", "paypal"); + //payer.Add("payer_info", payerInfo); + data.Add("payer", payer); + + var amount = CreateAmount(store, customer, cart, providerSystemName, items); + + itemList.Add("items", items); + + transaction.Add("amount", amount); + transaction.Add("item_list", itemList); + transaction.Add("invoice_number", session.OrderGuid.ToString()); + + data.Add("transactions", new List> { transaction }); + + var result = CallApi("POST", "/v1/payments/payment", session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + //Logger.InsertLog(LogLevel.Information, "PayPal PLUS", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + (result.Json != null ? result.Json.ToString() : "")); + + return result; + } + + public PayPalResponse PatchShipping( + PayPalApiSettingsBase settings, + PayPalSessionData session, + List cart, + string providerSystemName) + { + var data = new List>(); + var amountTotal = new Dictionary(); + + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + + if (customer.ShippingAddress != null) + { + var shippingAddress = new Dictionary(); + shippingAddress.Add("op", "add"); + shippingAddress.Add("path", "/transactions/0/item_list/shipping_address"); + shippingAddress.Add("value", CreateAddress(customer.ShippingAddress, true)); + data.Add(shippingAddress); + } + + // update of whole amount object required. patching single amount values not possible (MALFORMED_REQUEST). + var amount = CreateAmount(store, customer, cart, providerSystemName, null); + + amountTotal.Add("op", "replace"); + amountTotal.Add("path", "/transactions/0/amount"); + amountTotal.Add("value", amount); + data.Add(amountTotal); + + var result = CallApi("PATCH", "/v1/payments/payment/{0}".FormatInvariant(session.PaymentId), session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + //Logger.InsertLog(LogLevel.Information, "PayPal PLUS", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + (result.Json != null ? result.Json.ToString() : "")); + + return result; + } + + public PayPalResponse ExecutePayment(PayPalApiSettingsBase settings, PayPalSessionData session) + { + var data = new Dictionary(); + data.Add("payer_id", session.PayerId); + + var result = CallApi("POST", "/v1/payments/payment/{0}/execute".FormatInvariant(session.PaymentId), session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + + //Logger.InsertLog(LogLevel.Information, "PayPal PLUS", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + result.Json.ToString()); + } + + return result; + } + + public PayPalResponse Refund(PayPalApiSettingsBase settings, PayPalSessionData session, RefundPaymentRequest request) + { + var data = new Dictionary(); + var store = _services.StoreService.GetStoreById(request.Order.StoreId); + var isSale = request.Order.AuthorizationTransactionResult.Contains("(sale)"); + + var path = "/v1/payments/{0}/{1}/refund".FormatInvariant(isSale ? "sale" : "capture", request.Order.CaptureTransactionId); + + var amount = new Dictionary(); + amount.Add("total", request.AmountToRefund.FormatInvariant()); + amount.Add("currency", store.PrimaryStoreCurrency.CurrencyCode); + + data.Add("amount", amount); + + var result = CallApi("POST", path, session.AccessToken, settings, data.Any() ? JsonConvert.SerializeObject(data) : null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + //Logger.InsertLog(LogLevel.Information, "PayPal Refund", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + (result.Json != null ? result.Json.ToString() : "")); + + return result; + } + + public PayPalResponse Capture(PayPalApiSettingsBase settings, PayPalSessionData session, CapturePaymentRequest request) + { + var data = new Dictionary(); + //var isAuthorize = request.Order.AuthorizationTransactionCode.IsCaseInsensitiveEqual("authorize"); + + var path = "/v1/payments/authorization/{0}/capture".FormatInvariant(request.Order.AuthorizationTransactionId); + + var store = _services.StoreService.GetStoreById(request.Order.StoreId); + + var amount = new Dictionary(); + amount.Add("total", request.Order.OrderTotal.FormatInvariant()); + amount.Add("currency", store.PrimaryStoreCurrency.CurrencyCode); + + data.Add("amount", amount); + + var result = CallApi("POST", path, session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse Void(PayPalApiSettingsBase settings, PayPalSessionData session, VoidPaymentRequest request) + { + var path = "/v1/payments/authorization/{0}/void".FormatInvariant(request.Order.AuthorizationTransactionId); + + var result = CallApi("POST", path, session.AccessToken, settings, null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse UpsertCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session, Store store) + { + PayPalResponse result; + var name = store.Name; + var logo = _pictureService.Value.GetPictureById(store.LogoPictureId); + var path = "/v1/payment-experience/web-profiles"; + + var data = new Dictionary(); + var presentation = new Dictionary(); + var inpuFields = new Dictionary(); + + // find existing profile id, only one profile per profile name possible + if (settings.ExperienceProfileId.IsEmpty()) + { + result = CallApi("GET", path, session.AccessToken, settings, null); + if (result.Success && result.Json != null) + { + foreach (var profile in result.Json) + { + var profileName = (string)profile.name; + if (profileName.IsCaseInsensitiveEqual(name)) + { + settings.ExperienceProfileId = (string)profile.id; + break; + } + } + } + } + + presentation.Add("brand_name", name); + presentation.Add("locale_code", _services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToUpper()); + + if (logo != null) + presentation.Add("logo_image", _pictureService.Value.GetPictureUrl(logo, showDefaultPicture: false, storeLocation: store.Url)); + + inpuFields.Add("allow_note", false); + inpuFields.Add("no_shipping", 0); + inpuFields.Add("address_override", 1); + + data.Add("name", name); + data.Add("presentation", presentation); + data.Add("input_fields", inpuFields); + + if (settings.ExperienceProfileId.HasValue()) + path = string.Concat(path, "/", HttpUtility.UrlPathEncode(settings.ExperienceProfileId)); + + result = CallApi(settings.ExperienceProfileId.HasValue() ? "PUT" : "POST", path, session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success) + { + if (result.Json != null) + result.Id = (string)result.Json.id; + else + result.Id = settings.ExperienceProfileId; + } + + return result; + } + + public PayPalResponse DeleteCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session) + { + var result = CallApi("DELETE", "/v1/payment-experience/web-profiles/" + settings.ExperienceProfileId, session.AccessToken, settings, null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse CreateWebhook(PayPalApiSettingsBase settings, PayPalSessionData session, string url) + { + var data = new Dictionary(); + var events = new List>(); + + events.Add(new Dictionary { { "name", "PAYMENT.AUTHORIZATION.VOIDED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.COMPLETED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.DENIED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.PENDING" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.REFUNDED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.REVERSED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.COMPLETED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.DENIED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.PENDING" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.REFUNDED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.REVERSED" } }); + + data.Add("url", url); + data.Add("event_types", events); + + var result = CallApi("POST", "/v1/notifications/webhooks", session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse DeleteWebhook(PayPalApiSettingsBase settings, PayPalSessionData session) + { + var result = CallApi("DELETE", "/v1/notifications/webhooks/" + settings.WebhookId, session.AccessToken, settings, null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + /// return 503 (HttpStatusCode.ServiceUnavailable) to ask paypal to resend it at later time again + public HttpStatusCode ProcessWebhook( + PayPalApiSettingsBase settings, + NameValueCollection headers, + string rawJson, + string providerSystemName) + { + if (rawJson.IsEmpty()) + return HttpStatusCode.OK; + + dynamic json = JObject.Parse(rawJson); + var eventType = (string)json.event_type; + + //foreach (var key in headers.AllKeys)"{0}: {1}".FormatInvariant(key, headers[key]).Dump(); + //string data = JsonConvert.SerializeObject(json, Formatting.Indented);data.Dump(); + + + // validating against PayPal SDK failing using sandbox, so better we do not use it: + //var apiContext = new global::PayPal.Api.APIContext + //{ + // AccessToken = "I do not have one here", + // Config = new Dictionary + // { + // { "mode", settings.UseSandbox ? "sandbox" : "live" }, + // { "clientId", settings.ClientId }, + // { "clientSecret", settings.Secret }, + // { "webhook.id", setting.WebhookId }, + // } + //}; + //var result = global::PayPal.Api.WebhookEvent.ValidateReceivedEvent(apiContext, headers, rawJson, webhookId); + //} + + var paymentId = (string)json.resource.parent_payment; + if (paymentId.IsEmpty()) + { + LogError(null, T("Plugins.SmartStore.PayPal.FoundOrderForPayment", 0, "".NaIfEmpty()), JsonConvert.SerializeObject(json, Formatting.Indented), isWarning: true); + return HttpStatusCode.OK; + } + + var orders = _orderRepository.Value.Table + .Where(x => x.PaymentMethodSystemName == providerSystemName && x.AuthorizationTransactionCode == paymentId) + .ToList(); + + if (orders.Count != 1) + { + LogError(null, T("Plugins.SmartStore.PayPal.FoundOrderForPayment", orders.Count, paymentId), JsonConvert.SerializeObject(json, Formatting.Indented), isWarning: true); + return HttpStatusCode.OK; + } + + var order = orders.First(); + var store = _services.StoreService.GetStoreById(order.StoreId); + + var total = decimal.Zero; + var currency = (string)json.resource.amount.currency; + var primaryCurrency = store.PrimaryStoreCurrency.CurrencyCode; + + if (!primaryCurrency.IsCaseInsensitiveEqual(currency)) + { + LogError(null, T("Plugins.SmartStore.PayPal.CurrencyNotEqual", currency.NaIfEmpty(), primaryCurrency), JsonConvert.SerializeObject(json, Formatting.Indented), isWarning: true); + return HttpStatusCode.OK; + } + + eventType = eventType.Substring(eventType.LastIndexOf('.') + 1); + + var newPaymentStatus = GetPaymentStatus(eventType, "authorization", order.PaymentStatus); + + var isValidTotal = decimal.TryParse((string)json.resource.amount.total, NumberStyles.Currency, CultureInfo.InvariantCulture, out total); + + if (newPaymentStatus == PaymentStatus.Refunded && (Math.Abs(order.OrderTotal) - Math.Abs(total)) > decimal.Zero) + { + newPaymentStatus = PaymentStatus.PartiallyRefunded; + } + + switch (newPaymentStatus) + { + case PaymentStatus.Pending: + break; + case PaymentStatus.Authorized: + if (_orderProcessingService.CanMarkOrderAsAuthorized(order)) + _orderProcessingService.MarkAsAuthorized(order); + break; + case PaymentStatus.Paid: + if (_orderProcessingService.CanMarkOrderAsPaid(order)) + _orderProcessingService.MarkOrderAsPaid(order); + break; + case PaymentStatus.Refunded: + if (_orderProcessingService.CanRefundOffline(order)) + _orderProcessingService.RefundOffline(order); + break; + case PaymentStatus.PartiallyRefunded: + if (_orderProcessingService.CanPartiallyRefundOffline(order, Math.Abs(total))) + _orderProcessingService.PartiallyRefundOffline(order, Math.Abs(total)); + break; + case PaymentStatus.Voided: + if (_orderProcessingService.CanVoidOffline(order)) + _orderProcessingService.VoidOffline(order); + break; + } + + AddOrderNote(settings, order, (string)ToInfoString(json), true); + + return HttpStatusCode.OK; + } + } + + + public class PayPalResponse + { + public bool Success { get; set; } + public dynamic Json { get; set; } + public string ErrorMessage { get; set; } + public string Id { get; set; } + } + + public class PayPalSessionData + { + public PayPalSessionData() + { + OrderGuid = Guid.NewGuid(); + } + + public string AccessToken { get; set; } + public DateTime TokenExpiration { get; set; } + public string PaymentId { get; set; } + public string PayerId { get; set; } + public string ApprovalUrl { get; set; } + public Guid OrderGuid { get; private set; } + public PayPalPaymentInstruction PaymentInstruction { get; set; } + } + + public class PayPalPaymentInstruction + { + public string ReferenceNumber { get; set; } + public string Type { get; set; } + public decimal Amount { get; set; } + public string AmountCurrencyCode { get; set; } + public DateTime? DueDate { get; set; } + public string Note { get; set; } + public string Link { get; set; } + + public RecipientBankingInstruction RecipientBanking { get; set; } + + public bool IsManualBankTransfer + { + get { return Type.IsCaseInsensitiveEqual("MANUAL_BANK_TRANSFER"); } + } + + public bool IsPayUponInvoice + { + get { return Type.IsCaseInsensitiveEqual("PAY_UPON_INVOICE"); } + } + + public class RecipientBankingInstruction + { + public string BankName { get; set; } + public string AccountHolderName { get; set; } + public string AccountNumber { get; set; } + public string RoutingNumber { get; set; } + public string Iban { get; set; } + public string Bic { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs b/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs index 9e728d3e40..8109422c68 100644 --- a/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs +++ b/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using System.Net; using SmartStore.Core.Configuration; +using SmartStore.PayPal.Services; namespace SmartStore.PayPal.Settings { @@ -9,16 +11,19 @@ public PayPalSettingsBase() { SecurityProtocol = SecurityProtocolType.Tls12; IpnChangesPaymentStatus = true; + AddOrderNotes = true; } public SecurityProtocolType? SecurityProtocol { get; set; } public bool UseSandbox { get; set; } - /// - /// Gets or sets a value indicating whether to "additional fee" is specified as percentage. true - percentage, false - fixed value. - /// - public bool AdditionalFeePercentage { get; set; } + public bool AddOrderNotes { get; set; } + + /// + /// Gets or sets a value indicating whether to "additional fee" is specified as percentage. true - percentage, false - fixed value. + /// + public bool AdditionalFeePercentage { get; set; } public decimal AdditionalFee { get; set; } @@ -34,14 +39,35 @@ public abstract class PayPalApiSettingsBase : PayPalSettingsBase public string ApiAccountName { get; set; } public string ApiAccountPassword { get; set; } public string Signature { get; set; } + + /// + /// PayPal client id + /// + public string ClientId { get; set; } + + /// + /// PayPal secret + /// + public string Secret { get; set; } + + /// + /// PayPal experience profile id + /// + public string ExperienceProfileId { get; set; } + + /// + /// PayPal webhook id + /// + public string WebhookId { get; set; } } + public class PayPalDirectPaymentSettings : PayPalApiSettingsBase, ISettings { public PayPalDirectPaymentSettings() { + UseSandbox = true; TransactMode = TransactMode.Authorize; - UseSandbox = true; } } @@ -84,7 +110,30 @@ public PayPalExpressPaymentSettings() public decimal DefaultShippingPrice { get; set; } } - public class PayPalStandardPaymentSettings : PayPalSettingsBase, ISettings + public class PayPalPlusPaymentSettings : PayPalApiSettingsBase, ISettings + { + public PayPalPlusPaymentSettings() + { + UseSandbox = true; + } + + /// + /// Specifies other payment methods to be offered in payment wall + /// + public List ThirdPartyPaymentMethods { get; set; } + + /// + /// Specifies whether to display the logo of a third party payment method + /// + public bool DisplayPaymentMethodLogo { get; set; } + + /// + /// Specifies whether to display the description of a third party payment method + /// + public bool DisplayPaymentMethodDescription { get; set; } + } + + public class PayPalStandardPaymentSettings : PayPalSettingsBase, ISettings { public PayPalStandardPaymentSettings() { @@ -101,18 +150,13 @@ public PayPalStandardPaymentSettings() public string IpnUrl { get; set; } } - /// - /// Represents payment processor transaction mode - /// - public enum TransactMode : int + + /// + /// Represents payment processor transaction mode + /// + public enum TransactMode { - /// - /// Authorize - /// Authorize = 1, - /// - /// Authorize and capture - /// AuthorizeAndCapture = 2 } } diff --git a/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj b/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj index 57974e8e71..e82c4a4097 100644 --- a/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj +++ b/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj @@ -68,6 +68,10 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + False + @@ -114,7 +118,9 @@ Properties\AssemblyVersionInfo.cs + + @@ -122,17 +128,23 @@ + + + + + - + + + - @@ -141,6 +153,7 @@ Settings.settings + @@ -172,6 +185,9 @@ + + PreserveNewest + PreserveNewest @@ -183,9 +199,6 @@ PreserveNewest - - Always - Always @@ -271,6 +284,18 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + Designer diff --git a/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs b/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs new file mode 100644 index 0000000000..b7355b4299 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs @@ -0,0 +1,33 @@ +using System; +using FluentValidation; +using SmartStore.PayPal.Models; +using SmartStore.Services.Localization; +using SmartStore.Web.Framework.Validators; + +namespace SmartStore.PayPal.Validators +{ + public class PayPalPlusConfigValidator : SmartValidatorBase + { + public PayPalPlusConfigValidator(ILocalizationService localize, Func addRule) + { + if (addRule("ClientId")) + { + RuleFor(x => x.ClientId).NotEmpty() + .WithMessage(localize.GetResource("Plugins.SmartStore.PayPal.ValidateClientIdAndSecret")); + } + + if (addRule("Secret")) + { + RuleFor(x => x.Secret).NotEmpty() + .WithMessage(localize.GetResource("Plugins.SmartStore.PayPal.ValidateClientIdAndSecret")); + } + + if (addRule("ThirdPartyPaymentMethods")) + { + RuleFor(x => x.ThirdPartyPaymentMethods) + .Must(x => x == null || x.Count <= 5) + .WithMessage(localize.GetResource("Plugins.Payments.PayPalPlus.ValidateThirdPartyPaymentMethods")); + } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/Configure.cshtml index 2a07a8326d..128ae981b6 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/Configure.cshtml @@ -15,7 +15,7 @@ diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/Configure.cshtml index 37ec7f1e5f..5738f2df4a 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/Configure.cshtml @@ -16,7 +16,7 @@ diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml new file mode 100644 index 0000000000..129922ca27 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml @@ -0,0 +1,207 @@ +@using SmartStore.PayPal; +@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()); +} + +
+
+
+ + @Html.Raw(T("Plugins.Payments.PayPalPlus.AdminInstruction")) +
+
+
+ + PayPal + +
+
+ +@Html.Action("StoreScopeConfiguration", "Setting", new { area = "Admin" }) + +@Html.ValidationSummary(false) + +@using (Html.BeginForm()) +{ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ @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.SettingOverrideCheckbox(model => model.ClientId) + @Html.TextBoxFor(model => model.ClientId, new { @class = "control-xlarge" }) + @Html.ValidationMessageFor(model => model.ClientId) +
+ @Html.SmartLabelFor(model => model.Secret) + + @Html.SettingOverrideCheckbox(model => model.Secret) + @Html.TextBoxFor(model => model.Secret, new { @class = "control-xlarge" }) + @Html.ValidationMessageFor(model => model.Secret) +
+
+
+ @Model.ConfigGroups.SafeGet(1) +
+
+
+ @Html.SmartLabelFor(model => model.SecurityProtocol) + + @Html.DropDownListFor(model => model.SecurityProtocol, Model.AvailableSecurityProtocols, T("Common.Unspecified")) +
+ @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) +
+
+
+ @Model.ConfigGroups.SafeGet(2) +
+
+
+ @Html.SmartLabelFor(model => model.ThirdPartyPaymentMethods) + + @Html.SettingOverrideCheckbox(model => model.ThirdPartyPaymentMethods) + @Html.ListBoxFor(x => x.ThirdPartyPaymentMethods, + new MultiSelectList(Model.AvailableThirdPartyPaymentMethods, "Value", "Text"), + new { multiple = "multiple", @class = "control-xlarge" }) + @Html.ValidationMessageFor(model => model.ThirdPartyPaymentMethods) +
+ @Html.SmartLabelFor(model => model.DisplayPaymentMethodLogo) + + @Html.SettingEditorFor(model => model.DisplayPaymentMethodLogo) + @Html.ValidationMessageFor(model => model.DisplayPaymentMethodLogo) +
+ @Html.SmartLabelFor(model => model.DisplayPaymentMethodDescription) + + @Html.SettingEditorFor(model => model.DisplayPaymentMethodDescription) + @Html.ValidationMessageFor(model => model.DisplayPaymentMethodDescription) +
+ @Html.SmartLabelFor(model => model.AdditionalFee) + + @Html.SettingEditorFor(model => model.AdditionalFee) + @Html.ValidationMessageFor(model => model.AdditionalFee) +
+ @Html.SmartLabelFor(model => model.AdditionalFeePercentage) + + @Html.SettingEditorFor(model => model.AdditionalFeePercentage) + @Html.ValidationMessageFor(model => model.AdditionalFeePercentage) +
+   + + +
+} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.Mobile.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.Mobile.cshtml new file mode 100644 index 0000000000..266ae25887 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.Mobile.cshtml @@ -0,0 +1,66 @@ +@using SmartStore.Web.Models.Checkout; +@using SmartStore.PayPal.Models; +@model PayPalPlusCheckoutModel +@{ + Layout = "~/Views/Shared/_Root.cshtml"; + Html.AddTitleParts(T("PageTitle.Checkout").Text); +} + +@Html.Partial("PaymentWallScripting") + +
+
+

@T("Checkout.SelectPaymentMethod")

+
+
+ @Html.Widget("mobile_checkout_payment_method_top") + + @if (Model.ErrorMessage.HasValue()) + { +
+
    +
  • @Html.Raw(T("Plugins.Payments.PayPalPlus.MethodUnavailable"))
  • +
  • + @if (!Model.ApprovalUrl.HasValue()) + { + @T("Plugins.SmartStore.PayPal.NoApprovalUrlReturned") + } + @Model.ErrorMessage +
  • +
+
+ } + else if (Model.PayPalPlusPseudoMessageFlag.HasValue() && Model.PayPalPlusPseudoMessageFlag == "1") + { +
+
    +
  • @Html.Raw(T("Plugins.Payments.PayPalPlus.SorryFailure"))
  • +
+
+ } + + @if (Model.FullDescription.HasValue()) + { +
+ @Html.Raw(Model.FullDescription) +
+ } + + @if (Model.HasAnyFees) + { + + } + +
+ +
+ +
+ +
+ + @Html.Widget("mobile_checkout_payment_method_bottom") +
+
\ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.cshtml new file mode 100644 index 0000000000..461b02a3ab --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.cshtml @@ -0,0 +1,69 @@ +@using SmartStore.Web.Models.Checkout; +@using SmartStore.PayPal.Models; +@model PayPalPlusCheckoutModel +@{ + Layout = "~/Views/Shared/_Checkout.cshtml"; + Html.AddTitleParts(T("PageTitle.Checkout").Text); +} +@section orderProgress +{ + @Html.Action("CheckoutProgress", "Checkout", new { step = CheckoutProgressStep.Payment }) +} + +@Html.Partial("PaymentWallScripting") + +
+
+

@T("Checkout.SelectPaymentMethod")

+
+
+ @Html.Widget("checkout_payment_method_top") + + @if (Model.ErrorMessage.HasValue()) + { +
+ @Html.Raw(T("Plugins.Payments.PayPalPlus.MethodUnavailable")) +
+ @if (!Model.ApprovalUrl.HasValue()) + { + @T("Plugins.SmartStore.PayPal.NoApprovalUrlReturned") + } + @Model.ErrorMessage +
+ } + else if (Model.PayPalPlusPseudoMessageFlag.HasValue() && Model.PayPalPlusPseudoMessageFlag == "1") + { +
+ @Html.Raw(T("Plugins.Payments.PayPalPlus.SorryFailure")) +
+ } + + @if (Model.FullDescription.HasValue()) + { +
+ @Html.Raw(Model.FullDescription) +
+ } + + @if (Model.HasAnyFees) + { + + } + +
+ +
+ +  @T("Common.Back") + + + +
+ + @Html.Widget("checkout_payment_method_bottom") +
+
\ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml new file mode 100644 index 0000000000..c1f2cdf6f0 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml @@ -0,0 +1,117 @@ +@using SmartStore.PayPal +@using SmartStore.PayPal.Models +@model PayPalPlusCheckoutModel + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml index fdd6911358..5846c1ed43 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml @@ -14,7 +14,7 @@ diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml index c5b73f2d11..ee77251801 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml @@ -5,7 +5,7 @@
- PayPal + PayPal
@T("Plugins.Payments.PayPalStandard.Fields.RedirectionTip") diff --git a/src/Plugins/SmartStore.PayPal/changelog.md b/src/Plugins/SmartStore.PayPal/changelog.md index 7f022d9901..e074479a1d 100644 --- a/src/Plugins/SmartStore.PayPal/changelog.md +++ b/src/Plugins/SmartStore.PayPal/changelog.md @@ -1,5 +1,26 @@ #Release Notes +##Paypal 2.6.0.5 +###Bugfixes +* PayPal PLUS: Fixes "Cannot perform runtime binding on a null reference" when rendering the payment wall. + +##Paypal 2.6.0.4 +###Bugfixes +* PayPal PLUS: Excluding tax issue. Fixes "Transaction amount details (subtotal, tax, shipping) must add up to specified amount total". + +##Paypal 2.6.0.3 +###Bugfixes +* PayPal PLUS: Integration review through PayPal +* PayPal PLUS: Generic attribute caching problem. Fixes "Item amount must add up to specified amount subtotal (or total if amount details not specified)". + +##PayPal 2.6.0.1 +###Improvements +* Added PayPal partner attribution Id as request header + +##Paypal 2.5.0.2 +###New Features +* PayPal PLUS payment provider + ##Paypal 2.5.0.1 ###Bugfixes * PayPal Standard: The order amount transmitted to PayPal was wrong if gift cards or reward points were applied diff --git a/src/Plugins/SmartStore.PayPal/packages.config b/src/Plugins/SmartStore.PayPal/packages.config index d1ef764942..eb91eb3011 100644 --- a/src/Plugins/SmartStore.PayPal/packages.config +++ b/src/Plugins/SmartStore.PayPal/packages.config @@ -7,4 +7,5 @@ + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/web.config b/src/Plugins/SmartStore.PayPal/web.config index d9badc0f86..f6daa0fb4b 100644 --- a/src/Plugins/SmartStore.PayPal/web.config +++ b/src/Plugins/SmartStore.PayPal/web.config @@ -66,7 +66,7 @@ - + @@ -94,7 +94,7 @@ - + diff --git a/src/Plugins/SmartStore.Shipping/Description.txt b/src/Plugins/SmartStore.Shipping/Description.txt index 5d85eddbb8..f241173e84 100644 --- a/src/Plugins/SmartStore.Shipping/Description.txt +++ b/src/Plugins/SmartStore.Shipping/Description.txt @@ -2,7 +2,7 @@ Description: Provides shipping methods for fixed rate shipping and computation based on weight. SystemName: SmartStore.Shipping Group: Shipping -Version: 2.5.0 +Version: 2.6.0 MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.Shipping.dll diff --git a/src/Plugins/SmartStore.Shipping/web.config b/src/Plugins/SmartStore.Shipping/web.config index 32347fc005..c0f82db177 100644 --- a/src/Plugins/SmartStore.Shipping/web.config +++ b/src/Plugins/SmartStore.Shipping/web.config @@ -1,115 +1,115 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs b/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs index 40334476a9..a147a1855d 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs @@ -116,41 +116,41 @@ public ByWeightShippingComputationMethod(IShippingService shippingService, /// /// Gets available shipping options /// - /// A request for getting shipping options + /// A request for getting shipping options /// Represents a response of getting shipping rate options - public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest getShippingOptionRequest) + public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest request) { - if (getShippingOptionRequest == null) + if (request == null) throw new ArgumentNullException("getShippingOptionRequest"); var response = new GetShippingOptionResponse(); - if (getShippingOptionRequest.Items == null || getShippingOptionRequest.Items.Count == 0) + if (request.Items == null || request.Items.Count == 0) { response.AddError(T("Admin.System.Warnings.NoShipmentItems")); return response; } - - int storeId = _storeContext.CurrentStore.Id; + + int storeId = request.StoreId > 0 ? request.StoreId : _storeContext.CurrentStore.Id; decimal subTotal = decimal.Zero; int countryId = 0; string zip = null; - if (getShippingOptionRequest.ShippingAddress != null) + if (request.ShippingAddress != null) { - countryId = getShippingOptionRequest.ShippingAddress.CountryId ?? 0; - zip = getShippingOptionRequest.ShippingAddress.ZipPostalCode; + countryId = request.ShippingAddress.CountryId ?? 0; + zip = request.ShippingAddress.ZipPostalCode; } - foreach (var shoppingCartItem in getShippingOptionRequest.Items) + foreach (var shoppingCartItem in request.Items) { if (shoppingCartItem.Item.IsFreeShipping || !shoppingCartItem.Item.IsShipEnabled) continue; subTotal += _priceCalculationService.GetSubTotal(shoppingCartItem, true); } - decimal weight = _shippingService.GetShoppingCartTotalWeight(getShippingOptionRequest.Items); + decimal weight = _shippingService.GetShoppingCartTotalWeight(request.Items); - var shippingMethods = _shippingService.GetAllShippingMethods(getShippingOptionRequest); + var shippingMethods = _shippingService.GetAllShippingMethods(request); foreach (var shippingMethod in shippingMethods) { var record = _shippingByWeightService.FindRecord(shippingMethod.Id, storeId, countryId, weight, zip); diff --git a/src/Plugins/SmartStore.ShippingByWeight/Description.txt b/src/Plugins/SmartStore.ShippingByWeight/Description.txt index 03312a5943..218a277c2b 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Description.txt +++ b/src/Plugins/SmartStore.ShippingByWeight/Description.txt @@ -1,7 +1,7 @@ FriendlyName: Shipping by weight SystemName: SmartStore.ShippingByWeight Group: Shipping -Version: 2.5.0 +Version: 2.6.0 MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.ShippingByWeight.dll diff --git a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs index 06d9b7ff55..f60212f80c 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs @@ -48,7 +48,7 @@ public virtual IQueryable GetShippingByWeightRecords() { var query = from x in _sbwRepository.Table - orderby x.StoreId, x.CountryId, x.ShippingMethodId, x.From, x.Zip + orderby x.StoreId, x.CountryId, x.ShippingMethodId, x.From select x; return query; @@ -83,7 +83,7 @@ public virtual IList GetShippingByWeightModels(int pageIn var shippingMethod = _shippingService.GetShippingMethodById(x.ShippingMethodId); var country = _countryService.GetCountryById(x.CountryId); - var model = new ShippingByWeightModel() + var model = new ShippingByWeightModel { Id = x.Id, StoreId = x.StoreId, diff --git a/src/Plugins/SmartStore.ShippingByWeight/web.config b/src/Plugins/SmartStore.ShippingByWeight/web.config index 32347fc005..c0f82db177 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/web.config +++ b/src/Plugins/SmartStore.ShippingByWeight/web.config @@ -1,115 +1,115 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.Tax/Description.txt b/src/Plugins/SmartStore.Tax/Description.txt index 79d79ec7a8..ec211f9efe 100644 --- a/src/Plugins/SmartStore.Tax/Description.txt +++ b/src/Plugins/SmartStore.Tax/Description.txt @@ -2,7 +2,7 @@ Description: Contains default tax providers like FixedRate, ByRegion etc. Group: Tax SystemName: SmartStore.Tax -Version: 2.5.0 +Version: 2.6.0 MinAppVersion: 2.5.0 DisplayOrder: 0 FileName: SmartStore.Tax.dll diff --git a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj index 692427666d..3f4f325f97 100644 --- a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj +++ b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj @@ -96,8 +96,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/Plugins/SmartStore.Tax/packages.config b/src/Plugins/SmartStore.Tax/packages.config index cc784b42f4..7f794ac209 100644 --- a/src/Plugins/SmartStore.Tax/packages.config +++ b/src/Plugins/SmartStore.Tax/packages.config @@ -6,5 +6,5 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.Tax/web.config b/src/Plugins/SmartStore.Tax/web.config index 3e1b370f5d..ba87d0f098 100644 --- a/src/Plugins/SmartStore.Tax/web.config +++ b/src/Plugins/SmartStore.Tax/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.WebApi/Description.txt b/src/Plugins/SmartStore.WebApi/Description.txt index aa73f1d263..646ac2fd0d 100644 --- a/src/Plugins/SmartStore.WebApi/Description.txt +++ b/src/Plugins/SmartStore.WebApi/Description.txt @@ -1,6 +1,6 @@ FriendlyName: SmartStore.NET Web Api SystemName: SmartStore.WebApi -Version: 2.5.0 +Version: 2.6.0 Group: Api MinAppVersion: 2.5.0 Author: SmartStore AG diff --git a/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml b/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml index f34a2ceb2e..650e50d1ab 100644 --- a/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml @@ -30,10 +30,10 @@ Aus Sicherheitsgründen darf die Zeit, zu der die Anfrage an die API gesendet wurde, nicht zu weit von der aktuellen Server-Zeit abweichen. Mit dieser Einstellung legen Sie dieses Zeitfenster in Minuten fest. - Unauthorisierte Zugriffe speichern + Unautorisierte Zugriffe speichern - Jeder unauthorisierte Zugriff wird in der Ereignisliste gespeichert. + Jeder unautorisierte Zugriff wird in der Ereignisliste gespeichert. Keine Zeitstempelprüfung @@ -41,6 +41,12 @@ Legt fest, ob geprüft werden soll, ob der Zeitstempel der letzten Anfrage kleiner gleich der der aktuellen ist. Verhindert das Auftreten von HmacResult.TimestampOlderThanLastRequest. + + Authentifizierung ohne MD5-Hash erlauben + + + Legt fest, ob Authentifizierungen ohne MD5 Inhalts-Hash erlaubt sind. + Öffentlicher Schlüssel diff --git a/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml b/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml index c3a2bca64a..7aafa97635 100644 --- a/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml @@ -40,6 +40,12 @@ Specifies whether to validate that the timestamp of the previous request is less or equal than the one of the current. Prevents the occurrence of HmacResult.TimestampOlderThanLastRequest. + + + Allow authentification without MD5 hash + + + Specifies whether to allow authentifications without MD5 content hash. Public key diff --git a/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs b/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs index e5c3e39e0f..23882c149e 100644 --- a/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs +++ b/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs @@ -18,7 +18,10 @@ public class WebApiConfigModel : ModelBase [SmartResourceDisplayName("Plugins.Api.WebApi.NoRequestTimestampValidation")] public bool NoRequestTimestampValidation { get; set; } - [SmartResourceDisplayName("Plugins.Api.WebApi.LogUnauthorized")] + [SmartResourceDisplayName("Plugins.Api.WebApi.AllowEmptyMd5Hash")] + public bool AllowEmptyMd5Hash { get; set; } + + [SmartResourceDisplayName("Plugins.Api.WebApi.LogUnauthorized")] public bool LogUnauthorized { get; set; } public int GridPageSize { get; set; } @@ -29,12 +32,14 @@ public void Copy(WebApiSettings settings, bool fromSettings) { ValidMinutePeriod = settings.ValidMinutePeriod; NoRequestTimestampValidation = settings.NoRequestTimestampValidation; + AllowEmptyMd5Hash = settings.AllowEmptyMd5Hash; LogUnauthorized = settings.LogUnauthorized; } else { settings.ValidMinutePeriod = ValidMinutePeriod; settings.NoRequestTimestampValidation = NoRequestTimestampValidation; + settings.AllowEmptyMd5Hash = AllowEmptyMd5Hash; settings.LogUnauthorized = LogUnauthorized; } } diff --git a/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj b/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj index 2961aa6848..73e315200d 100644 --- a/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj +++ b/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj @@ -107,8 +107,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/Plugins/SmartStore.WebApi/Views/WebApi/Configure.cshtml b/src/Plugins/SmartStore.WebApi/Views/WebApi/Configure.cshtml index 9e21e21993..9c6dbd98ef 100644 --- a/src/Plugins/SmartStore.WebApi/Views/WebApi/Configure.cshtml +++ b/src/Plugins/SmartStore.WebApi/Views/WebApi/Configure.cshtml @@ -107,6 +107,15 @@ @Html.ValidationMessageFor(model => model.NoRequestTimestampValidation) + + + @Html.SmartLabelFor(model => model.AllowEmptyMd5Hash) + + + @Html.EditorFor(model => model.AllowEmptyMd5Hash) + @Html.ValidationMessageFor(model => model.AllowEmptyMd5Hash) + + @Html.SmartLabelFor(model => model.LogUnauthorized) diff --git a/src/Plugins/SmartStore.WebApi/changelog.md b/src/Plugins/SmartStore.WebApi/changelog.md index 57922440d4..a2f7a528c8 100644 --- a/src/Plugins/SmartStore.WebApi/changelog.md +++ b/src/Plugins/SmartStore.WebApi/changelog.md @@ -1,5 +1,9 @@ #Release Notes +##Web Api 2.5.0.1 +###New Features +* Option to allow authentification without MD5 content hash + ##Web Api 2.2.0.5 ###New Features * Bridge to import framework: uploading import files to import profile directory diff --git a/src/Plugins/SmartStore.WebApi/packages.config b/src/Plugins/SmartStore.WebApi/packages.config index 4b574c3ff9..9e4fbc38a8 100644 --- a/src/Plugins/SmartStore.WebApi/packages.config +++ b/src/Plugins/SmartStore.WebApi/packages.config @@ -16,6 +16,6 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.WebApi/web.config b/src/Plugins/SmartStore.WebApi/web.config index 71013386a2..95ed427e0a 100644 --- a/src/Plugins/SmartStore.WebApi/web.config +++ b/src/Plugins/SmartStore.WebApi/web.config @@ -50,7 +50,7 @@ - + diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs index b5db948b54..29fdb90d52 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs @@ -113,14 +113,69 @@ protected virtual void NotifyAccessDenied(bool durable = true, bool log = true) Services.Notifier.Error(message, durable); } - protected virtual ActionResult RedirectToReferrer() + protected ActionResult RedirectToReferrer() { - if (Request.UrlReferrer != null && Request.UrlReferrer.ToString().HasValue()) + return RedirectToReferrer(null, () => RedirectToRoute("HomePage")); + } + + protected ActionResult RedirectToReferrer(string referrer) + { + return RedirectToReferrer(referrer, () => RedirectToRoute("HomePage")); + } + + protected ActionResult RedirectToReferrer(string referrer, string fallbackUrl) + { + // addressing "Open Redirection Vulnerability" (prevent cross-domain redirects / phishing) + if (fallbackUrl.HasValue() && !Url.IsLocalUrl(fallbackUrl)) { - return Redirect(Request.UrlReferrer.ToString()); + fallbackUrl = null; } - return RedirectToRoute("HomePage"); + return RedirectToReferrer( + referrer, + fallbackUrl.HasValue() ? () => Redirect(fallbackUrl) : (Func)null); + } + + protected virtual ActionResult RedirectToReferrer(string referrer, Func fallbackResult) + { + bool skipLocalCheck = false; + + if (referrer.IsEmpty() && Request.UrlReferrer != null && Request.UrlReferrer.ToString().HasValue()) + { + referrer = Request.UrlReferrer.ToString(); + if (referrer.HasValue()) + { + var domain1 = (new Uri(referrer)).GetLeftPart(UriPartial.Authority); + var domain2 = this.Request.Url.GetLeftPart(UriPartial.Authority); + if (domain1.IsCaseInsensitiveEqual(domain2)) + { + // always allow fully qualified urls from local host + skipLocalCheck = true; + } + else + { + referrer = null; + } + } + } + + // addressing "Open Redirection Vulnerability" (prevent cross-domain redirects / phishing) + if (referrer.HasValue() && !skipLocalCheck && !Url.IsLocalUrl(referrer)) + { + referrer = null; + } + + if (referrer.HasValue()) + { + return Redirect(referrer); + } + + if (fallbackResult != null) + { + return fallbackResult(); + } + + return HttpNotFound(); } protected virtual ActionResult RedirectToHomePageWithError(string reason, bool durable = true) diff --git a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs index 37ea64ef94..73a2c9bd9a 100644 --- a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs +++ b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs @@ -77,6 +77,9 @@ using SmartStore.Web.Framework.WebApi; using SmartStore.Web.Framework.WebApi.Configuration; using Module = Autofac.Module; +using SmartStore.Services.Catalog.Importer; +using SmartStore.Services.Customers.Importer; +using SmartStore.Services.Messages.Importer; namespace SmartStore.Web.Framework { @@ -104,7 +107,8 @@ public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, b builder.RegisterModule(new PackagingModule()); builder.RegisterModule(new ProvidersModule(typeFinder, pluginFinder)); builder.RegisterModule(new TasksModule(typeFinder)); - } + builder.RegisterModule(new DataExchangeModule(typeFinder)); + } public int Order { @@ -242,11 +246,6 @@ 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(); @@ -1082,9 +1081,44 @@ protected override void Load(ContainerBuilder builder) }); } - } + public class DataExchangeModule : Module + { + private readonly ITypeFinder _typeFinder; + + public DataExchangeModule(ITypeFinder typeFinder) + { + _typeFinder = typeFinder; + } + + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); + + if (!DataSettings.DatabaseIsInstalled()) + return; + + builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); + + // IEntityImporter implementations + builder.RegisterType().Keyed(ImportEntityType.Product).InstancePerRequest(); + builder.RegisterType().Keyed(ImportEntityType.Category).InstancePerRequest(); + builder.RegisterType().Keyed(ImportEntityType.Customer).InstancePerRequest(); + builder.RegisterType().Keyed(ImportEntityType.NewsLetterSubscription).InstancePerRequest(); + + // Register resolving delegate + builder.Register>(c => + { + var cc = c.Resolve(); + return keyed => cc.ResolveKeyed(keyed); + }); + } + + } + #endregion #region Sources diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs index a6adbe2e1a..b56f183331 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs @@ -1,29 +1,29 @@ using System; -using System.Web; -using System.Threading; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Text; +using System.Threading; +using System.Web; using System.Web.Mvc; -using System.Web.Routing; using System.Web.Mvc.Html; +using System.Web.Routing; using System.Web.WebPages; using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Infrastructure; using SmartStore.Services.Localization; -using SmartStore.Web.Framework.Localization; -using SmartStore.Web.Framework.UI; -using SmartStore.Web.Framework.Settings; using SmartStore.Utilities; -using SmartStore.Core.Domain.Catalog; +using SmartStore.Web.Framework.Localization; using SmartStore.Web.Framework.Modelling; +using SmartStore.Web.Framework.Settings; +using SmartStore.Web.Framework.UI; namespace SmartStore.Web.Framework { - public enum InputEditorType + public enum InputEditorType { TextBox, Password, Hidden, @@ -447,8 +447,12 @@ public static MvcHtmlString ControlGroupFor( { return html.HiddenFor(expression); } - - var sb = new StringBuilder("
"); + + string inputHtml = ""; + var htmlAttributes = new RouteValueDictionary(); + var dataTypeName = ModelMetadata.FromLambdaExpression(expression, html.ViewData).DataTypeName.EmptyNull(); + + var sb = new StringBuilder("
"); if (editorType != InputEditorType.Checkbox) { @@ -458,14 +462,23 @@ public static MvcHtmlString ControlGroupFor( } sb.AppendLine("
"); - string inputHtml = ""; - object attrs = null; + if (!required && (editorType == InputEditorType.TextBox || editorType == InputEditorType.Password)) { - attrs = new { placeholder = "Optional" /* TODO: Loc */ }; + htmlAttributes.Add("placeholder", EngineContext.Current.Resolve().GetResource("Common.Optional")); } - //var x = ModelMetadata.FromLambdaExpression(expression, html.ViewData).DisplayName; - switch (editorType) + + switch (dataTypeName) + { + case "EmailAddress": + htmlAttributes.Add("type", "email"); + break; + case "PhoneNumber": + htmlAttributes.Add("type", "tel"); + break; + } + + switch (editorType) { case InputEditorType.Checkbox: inputHtml = string.Format("", @@ -473,12 +486,13 @@ public static MvcHtmlString ControlGroupFor( ModelMetadata.FromLambdaExpression(expression, html.ViewData).DisplayName); // TBD: ist das OK so? break; case InputEditorType.Password: - inputHtml = html.PasswordFor(expression, attrs).ToString(); + inputHtml = html.PasswordFor(expression, htmlAttributes).ToString(); break; default: - inputHtml = html.TextBoxFor(expression, attrs).ToString(); + inputHtml = html.TextBoxFor(expression, htmlAttributes).ToString(); break; } + sb.AppendLine(inputHtml); sb.AppendLine(html.ValidationMessageFor(expression).ToString()); if (helpHint.HasValue()) diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs index fce317402c..11b8f1eeb0 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs @@ -8,7 +8,6 @@ namespace SmartStore.Web.Framework.Filters { public class StoreIpAddressAttribute : ActionFilterAttribute { - public Lazy WebHelper { get; set; } public Lazy WorkContext { get; set; } public Lazy CustomerService { get; set; } @@ -21,17 +20,15 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) if (filterContext == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) return; - //don't apply filter to child methods if (filterContext.IsChildAction) return; - //only GET requests if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) return; var webHelper = this.WebHelper.Value; - //update IP address + // Update IP address string currentIpAddress = webHelper.GetCurrentIpAddress(); if (!String.IsNullOrEmpty(currentIpAddress)) { diff --git a/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs index 2b3d2f2bd5..f683d550e1 100644 --- a/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs @@ -57,7 +57,7 @@ public virtual void OnAuthorization(AuthorizationContext filterContext) if (securityMode == HttpSecurityMode.SharedSsl) { // Don't attempt to redirect when shared SSL is being used and current request is secured. - // Redirecting from https://ssl.bla.com to https://www.ssl.bla.com will most probably fail. + // Redirecting from http://ssl.bla.com to https://www.ssl.bla.com will most probably fail. return; } } diff --git a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs index 0c73aa1f3a..80a2d035e8 100644 --- a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs @@ -19,6 +19,7 @@ public StoreDependingSettingHelper(ViewDataDictionary viewData) } public static string ViewDataKey { get { return "StoreDependingSettingData"; } } + public StoreDependingSettingData Data { get @@ -38,16 +39,19 @@ private bool IsOverrideChecked(string settingKey, FormCollection form) } return false; } + public bool IsOverrideChecked(object settings, string name, FormCollection form) { var key = settings.GetType().Name + "." + name; return IsOverrideChecked(key, form); } + public void AddOverrideKey(object settings, string name) { var key = settings.GetType().Name + "." + name; Data.OverrideSettingKeys.Add(key); } + public void CreateViewDataObject(int activeStoreScopeConfiguration, string rootSettingClass = null) { _viewData[ViewDataKey] = new StoreDependingSettingData() @@ -57,112 +61,60 @@ public void CreateViewDataObject(int activeStoreScopeConfiguration, string rootS }; } - public void GetOverrideKeys(object settings, object model, int storeId, ISettingService settingService, bool isRootModel = true) - { - if (storeId <= 0) - return; // single store mode -> there are no overrides + public void GetOverrideKeys(object settings, object model, int storeId, ISettingService settingService, bool isRootModel = true, ILocalizedModelLocal localized = null, int? index = null) + { + if (storeId <= 0) + return; // single store mode -> there are no overrides - var data = Data; - if (data == null) - data = new StoreDependingSettingData(); + var data = Data; + if (data == null) + data = new StoreDependingSettingData(); - var settingName = settings.GetType().Name; - var properties = settings.GetType().GetProperties(); + var settingName = settings.GetType().Name; + var properties = settings.GetType().GetProperties(); + var localizedEntityService = EngineContext.Current.Resolve(); var modelType = model.GetType(); - foreach (var prop in properties) - { - var name = prop.Name; - var modelProperty = modelType.GetProperty(name); - - if (modelProperty == null) - continue; // setting is not configurable or missing or whatever... however we don't need the override info - - var key = settingName + "." + name; - var setting = settingService.GetSettingByKey(key, storeId: storeId); - - if (setting != null) - data.OverrideSettingKeys.Add(key); - } - - if (isRootModel) - { - data.ActiveStoreScopeConfiguration = storeId; - data.RootSettingClass = settingName; - - _viewData[ViewDataKey] = data; - } - } - - // DRY? - public void GetOverrideKeysLocalized(object settings, object model, int storeId, ISettingService settingService, bool isRootModel = true, ILocalizedModelLocal localized = null, int? index = null) - { - if (storeId <= 0) - return; // single store mode -> there are no overrides - - var data = Data; - if (data == null) - data = new StoreDependingSettingData(); - - var settingName = settings.GetType().Name; - var properties = localized.GetType().GetProperties(); - var localizedEntityService = EngineContext.Current.Resolve(); - - var modelType = model.GetType(); - - foreach (var prop in properties) - { - var name = prop.Name; + foreach (var prop in properties) + { + var name = prop.Name; var modelProperty = modelType.GetProperty(name); - if (modelProperty == null) - continue; // setting is not configurable or missing or whatever... however we don't need the override info - - var key = "Locales[" + index.ToString() + "]." + name; + if (modelProperty == null) + continue; // setting is not configurable or missing or whatever... however we don't need the override info - var resultStr = localizedEntityService.GetLocalizedValue(localized.LanguageId, 0, settingName, name); + var key = String.Empty; + var setting = String.Empty; - if (!String.IsNullOrEmpty(resultStr)) - data.OverrideSettingKeys.Add(key); - } + if (localized == null) + { + key = settingName + "." + name; + setting = settingService.GetSettingByKey(key, storeId: storeId); + } + else + { + key = "Locales[" + index.ToString() + "]." + name; + setting = localizedEntityService.GetLocalizedValue(localized.LanguageId, 0, settingName, name); + } - if (isRootModel) - { - data.ActiveStoreScopeConfiguration = storeId; - data.RootSettingClass = settingName; + if (!String.IsNullOrEmpty(setting)) + data.OverrideSettingKeys.Add(key); + } - _viewData[ViewDataKey] = data; - } - } + if (isRootModel) + { + data.ActiveStoreScopeConfiguration = storeId; + data.RootSettingClass = settingName; - public void UpdateSettings(object settings, FormCollection form, int storeId, ISettingService settingService) - { - var settingName = settings.GetType().Name; - var properties = FastProperty.GetProperties(settings.GetType()).Values; - - foreach (var prop in properties) - { - var name = prop.Name; - var key = settingName + "." + name; - - if (storeId == 0 || IsOverrideChecked(key, form)) - { - dynamic value = prop.GetValue(settings); - settingService.SetSetting(key, value == null ? "" : value, storeId, false); - } - else if (storeId > 0) - { - settingService.DeleteSetting(key, storeId); - } - } - } + _viewData[ViewDataKey] = data; + } + } - // DRY? - public void UpdateLocalizedSettings(object settings, FormCollection form, int storeId, ISettingService settingService, ILocalizedModelLocal localized) + public void UpdateSettings(object settings, FormCollection form, int storeId, ISettingService settingService, ILocalizedModelLocal localized = null) { var settingName = settings.GetType().Name; - var properties = FastProperty.GetProperties(localized.GetType()).Values; + var properties = FastProperty.GetProperties(localized == null ? settings.GetType() : localized.GetType()).Values; foreach (var prop in properties) { @@ -171,7 +123,7 @@ public void UpdateLocalizedSettings(object settings, FormCollection form, int st if (storeId == 0 || IsOverrideChecked(key, form)) { - dynamic value = prop.GetValue(settings); + dynamic value = prop.GetValue(localized == null ? settings : localized); settingService.SetSetting(key, value == null ? "" : value, storeId, false); } else if (storeId > 0) diff --git a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj index 729a38d055..b37e9baf66 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj +++ b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj @@ -122,9 +122,9 @@ True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs index b4a4268f49..b6fade194d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs @@ -322,9 +322,9 @@ public string GetThemeVariable(string varname, string defaultValue = "") return defaultValue; } - public static string GenerateHelpUrl(string path) + public string GenerateHelpUrl(string path) { - return SmartStoreVersion.GenerateHelpUrl(path); + return SmartStoreVersion.GenerateHelpUrl(WorkContext.WorkingLanguage.UniqueSeoCode, path); } private void EnsureThemeContextInitialized() diff --git a/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs b/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs index 6f8c74b211..7d349f7e4a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs @@ -16,10 +16,13 @@ public void Validate(T model, ModelStateDictionary modelState) if (!result.IsValid) { - int i = 0; foreach (var error in result.Errors) { - modelState.AddModelError(error.PropertyName + (++i).ToString(), error.ErrorMessage); + try + { + modelState.AddModelError(error.PropertyName, error.ErrorMessage); + } + catch { } } } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs index d68ad50cef..f0b5de4270 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs @@ -39,6 +39,7 @@ public static WebApiControllingCacheData Data() { ValidMinutePeriod = settings.ValidMinutePeriod, NoRequestTimestampValidation = settings.NoRequestTimestampValidation, + AllowEmptyMd5Hash = settings.AllowEmptyMd5Hash, LogUnauthorized = settings.LogUnauthorized, ApiUnavailable = (plugin == null || !plugin.Installed), PluginVersion = (plugin == null ? "1.0" : plugin.Version.ToString()) @@ -57,6 +58,7 @@ public partial class WebApiControllingCacheData public bool ApiUnavailable { get; set; } public int ValidMinutePeriod { get; set; } public bool NoRequestTimestampValidation { get; set; } + public bool AllowEmptyMd5Hash { get; set; } public bool LogUnauthorized { get; set; } public string PluginVersion { get; set; } @@ -64,7 +66,7 @@ public string Version { get { - return "{0} {1}".FormatWith(WebApiGlobal.MaxApiVersion, PluginVersion); + return "{0} {1}".FormatInvariant(WebApiGlobal.MaxApiVersion, PluginVersion); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs index 3a0e10857e..db9be1cdb6 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs @@ -167,7 +167,21 @@ protected virtual HmacResult IsAuthenticated(HttpActionContext actionContext, Da string signatureProvider = _hmac.CreateSignature(apiUser.SecretKey, messageRepresentation); if (signatureProvider != signatureConsumer) - return HmacResult.InvalidSignature; + { + if (cacheControllingData.AllowEmptyMd5Hash) + { + messageRepresentation = _hmac.CreateMessageRepresentation(context, null, headTimestamp); + + signatureProvider = _hmac.CreateSignature(apiUser.SecretKey, messageRepresentation); + + if (signatureProvider != signatureConsumer) + return HmacResult.InvalidSignature; + } + else + { + return HmacResult.InvalidSignature; + } + } customer = GetCustomer(apiUser.CustomerId); if (customer == null) diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiEntityController.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiEntityController.cs index 2d7a98ecda..000f1549ea 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiEntityController.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiEntityController.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Data.Entity; using System.Data.Entity.Core.Metadata.Edm; using System.Data.Entity.Infrastructure; using System.Linq; @@ -8,15 +10,13 @@ using System.Web.Http; using System.Web.Http.OData; using System.Web.Http.OData.Routing; -using System.Data.Entity; +using Autofac; +using SmartStore.ComponentModel; using SmartStore.Core; using SmartStore.Core.Data; +using SmartStore.Services; using SmartStore.Services.Directory; using SmartStore.Services.Localization; -using System.Collections.Generic; -using SmartStore.Services; -using Autofac; -using SmartStore.ComponentModel; namespace SmartStore.Web.Framework.WebApi { @@ -338,7 +338,20 @@ protected internal virtual SingleResult GetRelatedEntity( public override HttpResponseMessage Post(TEntity entity) { - return Request.CreateResponse(HttpStatusCode.OK, CreateEntity(entity)); + var response = Request.CreateResponse(HttpStatusCode.OK, CreateEntity(entity)); + + try + { + var entityUrl = Url.ODataLink( + new EntitySetPathSegment(entity.GetType().Name.EnsureEndsWith("s")), + new KeyValuePathSegment(entity.Id.ToString()) + ); + + response.Headers.Location = new Uri(entityUrl); + } + catch { } + + return response; } protected override TEntity CreateEntity(TEntity entity) diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiSettings.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiSettings.cs index 1f2207d94f..392e14d13f 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiSettings.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiSettings.cs @@ -7,5 +7,6 @@ public class WebApiSettings : ISettings public int ValidMinutePeriod { get; set; } public bool LogUnauthorized { get; set; } public bool NoRequestTimestampValidation { get; set; } + public bool AllowEmptyMd5Hash { get; set; } } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs b/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs index 298f30bc62..5651615faf 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net; using System.Web; using SmartStore.Core; using SmartStore.Core.Caching; @@ -24,9 +25,7 @@ namespace SmartStore.Web.Framework ///
public partial class WebWorkContext : IWorkContext { - private const string CustomerCookieName = "smartstore.customer"; - - private readonly HttpContextBase _httpContext; + private readonly HttpContextBase _httpContext; private readonly ICustomerService _customerService; private readonly IStoreContext _storeContext; private readonly IAuthenticationService _authenticationService; @@ -76,44 +75,6 @@ public WebWorkContext(Func cacheManager, this._userAgent = userAgent; } - protected HttpCookie GetCustomerCookie() - { - if (_httpContext == null || _httpContext.Request == null) - return null; - - return _httpContext.Request.Cookies[CustomerCookieName]; - } - - protected void SetCustomerCookie(Guid customerGuid) - { - if (_httpContext != null && _httpContext.Response != null) - { - var cookie = new HttpCookie(CustomerCookieName); - cookie.HttpOnly = true; - cookie.Value = customerGuid.ToString(); - if (customerGuid == Guid.Empty) - { - cookie.Expires = DateTime.Now.AddMonths(-1); - } - else - { - int cookieExpires = 24 * 365; //TODO make configurable - cookie.Expires = DateTime.Now.AddHours(cookieExpires); - } - - try - { - if (_httpContext.Response.Cookies[CustomerCookieName] != null) - _httpContext.Response.Cookies.Remove(CustomerCookieName); - } - catch (Exception) { } - - _httpContext.Response.Cookies.Add(cookie); - } - } - - //public Lazy TaxService { get; set; } - /// /// Gets or sets the current customer /// @@ -126,38 +87,16 @@ public Customer CurrentCustomer Customer customer = null; - // check whether request is made by a background task - // in this case return built-in customer record for background task - if (_httpContext == null || _httpContext.IsFakeContext()) - { - customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.BackgroundTask); - } - - // check whether request is made by a search engine - // in this case return built-in customer record for search engines - if (customer == null || customer.Deleted || !customer.Active) - { - if (_userAgent.IsBot) - { - customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); - } - } - - // check whether request is made by the PDF converter - // in this case return built-in customer record for the converter - if (customer == null || customer.Deleted || !customer.Active) + // Is system account? + if (TryGetSystemAccount(out customer)) { - if (_userAgent.IsPdfConverter) - { - customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.PdfConverter); - } + // Get out quickly. Bots tend to overstress the shop. + _cachedCustomer = customer; + return customer; } - // registered user? - if (customer == null || customer.Deleted || !customer.Active) - { - customer = _authenticationService.GetAuthenticatedCustomer(); - } + // registered user? + customer = _authenticationService.GetAuthenticatedCustomer(); // impersonate user if required (currently used for 'phone order' support) if (customer != null && !customer.Deleted && customer.Active) @@ -168,59 +107,93 @@ public Customer CurrentCustomer var impersonatedCustomer = _customerService.GetCustomerById(impersonatedCustomerId.Value); if (impersonatedCustomer != null && !impersonatedCustomer.Deleted && impersonatedCustomer.Active) { - //set impersonated customer + // set impersonated customer _originalCustomerIfImpersonated = customer; customer = impersonatedCustomer; } } } - // load guest customer + // Load guest customer if (customer == null || customer.Deleted || !customer.Active) { - var customerCookie = GetCustomerCookie(); - if (customerCookie != null && !String.IsNullOrEmpty(customerCookie.Value)) - { - Guid customerGuid; - if (Guid.TryParse(customerCookie.Value, out customerGuid)) - { - var customerByCookie = _customerService.GetCustomerByGuid(customerGuid); - if (customerByCookie != null && !customerByCookie.IsRegistered()) - customer = customerByCookie; - } - } + customer = GetGuestCustomer(); } - // create guest if not exists - if (customer == null || customer.Deleted || !customer.Active) - { - customer = _customerService.InsertGuestCustomer(); - } - - - // validation - if (!customer.Deleted && customer.Active) - { - SetCustomerCookie(customer.CustomerGuid); - _cachedCustomer = customer; - } - - return _cachedCustomer; + _cachedCustomer = customer; + return _cachedCustomer; } set { - if (!value.IsSystemAccount) - { - SetCustomerCookie(value.CustomerGuid); - } _cachedCustomer = value; } } - /// - /// Gets or sets the original customer (in case the current one is impersonated) - /// - public Customer OriginalCustomerIfImpersonated + protected bool TryGetSystemAccount(out Customer customer) + { + // Never check whether customer is deleted/inactive in this method. + // System accounts should neither be deletable nor activatable, they are mandatory. + + customer = null; + + // check whether request is made by a background task + // in this case return built-in customer record for background task + if (_httpContext == null || _httpContext.IsFakeContext()) + { + customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.BackgroundTask); + } + + // check whether request is made by a search engine + // in this case return built-in customer record for search engines + if (customer == null && _userAgent.IsBot) + { + customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); + } + + // check whether request is made by the PDF converter + // in this case return built-in customer record for the converter + if (customer == null && _userAgent.IsPdfConverter) + { + customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.PdfConverter); + } + + return customer != null; + } + + protected virtual Customer GetGuestCustomer() + { + Customer customer = null; + Guid customerGuid = Guid.Empty; + + var anonymousId = _httpContext.Request.AnonymousID; + + if (anonymousId != null && anonymousId.HasValue()) + { + Guid.TryParse(anonymousId, out customerGuid); + } + + if (customerGuid == Guid.Empty) + { + _httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + _httpContext.Response.End(); + } + + // Try to load an existing record... + customer = _customerService.GetCustomerByGuid(customerGuid); + + if (customer == null || customer.Deleted || !customer.Active || customer.IsRegistered()) + { + // ...but no record yet. Create one. + customer = _customerService.InsertGuestCustomer(customerGuid); + } + + return customer; + } + + /// + /// Gets or sets the original customer (in case the current one is impersonated) + /// + public Customer OriginalCustomerIfImpersonated { get { @@ -243,11 +216,8 @@ public Language WorkingLanguage if (this.CurrentCustomer != null) { - customerLangId = this.CurrentCustomer.GetAttribute( - SystemCustomerAttributeNames.LanguageId, - _attrService, - _storeContext.CurrentStore.Id); - } + customerLangId = this.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.LanguageId, _attrService, storeId); + } #region Get language from URL (if possible) @@ -331,12 +301,8 @@ public Language WorkingLanguage private void SetCustomerLanguage(int languageId, int storeId) { - _attrService.SaveAttribute( - this.CurrentCustomer, - SystemCustomerAttributeNames.LanguageId, - languageId, - storeId); - } + _attrService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.LanguageId, languageId, storeId); + } /// /// Get or set current user working currency @@ -365,8 +331,8 @@ public Currency WorkingCurrency if (customer != null && !customer.IsSearchEngineAccount()) { // search engines should always crawl by primary store currency - var customerCurrencyId = customer.GetAttribute(SystemCustomerAttributeNames.CurrencyId, _attrService, _storeContext.CurrentStore.Id); - if (customerCurrencyId.GetValueOrDefault() > 0) + var customerCurrencyId = customer.GetAttribute(SystemCustomerAttributeNames.CurrencyId, _attrService, _storeContext.CurrentStore.Id); + if (customerCurrencyId.GetValueOrDefault() > 0) { if (storeCurrenciesMap.TryGetValue(customerCurrencyId.Value, out currency)) { @@ -428,7 +394,7 @@ public Currency WorkingCurrency { int? id = value != null ? value.Id : (int?)null; _attrService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.CurrencyId, id, _storeContext.CurrentStore.Id); - _cachedCurrency = null; + _cachedCurrency = null; } } @@ -455,10 +421,8 @@ public TaxDisplayType TaxDisplayType if (!_taxSettings.AllowCustomersToSelectTaxDisplayType) return; - _attrService.SaveAttribute(this.CurrentCustomer, - SystemCustomerAttributeNames.TaxDisplayTypeId, - (int)value, _storeContext.CurrentStore.Id); - } + _attrService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.TaxDisplayTypeId, (int)value, _storeContext.CurrentStore.Id); + } } public TaxDisplayType GetTaxDisplayTypeFor(Customer customer, int storeId) @@ -472,8 +436,9 @@ public TaxDisplayType GetTaxDisplayTypeFor(Customer customer, int storeId) if (_taxSettings.AllowCustomersToSelectTaxDisplayType && customer != null) { - taxDisplayType = customer.GetAttribute(SystemCustomerAttributeNames.TaxDisplayTypeId, storeId); - } + taxDisplayType = customer.GetAttribute(SystemCustomerAttributeNames.TaxDisplayTypeId, _attrService, storeId); + + } if (!taxDisplayType.HasValue && _taxSettings.EuVatEnabled) { @@ -538,6 +503,5 @@ public string GetDefaultLanguageSeoCode(int storeId = 0) { return _languageService.GetDefaultLanguageSeoCode(storeId); } - } } diff --git a/src/Presentation/SmartStore.Web.Framework/app.config b/src/Presentation/SmartStore.Web.Framework/app.config index 5d4773d412..751632b38f 100644 --- a/src/Presentation/SmartStore.Web.Framework/app.config +++ b/src/Presentation/SmartStore.Web.Framework/app.config @@ -24,7 +24,7 @@ - + diff --git a/src/Presentation/SmartStore.Web.Framework/packages.config b/src/Presentation/SmartStore.Web.Framework/packages.config index 06b8ceab7c..cbc00e31fa 100644 --- a/src/Presentation/SmartStore.Web.Framework/packages.config +++ b/src/Presentation/SmartStore.Web.Framework/packages.config @@ -24,7 +24,7 @@ - + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/500.html b/src/Presentation/SmartStore.Web/500.html index f9eaa260f1..741ebbcab5 100644 --- a/src/Presentation/SmartStore.Web/500.html +++ b/src/Presentation/SmartStore.Web/500.html @@ -41,7 +41,7 @@

- We apologize, an error ocurred while handling your request, this is not a problem with your computer or internet connection. + We apologize, an error occurred while handling your request, this is not a problem with your computer or internet connection. The details have been sent to our support team and we will investigate the issue very soon.

diff --git a/src/Presentation/SmartStore.Web/Administration/Content/admin.less b/src/Presentation/SmartStore.Web/Administration/Content/admin.less index 0932e0a4bd..5483f5971f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/admin.less +++ b/src/Presentation/SmartStore.Web/Administration/Content/admin.less @@ -405,7 +405,7 @@ td.adminData > input[type="checkbox"] { .title, .head { font-size: @fontSizeLarge; - padding: 8px 0; + padding: 12px 0; } .head { margin-bottom: 12px; @@ -1578,3 +1578,90 @@ table.payment-method-features td { display: inline-block; margin-left: 10px; } + + + + +/* Data exchange +-------------------------------------------------------------- */ +.profile-list { + margin-bottom: 10px; + border-bottom: 1px solid #ddd; +} + +.info-profile td { + padding-top: 20px; +} + +/* Import column mapping +-------------------------------------------------------------- */ +@media (max-width: 1280px) { + #ImportColumnMappings .right-label, + .column-mapping .mapping-button-label, + .mapping-list-item .right-label { + display: none; + } +} + +.column-mapping .right-label, +.mapping-list-item .right-label { + float: right; + font-size: 0.9em; +} +.mapping-list-item { + .ellipsis(); +} + +.column-mapping { + .column-one, .column-two { + width: 42%; + } + + .select-column, .select-property, .input-default { + width: 98%; + } + + th, td, .mapping-delete, .mapping-add, .mapping-apply, .mapping-cancel { + white-space: nowrap; + padding-left: 8px; + padding-right: 8px; + } + + .select2-container .select2-choice span { + margin-right: 12px; + white-space: nowrap; + } + + .mapping-edit { + white-space: nowrap; + + .select-column, .select-property { + width: 84%; + } + .select2-container { + float: left; + display: inline-block; + min-width: 220px; + margin-right: 4px; + } + } + + .mapping-item { + .item-inner { + white-space: nowrap; + cursor: pointer; + padding: 7px 0; + + .property-icon { + display: inline-block; + margin-right: 0; + } + .left-label { + display: inline-block; + } + .right-label { + margin-right: 20px; + } + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/LICENSE.txt b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/LICENSE.txt similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/LICENSE.txt rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/LICENSE.txt diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json new file mode 100644 index 0000000000..c08220bdfe --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json @@ -0,0 +1,33 @@ +{ + "FILES_ROOT": "~/Media/Uploaded", + "SESSION_PATH_KEY": "", + "THUMBS_VIEW_WIDTH": "140", + "THUMBS_VIEW_HEIGHT": "120", + "PREVIEW_THUMB_WIDTH": "300", + "PREVIEW_THUMB_HEIGHT": "200", + "MAX_IMAGE_WIDTH": "0", + "MAX_IMAGE_HEIGHT": "0", + "INTEGRATION": "ckeditor", + "DIRLIST": "../../../Admin/RoxyFileManager/ProcessRequest?a=DIRLIST", + "CREATEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=CREATEDIR", + "DELETEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=DELETEDIR", + "MOVEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=MOVEDIR", + "COPYDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=COPYDIR", + "RENAMEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=RENAMEDIR", + "FILESLIST": "../../../Admin/RoxyFileManager/ProcessRequest?a=FILESLIST", + "UPLOAD": "../../../Admin/RoxyFileManager/ProcessRequest?a=UPLOAD", + "DOWNLOAD": "../../../Admin/RoxyFileManager/ProcessRequest?a=DOWNLOAD", + "DOWNLOADDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=DOWNLOADDIR", + "DELETEFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=DELETEFILE", + "MOVEFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=MOVEFILE", + "COPYFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=COPYFILE", + "RENAMEFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=RENAMEFILE", + "GENERATETHUMB": "../../../Admin/RoxyFileManager/ProcessRequest?a=GENERATETHUMB", + "DEFAULTVIEW": "list", + "FORBIDDEN_UPLOADS": "zip js jsp jsb mhtml mht xhtml xht php phtml php3 php4 php5 phps shtml jhtml pl sh py cgi exe scr dll msi vbs bat com pif cmd vxd cpl htpasswd htaccess", + "ALLOWED_UPLOADS": "", + "FILEPERMISSIONS": "0644", + "DIRPERMISSIONS": "0755", + "LANG": "auto", + "DATEFORMAT": "dd.MM.yyyy HH:mm" +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/animated-overlay.gif b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/animated-overlay.gif similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/animated-overlay.gif rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/animated-overlay.gif diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_222222_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_222222_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_222222_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_222222_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_454545_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_454545_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_454545_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_454545_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_888888_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_888888_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_888888_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_888888_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/jquery-ui-1.10.4.custom.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.css similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/jquery-ui-1.10.4.custom.css rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.css diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/main.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.css similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/main.css rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.css diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/main.min.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.min.css similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/main.min.css rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.min.css diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/action-folder-paste.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-paste.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/action-folder-paste.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-paste.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/action-folder-rename.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-rename.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/action-folder-rename.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-rename.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/arrow_down.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_down.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/arrow_down.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_down.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/arrow_up.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_up.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/arrow_up.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_up.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/blank.gif b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/blank.gif similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/blank.gif rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/blank.gif diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/copy.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/copy.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/copy.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/copy.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/cut.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/cut.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/cut.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/cut.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/dir-minus.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-minus.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/dir-minus.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-minus.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/dir-plus.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-plus.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/dir-plus.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-plus.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-add.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-add.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-add.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-add.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-delete.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-delete.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-delete.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-delete.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-download.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-download.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-download.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-download.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-duplicate.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-duplicate.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-duplicate.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-duplicate.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-upload.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-upload.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-upload.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-upload.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_3gp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_3gp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_3gp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_3gp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_7z.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_7z.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_7z.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_7z.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ace.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ace.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ace.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ace.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ai.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ai.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ai.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ai.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_aif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_aif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_aiff.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aiff.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_aiff.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aiff.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_amr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_amr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_amr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_amr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_asf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_asf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_asx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_asx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bat.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bat.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bat.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bin.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bin.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bin.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bin.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bmp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bmp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bmp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bup.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bup.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bup.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bup.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cab.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cab.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cab.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cab.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cbr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cbr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cbr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cbr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cda.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cda.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cda.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cda.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cdl.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdl.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cdl.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdl.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cdr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cdr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_chm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_chm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_chm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_chm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dat.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dat.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dat.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_divx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_divx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_divx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_divx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dll.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dll.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dll.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dll.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dmg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dmg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dmg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dmg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_doc.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_doc.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_doc.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_doc.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dss.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dss.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dss.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dvf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dvf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dvf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dvf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dwg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dwg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dwg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dwg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_eml.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eml.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_eml.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eml.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_eps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_eps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_exe.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_exe.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_exe.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_exe.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_fla.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_fla.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_fla.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_fla.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_flv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_flv.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_flv.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_flv.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_gif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_gif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_gz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gz.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_gz.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gz.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_hqx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_hqx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_hqx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_hqx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_htm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_htm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_htm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_htm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_html.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_html.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_html.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_html.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ifo.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ifo.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ifo.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ifo.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_indd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_indd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_indd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_indd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_iso.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_iso.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_iso.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_iso.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jar.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jar.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jar.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpeg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jpeg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpeg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jpg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_lnk.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_lnk.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_lnk.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_lnk.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_log.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_log.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_log.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_log.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4a.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4a.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4a.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4a.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4b.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4b.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4b.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4b.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4p.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4p.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4p.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4p.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4v.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4v.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4v.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4v.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mcd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mcd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mcd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mdb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mdb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mdb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mdb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mid.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mid.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mid.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mid.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mov.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mov.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mov.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mov.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mp2.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp2.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mp2.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp2.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mp4.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp4.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mp4.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp4.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpeg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mpeg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpeg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mpg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_msi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_msi.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_msi.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_msi.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mswmm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mswmm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mswmm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mswmm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ogg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ogg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ogg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ogg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pdf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pdf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pdf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pdf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_png.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_png.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_png.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_png.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_psd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_psd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_psd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_psd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pst.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pst.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pst.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pst.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ptb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ptb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ptb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ptb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pub.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pub.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pub.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pub.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qbb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qbb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qbw.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbw.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qbw.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbw.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qxd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qxd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qxd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qxd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ram.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ram.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ram.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ram.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rar.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rar.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rar.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rmvb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rmvb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rmvb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rmvb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rtf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rtf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rtf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rtf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sea.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sea.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sea.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sea.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ses.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ses.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ses.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ses.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sit.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sit.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sit.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sit.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sitx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sitx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sitx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sitx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ss.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ss.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ss.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_swf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_swf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_swf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_swf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tgz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tgz.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tgz.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tgz.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_thm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_thm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_thm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_thm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tmp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tmp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tmp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_torrent.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_torrent.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_torrent.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_torrent.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ttf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ttf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ttf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ttf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_txt.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_txt.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_txt.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_txt.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_vcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vcd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_vcd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vcd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_vob.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vob.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_vob.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vob.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wav.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wav.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wav.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wav.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wma.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wma.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wma.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wma.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wmv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wmv.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wmv.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wmv.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_xls.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xls.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_xls.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xls.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_xpi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xpi.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_xpi.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xpi.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_zip.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_zip.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_zip.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_zip.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/unknown.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/unknown.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/unknown.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/unknown.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_3gp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_3gp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_3gp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_3gp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_7z.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_7z.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_7z.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_7z.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ace.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ace.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ace.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ace.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ai.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ai.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ai.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ai.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_aif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_aif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_aiff.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aiff.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_aiff.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aiff.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_amr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_amr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_amr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_amr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_asf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_asf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_asx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_asx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bat.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bat.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bat.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bin.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bin.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bin.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bin.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bmp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bmp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bmp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bup.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bup.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bup.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bup.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cab.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cab.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cab.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cab.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cbr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cbr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cbr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cbr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cda.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cda.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cda.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cda.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cdl.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdl.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cdl.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdl.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cdr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cdr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_chm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_chm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_chm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_chm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dat.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dat.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dat.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_divx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_divx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_divx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_divx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dll.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dll.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dll.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dll.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dmg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dmg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dmg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dmg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_doc.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_doc.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_doc.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_doc.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dss.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dss.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dss.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dvf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dvf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dvf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dvf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dwg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dwg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dwg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dwg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_eml.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eml.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_eml.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eml.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_eps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_eps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_exe.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_exe.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_exe.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_exe.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_fla.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_fla.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_fla.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_fla.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_flv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_flv.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_flv.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_flv.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_gif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_gif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_gz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gz.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_gz.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gz.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_hqx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_hqx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_hqx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_hqx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_htm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_htm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_htm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_htm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_html.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_html.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_html.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_html.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ifo.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ifo.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ifo.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ifo.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_indd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_indd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_indd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_indd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_iso.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_iso.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_iso.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_iso.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jar.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jar.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jar.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpeg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jpeg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpeg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jpg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_lnk.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_lnk.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_lnk.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_lnk.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_log.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_log.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_log.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_log.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4a.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4a.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4a.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4a.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4b.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4b.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4b.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4b.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4p.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4p.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4p.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4p.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4v.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4v.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4v.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4v.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mcd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mcd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mcd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mdb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mdb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mdb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mdb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mid.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mid.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mid.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mid.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mov.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mov.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mov.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mov.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mp2.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp2.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mp2.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp2.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mp4.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp4.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mp4.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp4.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpeg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mpeg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpeg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mpg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_msi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_msi.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_msi.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_msi.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mswmm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mswmm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mswmm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mswmm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ogg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ogg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ogg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ogg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pdf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pdf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pdf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pdf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_png.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_png.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_png.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_png.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_psd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_psd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_psd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_psd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pst.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pst.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pst.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pst.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ptb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ptb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ptb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ptb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pub.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pub.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pub.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pub.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qbb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qbb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qbw.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbw.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qbw.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbw.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qxd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qxd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qxd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qxd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ram.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ram.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ram.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ram.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rar.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rar.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rar.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rmvb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rmvb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rmvb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rmvb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rtf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rtf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rtf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rtf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sea.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sea.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sea.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sea.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ses.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ses.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ses.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ses.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sit.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sit.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sit.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sit.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sitx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sitx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sitx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sitx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ss.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ss.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ss.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_swf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_swf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_swf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_swf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tgz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tgz.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tgz.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tgz.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_thm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_thm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_thm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_thm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tmp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tmp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tmp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_torrent.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_torrent.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_torrent.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_torrent.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ttf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ttf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ttf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ttf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_txt.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_txt.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_txt.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_txt.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_vcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vcd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_vcd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vcd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_vob.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vob.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_vob.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vob.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wav.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wav.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wav.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wav.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wma.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wma.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wma.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wma.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wmv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wmv.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wmv.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wmv.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_xls.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xls.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_xls.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xls.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_xpi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xpi.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_xpi.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xpi.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_zip.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_zip.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_zip.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_zip.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/unknown.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/unknown.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/unknown.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/unknown.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/find.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/find.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/find.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/find.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-add.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-add.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-add.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-add.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-delete.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-delete.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-delete.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-delete.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-download.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-download.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-download.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-download.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-green.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-green.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-green.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-green.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-opened.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-opened.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-opened.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-opened.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/loading-dir.gif b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/loading-dir.gif similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/loading-dir.gif rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/loading-dir.gif diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/loading.gif b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/loading.gif similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/loading.gif rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/loading.gif diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/paste.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/paste.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/paste.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/paste.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/preview.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/preview.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/preview.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/preview.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/rename.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/rename.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/rename.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/rename.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/search.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/search.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/search.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/search.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/select.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/select.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/select.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/select.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/upload-big.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/upload-big.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/upload-big.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/upload-big.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/view-list.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-list.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/view-list.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-list.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/view-tile.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-tile.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/view-tile.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-tile.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/index.html b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/index.html similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/index.html rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/index.html diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/custom.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/custom.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/custom.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/custom.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/directory.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/directory.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/directory.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/directory.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/file.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/file.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/fileman-modified.zip b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/fileman-modified.zip similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/fileman-modified.zip rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/fileman-modified.zip diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/filetypes.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/filetypes.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/filetypes.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/filetypes.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-1.10.2.min.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-1.10.2.min.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-1.10.2.min.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-1.10.2.min.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-dateFormat.min.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-dateFormat.min.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-dateFormat.min.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-dateFormat.min.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-ui-1.10.4.custom.min.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-ui-1.10.4.custom.min.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-ui-1.10.4.custom.min.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-ui-1.10.4.custom.min.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/main.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/main.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/main.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/main.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/main.min.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/main.min.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/main.min.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/main.min.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/mini-main.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/mini-main.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/mini-main.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/mini-main.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/tiny_mce_popup.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/tiny_mce_popup.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/tiny_mce_popup.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/tiny_mce_popup.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/utils.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/utils.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/utils.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/utils.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/lang/bg.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/bg.json similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/lang/bg.json rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/bg.json diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/lang/de.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/de.json similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/lang/de.json rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/de.json diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/lang/en.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/en.json similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/lang/en.json rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/en.json diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/lang/fr.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/fr.json similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/lang/fr.json rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/fr.json diff --git a/src/Presentation/SmartStore.Web/Administration/Content/variables.less b/src/Presentation/SmartStore.Web/Administration/Content/variables.less index 1de0d20a5e..741c1de2bc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/variables.less +++ b/src/Presentation/SmartStore.Web/Administration/Content/variables.less @@ -71,7 +71,7 @@ // ------------------------- // Based on 14px font-size and 20px line-height -@fontSizeLarge: @baseFontSize * 1.25; // ~18px +@fontSizeLarge: @baseFontSize * 1.4; // ~18px @fontSizeSmall: @baseFontSize * 0.85; // ~12px @fontSizeMini: @baseFontSize * 0.75; // ~11px diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CheckoutAttributeController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CheckoutAttributeController.cs index 4ac32d8493..51db07dad9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CheckoutAttributeController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CheckoutAttributeController.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Web.Mvc; using SmartStore.Admin.Models.Orders; +using SmartStore.Core; +using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Logging; @@ -10,7 +12,9 @@ using SmartStore.Services.Localization; using SmartStore.Services.Orders; using SmartStore.Services.Security; +using SmartStore.Services.Stores; using SmartStore.Services.Tax; +using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Security; @@ -18,41 +22,49 @@ namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public class CheckoutAttributeController : AdminControllerBase { - #region Fields + #region Fields - private readonly ICheckoutAttributeService _checkoutAttributeService; + private readonly ICommonServices _services; + private readonly ICheckoutAttributeService _checkoutAttributeService; private readonly ILanguageService _languageService; private readonly ILocalizedEntityService _localizedEntityService; private readonly ITaxCategoryService _taxCategoryService; private readonly IMeasureService _measureService; private readonly MeasureSettings _measureSettings; private readonly ICustomerActivityService _customerActivityService; - private readonly ICommonServices _services; + private readonly IStoreMappingService _storeMappingService; - #endregion + private readonly AdminAreaSettings _adminAreaSettings; - #region Constructors + #endregion - public CheckoutAttributeController(ICheckoutAttributeService checkoutAttributeService, + #region Constructors + + public CheckoutAttributeController( + ICommonServices services, + ICheckoutAttributeService checkoutAttributeService, ILanguageService languageService, ILocalizedEntityService localizedEntityService, ITaxCategoryService taxCategoryService, ICustomerActivityService customerActivityService, IMeasureService measureService, MeasureSettings measureSettings, - ICommonServices services) + IStoreMappingService storeMappingService, + AdminAreaSettings adminAreaSettings) { - this._checkoutAttributeService = checkoutAttributeService; - this._languageService = languageService; - this._localizedEntityService = localizedEntityService; - this._taxCategoryService = taxCategoryService; - this._customerActivityService = customerActivityService; - this._measureService = measureService; - this._measureSettings = measureSettings; - this._services = services; + _services = services; + _checkoutAttributeService = checkoutAttributeService; + _languageService = languageService; + _localizedEntityService = localizedEntityService; + _taxCategoryService = taxCategoryService; + _customerActivityService = customerActivityService; + _measureService = measureService; + _measureSettings = measureSettings; + _storeMappingService = storeMappingService; + _adminAreaSettings = adminAreaSettings; } #endregion @@ -94,11 +106,27 @@ private void PrepareCheckoutAttributeModel(CheckoutAttributeModel model, Checkou if (model == null) throw new ArgumentNullException("model"); - //tax categories var taxCategories = _taxCategoryService.GetAllTaxCategories(); - foreach (var tc in taxCategories) - model.AvailableTaxCategories.Add(new SelectListItem() { Text = tc.Name, Value = tc.Id.ToString(), Selected = checkoutAttribute != null && !excludeProperties && tc.Id == checkoutAttribute.TaxCategoryId }); - } + + foreach (var tc in taxCategories) + { + model.AvailableTaxCategories.Add(new SelectListItem + { + Text = tc.Name, + Value = tc.Id.ToString(), + Selected = (checkoutAttribute != null && !excludeProperties && tc.Id == checkoutAttribute.TaxCategoryId) + }); + } + + model.AvailableStores = _services.StoreService.GetAllStores() + .Select(s => s.ToModel()) + .ToList(); + + if (!excludeProperties) + { + model.SelectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(checkoutAttribute); + } + } #endregion @@ -114,7 +142,14 @@ public ActionResult List() if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog)) return AccessDeniedView(); - return View(); + var model = new CheckoutAttributeListModel + { + GridPageSize = _adminAreaSettings.GridPageSize + }; + + model.AvailableStores = _services.StoreService.GetAllStores().ToSelectListItems(); + + return View(model); } [HttpPost, GridAction(EnableCustomBinding = true)] @@ -124,16 +159,17 @@ public ActionResult List(GridCommand command) if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog)) { - var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(true); + var query = _checkoutAttributeService.GetCheckoutAttributes(0, true); + var pagedList = new PagedList(query, command.Page - 1, command.PageSize); - model.Data = checkoutAttributes.Select(x => + model.Data = pagedList.Select(x => { var caModel = x.ToModel(); caModel.AttributeControlTypeName = x.AttributeControlType.GetLocalizedEnum(_services.Localization, _services.WorkContext); return caModel; }); - model.Total = checkoutAttributes.Count(); + model.Total = pagedList.TotalCount; } else { @@ -173,10 +209,13 @@ public ActionResult Create(CheckoutAttributeModel model, bool continueEditing) { var checkoutAttribute = model.ToEntity(); _checkoutAttributeService.InsertCheckoutAttribute(checkoutAttribute); + UpdateAttributeLocales(checkoutAttribute, model); - //activity log - _customerActivityService.InsertActivity("AddNewCheckoutAttribute", _services.Localization.GetResource("ActivityLog.AddNewCheckoutAttribute"), checkoutAttribute.Name); + _storeMappingService.SaveStoreMappings(checkoutAttribute, model.SelectedStoreIds); + + //activity log + _customerActivityService.InsertActivity("AddNewCheckoutAttribute", _services.Localization.GetResource("ActivityLog.AddNewCheckoutAttribute"), checkoutAttribute.Name); NotifySuccess(_services.Localization.GetResource("Admin.Catalog.Attributes.CheckoutAttributes.Added")); return continueEditing ? RedirectToAction("Edit", new { id = checkoutAttribute.Id }) : RedirectToAction("List"); @@ -195,7 +234,6 @@ public ActionResult Edit(int id) var checkoutAttribute = _checkoutAttributeService.GetCheckoutAttributeById(id); if (checkoutAttribute == null) - //No checkout attribute found with the specified id return RedirectToAction("List"); var model = checkoutAttribute.ToModel(); @@ -218,7 +256,6 @@ public ActionResult Edit(CheckoutAttributeModel model, bool continueEditing) var checkoutAttribute = _checkoutAttributeService.GetCheckoutAttributeById(model.Id); if (checkoutAttribute == null) - //No checkout attribute found with the specified id return RedirectToAction("List"); if (ModelState.IsValid) @@ -228,8 +265,10 @@ public ActionResult Edit(CheckoutAttributeModel model, bool continueEditing) UpdateAttributeLocales(checkoutAttribute, model); - //activity log - _customerActivityService.InsertActivity("EditCheckoutAttribute", _services.Localization.GetResource("ActivityLog.EditCheckoutAttribute"), checkoutAttribute.Name); + _storeMappingService.SaveStoreMappings(checkoutAttribute, model.SelectedStoreIds); + + //activity log + _customerActivityService.InsertActivity("EditCheckoutAttribute", _services.Localization.GetResource("ActivityLog.EditCheckoutAttribute"), checkoutAttribute.Name); NotifySuccess(_services.Localization.GetResource("Admin.Catalog.Attributes.CheckoutAttributes.Updated")); return continueEditing ? RedirectToAction("Edit", checkoutAttribute.Id) : RedirectToAction("List"); @@ -311,7 +350,6 @@ public ActionResult ValueCreatePopup(string btnId, string formId, CheckoutAttrib var checkoutAttribute = _checkoutAttributeService.GetCheckoutAttributeById(model.CheckoutAttributeId); if (checkoutAttribute == null) - //No checkout attribute found with the specified id return RedirectToAction("List"); model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; @@ -342,7 +380,6 @@ public ActionResult ValueEditPopup(int id) var cav = _checkoutAttributeService.GetCheckoutAttributeValueById(id); if (cav == null) - //No checkout attribute value found with the specified id return RedirectToAction("List"); var model = cav.ToModel(); @@ -366,7 +403,6 @@ public ActionResult ValueEditPopup(string btnId, string formId, CheckoutAttribut var cav = _checkoutAttributeService.GetCheckoutAttributeValueById(model.Id); if (cav == null) - //No checkout attribute value found with the specified id return RedirectToAction("List"); model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs index a8d2257e4a..4bebf35ee1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs @@ -519,36 +519,47 @@ public ActionResult Warnings() } // sitemap reachability - var sitemapReachable = false; + string sitemapUrl = null; try { - var sitemapUrl = Url.RouteUrl("SitemapSEO", (object)null, _securitySettings.Value.ForceSslForAllPages ? "https" : "http"); - var request = (HttpWebRequest)WebRequest.Create(sitemapUrl); + sitemapUrl = WebHelper.GetAbsoluteUrl(Url.RouteUrl("SitemapSEO"), this.Request); + var request = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(sitemapUrl)); request.Method = "HEAD"; request.Timeout = 15000; using (var response = (HttpWebResponse)request.GetResponse()) { - sitemapReachable = (response.StatusCode == HttpStatusCode.OK); - } - } - catch (WebException) { } + var status = response.StatusCode; + var warningModel = new SystemWarningModel(); + warningModel.Level = (status == HttpStatusCode.OK ? SystemWarningLevel.Pass : SystemWarningLevel.Warning); - if (sitemapReachable) - { - model.Add(new SystemWarningModel - { - Level = SystemWarningLevel.Pass, - Text = _localizationService.GetResource("Admin.System.Warnings.SitemapReachable.OK") - }); + switch (status) + { + case HttpStatusCode.OK: + warningModel.Text = T("Admin.System.Warnings.SitemapReachable.OK"); + break; + default: + if (status == HttpStatusCode.MethodNotAllowed) + warningModel.Text = T("Admin.System.Warnings.SitemapReachable.MethodNotAllowed"); + else + warningModel.Text = T("Admin.System.Warnings.SitemapReachable.Wrong"); + + warningModel.Text = string.Concat(warningModel.Text, " ", T("Admin.Common.HttpStatus", (int)status, status.ToString())); + break; + } + + model.Add(warningModel); + } } - else + catch (WebException exception) { model.Add(new SystemWarningModel { Level = SystemWarningLevel.Warning, - Text = _localizationService.GetResource("Admin.System.Warnings.SitemapReachable.Wrong") + Text = T("Admin.System.Warnings.SitemapReachable.Wrong") }); + + Logger.Warning(sitemapUrl.IsEmpty() ? "SitemapSEO" : sitemapUrl, exception); } //primary exchange rate currency diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ContentSliderController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ContentSliderController.cs index e9824617e3..73192b52db 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ContentSliderController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ContentSliderController.cs @@ -234,16 +234,11 @@ public ActionResult EditSlide(ContentSliderSlideModel model, bool continueEditin model.LanguageName = lang.Name; } - model.PictureUrl = _pictureService.GetPictureUrl(model.PictureId); + model.PictureUrl = _pictureService.GetPictureUrl(model.PictureId); - //delete an old picture (if deleted or updated) - int prevPictureId = _contentSliderSettings.Slides[index].PictureId; - if (prevPictureId > 0 && prevPictureId != model.PictureId) - { - var prevPicture = _pictureService.GetPictureById(prevPictureId); - if (prevPicture != null) - _pictureService.DeletePicture(prevPicture); - } + // delete an old picture (if deleted or updated) + int prevPictureId = _contentSliderSettings.Slides[index].PictureId; + MediaHelper.UpdatePictureTransientState(prevPictureId, model.PictureId, true); _contentSliderSettings.Slides[index] = model.ToEntity(); _settingService.SaveSetting(_contentSliderSettings); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs index ee08e3f976..44b6c10a0f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs @@ -974,19 +974,22 @@ public ActionResult ChangePassword(CustomerModel model) var customer = _customerService.GetCustomerById(model.Id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); if (ModelState.IsValid) { - var changePassRequest = new ChangePasswordRequest(model.Email, - false, _customerSettings.DefaultPasswordFormat, model.Password); + var changePassRequest = new ChangePasswordRequest(model.Email, false, _customerSettings.DefaultPasswordFormat, model.Password); var changePassResult = _customerRegistrationService.ChangePassword(changePassRequest); - if (changePassResult.Success) - NotifySuccess(_localizationService.GetResource("Admin.Customers.Customers.PasswordChanged")); - else - foreach (var error in changePassResult.Errors) + + if (changePassResult.Success) + { + NotifySuccess(_localizationService.GetResource("Admin.Customers.Customers.PasswordChanged")); + } + else + { + foreach (var error in changePassResult.Errors) NotifyError(error); + } } return RedirectToAction("Edit", customer.Id); @@ -1001,7 +1004,6 @@ public ActionResult MarkVatNumberAsValid(CustomerModel model) var customer = _customerService.GetCustomerById(model.Id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); _genericAttributeService.SaveAttribute(customer, @@ -1020,7 +1022,6 @@ public ActionResult MarkVatNumberAsInvalid(CustomerModel model) var customer = _customerService.GetCustomerById(model.Id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); _genericAttributeService.SaveAttribute(customer, @@ -1078,7 +1079,6 @@ public ActionResult Impersonate(int id) var customer = _customerService.GetCustomerById(id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); // ensure that a non-admin user cannot impersonate as an administrator @@ -1150,19 +1150,18 @@ public ActionResult SendPm(CustomerModel model) var customer = _customerService.GetCustomerById(model.Id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); try { if (!_forumSettings.AllowPrivateMessages) - throw new SmartException("Private messages are disabled"); + throw new SmartException(T("PrivateMessages.Disabled")); if (customer.IsGuest()) - throw new SmartException("Customer should be registered"); + throw new SmartException(T("Common.MethodNotSupportedForGuests")); if (String.IsNullOrWhiteSpace(model.SendPm.Subject)) - throw new SmartException("PM subject is empty"); + throw new SmartException(T("Admin.Customers.Customers.SendPM.Subject.Hint")); if (String.IsNullOrWhiteSpace(model.SendPm.Message)) - throw new SmartException("PM message is empty"); + throw new SmartException(T("Admin.Customers.Customers.SendPM.Message.Hint")); var privateMessage = new PrivateMessage @@ -1314,7 +1313,6 @@ public ActionResult AddressCreate(int customerId) var customer = _customerService.GetCustomerById(customerId); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); var model = new CustomerAddressModel(); @@ -1359,7 +1357,6 @@ public ActionResult AddressCreate(CustomerAddressModel model) var customer = _customerService.GetCustomerById(model.CustomerId); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); if (ModelState.IsValid) @@ -1408,12 +1405,10 @@ public ActionResult AddressEdit(int addressId, int customerId) var customer = _customerService.GetCustomerById(customerId); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); var address = _addressService.GetAddressById(addressId); if (address == null) - //No address found with the specified id return RedirectToAction("Edit", new { id = customer.Id }); var model = new CustomerAddressModel(); @@ -1466,12 +1461,10 @@ public ActionResult AddressEdit(CustomerAddressModel model) var customer = _customerService.GetCustomerById(model.CustomerId); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); var address = _addressService.GetAddressById(model.Address.Id); if (address == null) - //No address found with the specified id return RedirectToAction("Edit", new { id = customer.Id }); if (ModelState.IsValid) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/DownloadController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/DownloadController.cs index 7a6d174ad1..c215d9b7e9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/DownloadController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/DownloadController.cs @@ -73,7 +73,7 @@ public ActionResult AsyncUpload(bool minimalMode = false, string fieldName = nul var postedFile = Request.ToPostedFileResult(); if (postedFile == null) { - throw new ArgumentException("No file uploaded"); + throw new ArgumentException(T("Common.NoFileUploaded")); } var download = new Download diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/EmailAccountController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/EmailAccountController.cs index 2586a826cb..2d6716d284 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/EmailAccountController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/EmailAccountController.cs @@ -1,11 +1,8 @@ using System; using System.Linq; -using System.Net.Mail; -using System.Threading.Tasks; using System.Web.Mvc; using SmartStore.Admin.Models.Messages; using SmartStore.Core; -using SmartStore.Core.Domain; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Email; using SmartStore.Services.Configuration; @@ -184,28 +181,32 @@ public ActionResult SendTestEmail(EmailAccountModel model) var emailAccount = _emailAccountService.GetEmailAccountById(model.Id); if (emailAccount == null) - //No email account found with the specified id return RedirectToAction("List"); try { - if (String.IsNullOrWhiteSpace(model.SendTestEmailTo)) - throw new SmartException("Enter test email address"); + if (model.SendTestEmailTo.IsEmpty()) + { + NotifyError(T("Admin.Common.EnterEmailAdress")); + } + else + { + var to = new EmailAddress(model.SendTestEmailTo); + var from = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); + var subject = string.Concat(_storeContext.CurrentStore.Name, ". ", T("Admin.Configuration.EmailAccounts.TestingEmail")); + var body = T("Admin.Common.EmailSuccessfullySent"); - var to = new EmailAddress(model.SendTestEmailTo); - var from = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); - string subject = _storeContext.CurrentStore.Name + ". Testing email functionality."; - string body = "Email works fine."; + var msg = new EmailMessage(to, subject, body, from); - var msg = new EmailMessage(to, subject, body, from); - - _emailSender.SendEmail(new SmtpContext(emailAccount), msg); + _emailSender.SendEmail(new SmtpContext(emailAccount), msg); - NotifySuccess(_localizationService.GetResource("Admin.Configuration.EmailAccounts.SendTestEmail.Success"), false); + NotifySuccess(T("Admin.Configuration.EmailAccounts.SendTestEmail.Success"), false); + } } - catch (Exception exc) + catch (Exception exception) { - NotifyError(exc.Message, false); + model.TestEmailShortErrorMessage = exception.ToAllMessages(); + model.TestEmailFullErrorMessage = exception.ToString(); } //If we got this far, something failed, redisplay form diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs index 6d368aead9..95b26a0da0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Net.Mime; using System.Text; -using System.Web; using System.Web.Mvc; using SmartStore.Admin.Extensions; using SmartStore.Admin.Models.DataExchange; @@ -20,10 +19,10 @@ using SmartStore.Core.Domain.Stores; using SmartStore.Core.IO; using SmartStore.Core.Plugins; -using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Customers; using SmartStore.Services.DataExchange.Export; +using SmartStore.Services.DataExchange.Export.Deployment; using SmartStore.Services.Directory; using SmartStore.Services.Helpers; using SmartStore.Services.Localization; @@ -43,7 +42,6 @@ namespace SmartStore.Admin.Controllers [AdminAuthorize] public class ExportController : AdminControllerBase { - private readonly ICommonServices _services; private readonly IExportProfileService _exportService; private readonly PluginMediator _pluginMediator; private readonly ICategoryService _categoryService; @@ -61,7 +59,6 @@ public class ExportController : AdminControllerBase private readonly Lazy _customerSettings; public ExportController( - ICommonServices services, IExportProfileService exportService, PluginMediator pluginMediator, ICategoryService categoryService, @@ -78,7 +75,6 @@ public ExportController( IDataExporter dataExporter, Lazy customerSettings) { - _services = services; _exportService = exportService; _pluginMediator = pluginMediator; _categoryService = categoryService; @@ -113,42 +109,142 @@ private string GetThumbnailUrl(Provider provider) return url; } - private ExportProfileDetailsModel PrepareProfileDetailsModel(ExportProfile profile, bool forEdit) + private void ModelToEntity(ExportDeploymentModel model, ExportDeployment deployment) { - var model = new ExportProfileDetailsModel + deployment.ProfileId = model.ProfileId; + deployment.Name = model.Name; + deployment.Enabled = model.Enabled; + deployment.DeploymentType = model.DeploymentType; + deployment.Username = model.Username; + deployment.Password = model.Password; + deployment.Url = model.Url; + deployment.HttpTransmissionType = model.HttpTransmissionType; + deployment.FileSystemPath = model.FileSystemPath; + deployment.SubFolder = model.SubFolder; + deployment.EmailAddresses = model.EmailAddresses; + deployment.EmailSubject = model.EmailSubject; + deployment.EmailAccountId = model.EmailAccountId; + deployment.PassiveMode = model.PassiveMode; + deployment.UseSsl = model.UseSsl; + } + + private ActionResult SmartRedirect(bool continueEditing, int profileId, int deploymentId) + { + if (!continueEditing) { - Id = profile.Id, - PublicFiles = new List() + TempData["SelectedTab.export-profile-edit"] = new SelectedTabInfo + { + TabId = "export-profile-edit-6", + Path = Url.Action("Edit", new { id = profileId }) + }; + } + + return (continueEditing ? + RedirectToAction("EditDeployment", new { id = deploymentId }) : + RedirectToAction("Edit", new { id = profileId })); + } + + private void AddFileInfo(List list, string path, string publicFolderUrl = null, Store store = null) + { + if (System.IO.File.Exists(path) && !list.Any(x => x.FilePath == path)) + { + var fileInfo = new ExportFileDetailsModel.FileInfo(); + fileInfo.FilePath = path; + fileInfo.FileName = Path.GetFileName(path); + fileInfo.FileExtension = Path.GetExtension(path); + fileInfo.DisplayOrder = (fileInfo.FileExtension.IsCaseInsensitiveEqual(".zip") ? 0 : 1); + + if (store != null) + { + fileInfo.StoreId = store.Id; + fileInfo.StoreName = store.Name; + } + + if (publicFolderUrl.HasValue()) + { + fileInfo.FileUrl = publicFolderUrl + fileInfo.FileName; + } + + list.Add(fileInfo); + } + } + + private ExportFileDetailsModel CreateFileDetailsModel(ExportProfile profile, Provider provider, ExportDeployment deployment) + { + var model = new ExportFileDetailsModel + { + Id = (deployment == null ? profile.Id : deployment.Id), + IsForDeployment = (deployment != null), + ExportFiles = new List(), + PublicFiles = new List() }; try { + // add export files var zipPath = profile.GetExportZipPath(); - model.ZipPath = (System.IO.File.Exists(zipPath) ? zipPath : null); - model.ExportFiles = profile.GetExportFiles(); + if (deployment == null) + { + AddFileInfo(model.ExportFiles, zipPath); - if (forEdit && profile.Deployments.Any(x => x.IsPublic && x.DeploymentType == ExportDeploymentType.FileSystem)) + foreach (var path in profile.GetExportFiles(provider)) + { + AddFileInfo(model.ExportFiles, path); + } + } + else if (deployment.DeploymentType == ExportDeploymentType.FileSystem) { - var allStores = _services.StoreService.GetAllStores(); - var publicFolder = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder); + var deploymentFolder = deployment.GetDeploymentFolder(); var resultInfo = XmlHelper.Deserialize(profile.ResultInfo); - if (resultInfo != null && resultInfo.Files != null) + if (resultInfo.Files != null) { - foreach (var fileInfo in resultInfo.Files) + foreach (var file in resultInfo.Files) { - if (System.IO.File.Exists(Path.Combine(publicFolder, fileInfo.FileName)) && !model.PublicFiles.Any(x => x.FileName == fileInfo.FileName)) + AddFileInfo(model.ExportFiles, Path.Combine(deploymentFolder, file.FileName)); + } + } + } + + // add public files + ExportDeployment publicDeployment = null; + + if (deployment == null) + publicDeployment = profile.Deployments.FirstOrDefault(x => x.DeploymentType == ExportDeploymentType.PublicFolder); + else + publicDeployment = (deployment.DeploymentType == ExportDeploymentType.PublicFolder ? deployment : null); + + if (publicDeployment != null) + { + var currentStore = Services.StoreContext.CurrentStore; + var deploymentFolder = publicDeployment.GetDeploymentFolder(); + + // note public folder not cleaned up during export, so only display files that has been created during last export. + // otherwise the merchant might publish URLs of old export files. + if (profile.CreateZipArchive) + { + AddFileInfo( + model.PublicFiles, + Path.Combine(deploymentFolder, Path.GetFileName(zipPath)), + publicDeployment.GetPublicFolderUrl(Services, currentStore)); + } + else + { + var resultInfo = XmlHelper.Deserialize(profile.ResultInfo); + if (resultInfo.Files != null) + { + var allStores = Services.StoreService.GetAllStores(); + + foreach (var file in resultInfo.Files) { - var store = allStores.FirstOrDefault(y => y.Id == fileInfo.StoreId) ?? _services.StoreContext.CurrentStore; - - model.PublicFiles.Add(new ExportProfileDetailsModel.PublicFile - { - StoreId = store.Id, - StoreName = store.Name, - FileName = fileInfo.FileName, - FileUrl = string.Concat(store.Url.EnsureEndsWith("/"), DataExporter.PublicFolder.EnsureEndsWith("/"), fileInfo.FileName) - }); + var store = (file.StoreId == 0 ? null : allStores.FirstOrDefault(x => x.Id == file.StoreId)); + + AddFileInfo( + model.PublicFiles, + Path.Combine(deploymentFolder, file.FileName), + publicDeployment.GetPublicFolderUrl(Services, store ?? currentStore), + store); } } } @@ -162,7 +258,61 @@ private ExportProfileDetailsModel PrepareProfileDetailsModel(ExportProfile profi return model; } - private void PrepareProfileModel(ExportProfileModel model, ExportProfile profile, Provider provider, bool forEdit) + private ExportDeploymentModel CreateDeploymentModel(ExportProfile profile, ExportDeployment deployment, Provider provider, bool forEdit) + { + var model = new ExportDeploymentModel + { + Id = deployment.Id, + ProfileId = deployment.ProfileId, + Name = deployment.Name, + Enabled = deployment.Enabled, + DeploymentType = deployment.DeploymentType, + DeploymentTypeName = deployment.DeploymentType.GetLocalizedEnum(Services.Localization, Services.WorkContext), + Username = deployment.Username, + Password = deployment.Password, + Url = deployment.Url, + HttpTransmissionType = deployment.HttpTransmissionType, + FileSystemPath = deployment.FileSystemPath, + SubFolder = deployment.SubFolder, + EmailAddresses = deployment.EmailAddresses, + EmailSubject = deployment.EmailSubject, + EmailAccountId = deployment.EmailAccountId, + PassiveMode = deployment.PassiveMode, + UseSsl = deployment.UseSsl + }; + + model.PublicFolderUrl = deployment.GetPublicFolderUrl(Services); + + if (forEdit) + { + var allEmailAccounts = _emailAccountService.GetAllEmailAccounts(); + + model.CreateZip = profile.CreateZipArchive; + model.AvailableDeploymentTypes = ExportDeploymentType.FileSystem.ToSelectList(false).ToList(); + model.AvailableHttpTransmissionTypes = ExportHttpTransmissionType.SimplePost.ToSelectList(false).ToList(); + + model.SerializedEmailAddresses = string.Join(",", deployment.EmailAddresses.SplitSafe(",").Select(x => x.EncodeJsString())); + + model.AvailableEmailAccounts = allEmailAccounts + .Select(x => new SelectListItem { Text = x.FriendlyName, Value = x.Id.ToString() }) + .ToList(); + + if (provider != null) + { + model.ThumbnailUrl = GetThumbnailUrl(provider); + } + } + else + { + var fileDetailsModel = CreateFileDetailsModel(profile, null, deployment); + + model.FileCount = fileDetailsModel.FileCount; + } + + return model; + } + + private void PrepareProfileModel(ExportProfileModel model, ExportProfile profile, Provider provider) { model.Id = profile.Id; model.Name = profile.Name; @@ -183,36 +333,31 @@ private void PrepareProfileModel(ExportProfileModel model, ExportProfile profile model.Provider = new ExportProfileModel.ProviderModel(); model.Provider.ThumbnailUrl = GetThumbnailUrl(provider); - model.Details = PrepareProfileDetailsModel(profile, forEdit); + var descriptor = provider.Metadata.PluginDescriptor; - if (provider != null) + if (descriptor != null) { - var descriptor = provider.Metadata.PluginDescriptor; - - if (descriptor != null) - { - model.Provider.Url = descriptor.Url; - model.Provider.Author = descriptor.Author; - model.Provider.Version = descriptor.Version.ToString(); - } - - model.Provider.FriendlyName = _pluginMediator.GetLocalizedFriendlyName(provider.Metadata); - model.Provider.Description = _pluginMediator.GetLocalizedDescription(provider.Metadata); - model.Provider.EntityType = provider.Value.EntityType; - model.Provider.EntityTypeName = provider.Value.EntityType.GetLocalizedEnum(_services.Localization, _services.WorkContext); - model.Provider.FileExtension = provider.Value.FileExtension; + model.Provider.Url = descriptor.Url; + model.Provider.Author = descriptor.Author; + model.Provider.Version = descriptor.Version.ToString(); } - } + + model.Provider.FriendlyName = _pluginMediator.GetLocalizedFriendlyName(provider.Metadata); + model.Provider.Description = _pluginMediator.GetLocalizedDescription(provider.Metadata); + model.Provider.EntityType = provider.Value.EntityType; + model.Provider.EntityTypeName = provider.Value.EntityType.GetLocalizedEnum(Services.Localization, Services.WorkContext); + model.Provider.FileExtension = provider.Value.FileExtension; + } private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile profile, Provider provider) { var filter = XmlHelper.Deserialize(profile.Filtering); var projection = XmlHelper.Deserialize(profile.Projection); - var language = _services.WorkContext.WorkingLanguage; - var store = _services.StoreContext.CurrentStore; + var language = Services.WorkContext.WorkingLanguage; + var store = Services.StoreContext.CurrentStore; - var allStores = _services.StoreService.GetAllStores(); + var allStores = Services.StoreService.GetAllStores(); var allLanguages = _languageService.GetAllLanguages(true); var allCurrencies = _currencyService.GetAllCurrencies(true); var allEmailAccounts = _emailAccountService.GetAllEmailAccounts(); @@ -318,10 +463,19 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile model.Deployments = profile.Deployments .Select(x => { - var deploymentModel = PrepareDeploymentModel(x, null, false); + var deploymentModel = CreateDeploymentModel(profile, x, null, false); - deploymentModel.ProfileDetails = PrepareProfileDetailsModel(profile, true); + if (x.ResultInfo.HasValue()) + { + var resultInfo = XmlHelper.Deserialize(x.ResultInfo); + deploymentModel.LastResult = new ExportDeploymentModel.LastResultInfo + { + Execution = _dateTimeHelper.ConvertToUserTime(resultInfo.LastExecutionUtc, DateTimeKind.Utc), + ExecutionPretty = resultInfo.LastExecutionUtc.RelativeFormat(true, "f"), + Error = resultInfo.LastError + }; + } return deploymentModel; }) .ToList(); @@ -417,88 +571,6 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile } } - private ExportDeploymentModel PrepareDeploymentModel(ExportDeployment deployment, Provider provider, bool forEdit) - { - var model = new ExportDeploymentModel - { - Id = deployment.Id, - ProfileId = deployment.ProfileId, - Name = deployment.Name, - Enabled = deployment.Enabled, - IsPublic = deployment.IsPublic, - CreateZip = deployment.CreateZip, - DeploymentType = deployment.DeploymentType, - DeploymentTypeName = deployment.DeploymentType.GetLocalizedEnum(_services.Localization, _services.WorkContext), - Username = deployment.Username, - Password = deployment.Password, - Url = deployment.Url, - HttpTransmissionType = deployment.HttpTransmissionType, - FileSystemPath = deployment.FileSystemPath, - EmailAddresses = deployment.EmailAddresses, - EmailSubject = deployment.EmailSubject, - EmailAccountId = deployment.EmailAccountId, - PassiveMode = deployment.PassiveMode, - UseSsl = deployment.UseSsl - }; - - if (forEdit) - { - var allEmailAccounts = _emailAccountService.GetAllEmailAccounts(); - - model.AvailableDeploymentTypes = ExportDeploymentType.FileSystem.ToSelectList(false).ToList(); - model.AvailableHttpTransmissionTypes = ExportHttpTransmissionType.SimplePost.ToSelectList(false).ToList(); - - model.SerializedEmailAddresses = string.Join(",", deployment.EmailAddresses.SplitSafe(",").Select(x => x.EncodeJsString())); - - model.AvailableEmailAccounts = allEmailAccounts - .Select(x => new SelectListItem { Text = x.FriendlyName, Value = x.Id.ToString() }) - .ToList(); - - if (provider != null) - { - model.ThumbnailUrl = GetThumbnailUrl(provider); - } - } - - return model; - } - - private void ModelToEntity(ExportDeploymentModel model, ExportDeployment deployment) - { - deployment.ProfileId = model.ProfileId; - deployment.Name = model.Name; - deployment.Enabled = model.Enabled; - deployment.DeploymentType = model.DeploymentType; - deployment.IsPublic = model.IsPublic; - deployment.CreateZip = model.CreateZip; - deployment.Username = model.Username; - deployment.Password = model.Password; - deployment.Url = model.Url; - deployment.HttpTransmissionType = model.HttpTransmissionType; - deployment.FileSystemPath = model.FileSystemPath; - deployment.EmailAddresses = model.EmailAddresses; - deployment.EmailSubject = model.EmailSubject; - deployment.EmailAccountId = model.EmailAccountId; - deployment.PassiveMode = model.PassiveMode; - deployment.UseSsl = model.UseSsl; - } - - private ActionResult SmartRedirect(bool continueEditing, int profileId, int deploymentId) - { - if (!continueEditing) - { - TempData["SelectedTab.export-profile-edit"] = new SelectedTabInfo - { - TabId = "export-profile-edit-6", - Path = Url.Action("Edit", new { id = profileId }) - }; - } - - return (continueEditing ? - RedirectToAction("EditDeployment", new { id = deploymentId }) : - RedirectToAction("Edit", new { id = profileId })); - } - #endregion public ActionResult Index() @@ -508,7 +580,7 @@ public ActionResult Index() public ActionResult List() { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var providers = _exportService.LoadAllExportProviders(0, false).ToList(); @@ -521,10 +593,12 @@ public ActionResult List() if (provider != null) { var profileModel = new ExportProfileModel(); + var fileDetailsModel = CreateFileDetailsModel(profile, provider, null); - PrepareProfileModel(profileModel, profile, provider, false); + PrepareProfileModel(profileModel, profile, provider); - profileModel.TaskModel = profile.ScheduleTask.ToScheduleTaskModel(_services.Localization, _dateTimeHelper, Url); + profileModel.FileCount = fileDetailsModel.FileCount; + profileModel.TaskModel = profile.ScheduleTask.ToScheduleTaskModel(Services.Localization, _dateTimeHelper, Url); model.Add(profileModel); } @@ -535,33 +609,48 @@ public ActionResult List() public ActionResult ProfileListDetails(int profileId) { - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) { var profile = _exportService.GetExportProfileById(profileId); if (profile != null) { - var model = PrepareProfileDetailsModel(profile, false); - - return Json(new + var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider != null && !provider.Metadata.IsHidden) { - exportFileCount = this.RenderPartialViewToString("ProfileFileCount", model) - }, JsonRequestBehavior.AllowGet); + var model = CreateFileDetailsModel(profile, provider, null); + return Json(this.RenderPartialViewToString("ProfileFileCount", model.FileCount), JsonRequestBehavior.AllowGet); + } } } return new EmptyResult(); } - public ActionResult ProfileFileDetails(int profileId) + public ActionResult ProfileFileDetails(int profileId, int deploymentId) { - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) { - var profile = _exportService.GetExportProfileById(profileId); - if (profile != null) + if (profileId != 0) { - var model = PrepareProfileDetailsModel(profile, true); - - return PartialView(model); + var profile = _exportService.GetExportProfileById(profileId); + if (profile != null) + { + var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider != null && !provider.Metadata.IsHidden) + { + var model = CreateFileDetailsModel(profile, provider, null); + return PartialView(model); + } + } + } + else if (deploymentId != 0) + { + var deployment = _exportService.GetExportDeploymentById(deploymentId); + if (deployment != null) + { + var model = CreateFileDetailsModel(deployment.Profile, null, deployment); + return PartialView(model); + } } } @@ -570,7 +659,7 @@ public ActionResult ProfileFileDetails(int profileId) public ActionResult Create() { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return Content(T("Admin.AccessDenied.Description")); var count = 0; @@ -617,7 +706,7 @@ public ActionResult Create() [HttpPost] public ActionResult Create(ExportProfileModel model) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); if (model.ProviderSystemName.HasValue()) @@ -638,7 +727,7 @@ public ActionResult Create(ExportProfileModel model) public ActionResult Edit(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); @@ -646,12 +735,12 @@ public ActionResult Edit(int id) return RedirectToAction("List"); var provider = _exportService.LoadProvider(profile.ProviderSystemName); - if (provider.Metadata.IsHidden) + if (provider == null || provider.Metadata.IsHidden) return RedirectToAction("List"); var model = new ExportProfileModel(); - PrepareProfileModel(model, profile, provider, true); + PrepareProfileModel(model, profile, provider); PrepareProfileModelForEdit(model, profile, provider); return View(model); @@ -661,7 +750,7 @@ public ActionResult Edit(int id) [FormValueRequired("save", "save-continue")] public ActionResult Edit(ExportProfileModel model, bool continueEditing) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(model.Id); @@ -669,12 +758,12 @@ public ActionResult Edit(ExportProfileModel model, bool continueEditing) return RedirectToAction("List"); var provider = _exportService.LoadProvider(profile.ProviderSystemName); - if (provider.Metadata.IsHidden) + if (provider == null || provider.Metadata.IsHidden) return RedirectToAction("List"); if (!ModelState.IsValid) { - PrepareProfileModel(model, profile, provider, true); + PrepareProfileModel(model, profile, provider); PrepareProfileModelForEdit(model, profile, provider); return View(model); } @@ -794,7 +883,7 @@ public ActionResult Edit(ExportProfileModel model, bool continueEditing) [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); @@ -802,7 +891,7 @@ public ActionResult DeleteConfirmed(int id) return RedirectToAction("List"); var provider = _exportService.LoadProvider(profile.ProviderSystemName); - if (provider.Metadata.IsHidden) + if (provider == null || provider.Metadata.IsHidden) return RedirectToAction("List"); try @@ -823,7 +912,7 @@ public ActionResult DeleteConfirmed(int id) public ActionResult Preview(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); @@ -831,7 +920,7 @@ public ActionResult Preview(int id) return RedirectToAction("List"); var provider = _exportService.LoadProvider(profile.ProviderSystemName); - if (provider.Metadata.IsHidden) + if (provider == null || provider.Metadata.IsHidden) return RedirectToAction("List"); if (!profile.Enabled) @@ -862,7 +951,7 @@ public ActionResult Preview(int id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult PreviewList(GridCommand command, int id, int totalRecords) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) { NotifyAccessDenied(); @@ -905,7 +994,7 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) { Id = product.Id, ProductTypeId = product.ProductTypeId, - ProductTypeName = product.GetProductTypeLabel(_services.Localization), + ProductTypeName = product.GetProductTypeLabel(Services.Localization), ProductTypeLabelHint = product.ProductTypeLabelHint, Name = item.Name, Sku = item.Sku, @@ -986,7 +1075,7 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) var subscription = item.Entity as NewsLetterSubscription; if (allStores == null) - allStores = _services.StoreService.GetAllStores(); + allStores = Services.StoreService.GetAllStores(); var store = allStores.FirstOrDefault(x => x.Id == subscription.StoreId); @@ -1042,11 +1131,11 @@ public ActionResult Execute(int id, string selectedIds) return RedirectToAction("List"); var provider = _exportService.LoadProvider(profile.ProviderSystemName); - if (provider.Metadata.IsHidden) + if (provider == null || provider.Metadata.IsHidden) return RedirectToAction("List"); var taskParams = new Dictionary(); - taskParams.Add(TaskExecutor.CurrentCustomerIdParamName, _services.WorkContext.CurrentCustomer.Id.ToString()); + taskParams.Add(TaskExecutor.CurrentCustomerIdParamName, Services.WorkContext.CurrentCustomer.Id.ToString()); if (selectedIds.HasValue()) taskParams.Add("SelectedIds", selectedIds); @@ -1055,6 +1144,10 @@ public ActionResult Execute(int id, string selectedIds) NotifyInfo(T("Admin.System.ScheduleTasks.RunNow.Progress.DataExportTask")); + var referrer = Services.WebHelper.GetUrlReferrer(); + if (referrer.HasValue()) + return Redirect(referrer); + return RedirectToAction("List"); } @@ -1090,7 +1183,7 @@ public ActionResult InfoProfile(string systemName, string returnUrl) public ActionResult DownloadLogFile(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); @@ -1099,31 +1192,54 @@ public ActionResult DownloadLogFile(int id) var path = profile.GetExportLogPath(); if (System.IO.File.Exists(path)) { - var stream = new FileStream(path, FileMode.Open); - var result = new FileStreamResult(stream, MediaTypeNames.Text.Plain); + try + { + var stream = new FileStream(path, FileMode.Open); + var result = new FileStreamResult(stream, MediaTypeNames.Text.Plain); - return result; + return result; + } + catch (IOException) + { + NotifyWarning(T("Admin.Common.FileInUse")); + } } } return RedirectToAction("List"); } - public ActionResult DownloadExportFile(int id, string name) + public ActionResult DownloadExportFile(int id, string name, bool? isDeployment) { string message = null; + string path = null; - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) { - var profile = _exportService.GetExportProfileById(id); - if (profile != null) + if (isDeployment ?? false) { - var path = Path.Combine(profile.GetExportFolder(true), name); - - if (!System.IO.File.Exists(path)) - path = Path.Combine(profile.GetExportFolder(false), name); + var deployment = _exportService.GetExportDeploymentById(id); + if (deployment != null) + { + var deploymentFolder = deployment.GetDeploymentFolder(); + if (deploymentFolder.HasValue()) + path = Path.Combine(deploymentFolder, name); + } + } + else + { + var profile = _exportService.GetExportProfileById(id); + 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)) + if (System.IO.File.Exists(path)) + { + try { var stream = new FileStream(path, FileMode.Open); @@ -1132,6 +1248,10 @@ public ActionResult DownloadExportFile(int id, string name) return result; } + catch (IOException) + { + NotifyWarning(T("Admin.Common.FileInUse")); + } } } else @@ -1151,12 +1271,10 @@ public ActionResult ResolveFileNamePatternExample(int id, string pattern) { var profile = _exportService.GetExportProfileById(id); - _services.DbContext.DetachEntity(profile); + Services.DbContext.DetachEntity(profile); profile.FileNamePattern = pattern.EmptyNull(); - var provider = _exportService.LoadProvider(profile.ProviderSystemName); - - var resolvedPattern = profile.ResolveFileNamePattern(_services.StoreContext.CurrentStore, 1, _dataExchangeSettings.MaxFileNameLength); + var resolvedPattern = profile.ResolveFileNamePattern(Services.StoreContext.CurrentStore, 1, _dataExchangeSettings.MaxFileNameLength); return this.Content(resolvedPattern); } @@ -1165,7 +1283,7 @@ public ActionResult ResolveFileNamePatternExample(int id, string pattern) public ActionResult CreateDeployment(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); @@ -1176,9 +1294,7 @@ public ActionResult CreateDeployment(int id) if (provider.Metadata.IsHidden) return RedirectToAction("List"); - //var fileSystemName = ExportDeploymentType.FileSystem.GetLocalizedEnum(_services.Localization, _services.WorkContext); - - var model = PrepareDeploymentModel(new ExportDeployment + var model = CreateDeploymentModel(profile, new ExportDeployment { ProfileId = id, Enabled = true, @@ -1193,7 +1309,7 @@ public ActionResult CreateDeployment(int id) [FormValueRequired("save", "save-continue")] public ActionResult CreateDeployment(ExportDeploymentModel model, bool continueEditing, ExportDeploymentType deploymentType) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(model.ProfileId); @@ -1201,7 +1317,7 @@ public ActionResult CreateDeployment(ExportDeploymentModel model, bool continueE return RedirectToAction("List"); var provider = _exportService.LoadProvider(profile.ProviderSystemName); - if (provider.Metadata.IsHidden) + if (provider == null || provider.Metadata.IsHidden) return RedirectToAction("List"); if (ModelState.IsValid) @@ -1222,7 +1338,7 @@ public ActionResult CreateDeployment(ExportDeploymentModel model, bool continueE public ActionResult EditDeployment(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var deployment = _exportService.GetExportDeploymentById(id); @@ -1230,10 +1346,10 @@ public ActionResult EditDeployment(int id) return RedirectToAction("List"); var provider = _exportService.LoadProvider(deployment.Profile.ProviderSystemName); - if (provider.Metadata.IsHidden) + if (provider == null || provider.Metadata.IsHidden) return RedirectToAction("List"); - var model = PrepareDeploymentModel(deployment, provider, true); + var model = CreateDeploymentModel(deployment.Profile, deployment, provider, true); return View(model); } @@ -1242,7 +1358,7 @@ public ActionResult EditDeployment(int id) [FormValueRequired("save", "save-continue")] public ActionResult EditDeployment(ExportDeploymentModel model, bool continueEditing) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var deployment = _exportService.GetExportDeploymentById(model.Id); @@ -1250,7 +1366,7 @@ public ActionResult EditDeployment(ExportDeploymentModel model, bool continueEdi return RedirectToAction("List"); var provider = _exportService.LoadProvider(deployment.Profile.ProviderSystemName); - if (provider.Metadata.IsHidden) + if (provider == null || provider.Metadata.IsHidden) return RedirectToAction("List"); if (ModelState.IsValid) @@ -1264,14 +1380,15 @@ public ActionResult EditDeployment(ExportDeploymentModel model, bool continueEdi return SmartRedirect(continueEditing, deployment.ProfileId, deployment.Id); } - model = PrepareDeploymentModel(deployment, _exportService.LoadProvider(deployment.Profile.ProviderSystemName), true); + model = CreateDeploymentModel(deployment.Profile, deployment, _exportService.LoadProvider(deployment.Profile.ProviderSystemName), true); return View(model); } + [HttpPost] public ActionResult DeleteDeployment(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var deployment = _exportService.GetExportDeploymentById(id); @@ -1279,7 +1396,7 @@ public ActionResult DeleteDeployment(int id) return RedirectToAction("List"); var provider = _exportService.LoadProvider(deployment.Profile.ProviderSystemName); - if (provider.Metadata.IsHidden) + if (provider == null || provider.Metadata.IsHidden) return RedirectToAction("List"); int profileId = deployment.ProfileId; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/GiftCardController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/GiftCardController.cs index 586be5bb45..0900557d31 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/GiftCardController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/GiftCardController.cs @@ -270,6 +270,8 @@ public ActionResult NotifyRecipient(GiftCardModel model) { giftCard.IsRecipientNotified = true; _giftCardService.UpdateGiftCard(giftCard); + + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); } } catch (Exception exc) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs index 9603804170..00a216f5d4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs @@ -11,7 +11,6 @@ using SmartStore.Core.Domain; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.IO; -using SmartStore.Services; using SmartStore.Services.Catalog.Importer; using SmartStore.Services.Customers.Importer; using SmartStore.Services.DataExchange.Csv; @@ -32,20 +31,17 @@ namespace SmartStore.Admin.Controllers [AdminAuthorize] public class ImportController : AdminControllerBase { - private readonly ICommonServices _services; private readonly IImportProfileService _importProfileService; private readonly IDateTimeHelper _dateTimeHelper; private readonly ITaskScheduler _taskScheduler; private readonly ILanguageService _languageService; public ImportController( - ICommonServices services, IImportProfileService importService, IDateTimeHelper dateTimeHelper, ITaskScheduler taskScheduler, ILanguageService languageService) { - _services = services; _importProfileService = importService; _dateTimeHelper = dateTimeHelper; _taskScheduler = taskScheduler; @@ -54,14 +50,77 @@ public ImportController( #region Utilities + private bool IsValidImportFile(string path, out string error) + { + error = null; + + try + { + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var unused = LightweightDataTable.FromFile(path, stream, stream.Length, CsvConfiguration.ExcelFriendlyConfiguration, 0, 1); + } + + return true; + } + catch (Exception exception) + { + error = exception.ToAllMessages(); + + FileSystemHelper.Delete(path); + + return false; + } + } + + private bool IsDefaultValueDisabled(string column, string property, string[] disabledFieldNames) + { + if (disabledFieldNames.Contains(property)) + return true; + + string columnWithoutIndex, columnIndex; + if (ColumnMap.ParseSourceName(property, out columnWithoutIndex, out columnIndex)) + return disabledFieldNames.Contains(columnWithoutIndex); + + return false; + } + + private string[] GetDisabledDefaultFieldNames(ImportProfile profile) + { + switch (profile.EntityType) + { + case ImportEntityType.Product: + return new string[] { "Name", "Sku", "ManufacturerPartNumber", "Gtin", "SeName" }; + case ImportEntityType.Category: + return new string[] { "Name", "SeName" }; + case ImportEntityType.Customer: + return new string[] { "CustomerGuid", "Email" }; + case ImportEntityType.NewsLetterSubscription: + return new string[] { "Email" }; + default: + return new string[0]; + } + } + + private string GetPropertyDescription(Dictionary allProperties, string property) + { + if (property.HasValue() && allProperties.ContainsKey(property)) + { + var result = allProperties[property]; + if (result.HasValue()) + return result; + } + return property; + } + private void PrepareProfileModel(ImportProfileModel model, ImportProfile profile, bool forEdit, ColumnMap invalidMap = null) { model.Id = profile.Id; model.Name = profile.Name; model.EntityType = profile.EntityType; model.Enabled = profile.Enabled; - model.Skip = profile.Skip; - model.Take = profile.Take; + model.Skip = (profile.Skip == 0 ? (int?)null : profile.Skip); + model.Take = (profile.Take == 0 ? (int?)null : profile.Take); model.UpdateOnly = profile.UpdateOnly; model.KeyFieldNames = profile.KeyFieldNames.SplitSafe(",").Distinct().ToArray(); model.ScheduleTaskId = profile.SchedulingTaskId; @@ -69,11 +128,7 @@ private void PrepareProfileModel(ImportProfileModel model, ImportProfile profile model.IsTaskRunning = profile.ScheduleTask.IsRunning; model.IsTaskEnabled = profile.ScheduleTask.Enabled; model.LogFileExists = System.IO.File.Exists(profile.GetImportLogPath()); - model.EntityTypeName = profile.EntityType.GetLocalizedEnum(_services.Localization, _services.WorkContext); - model.UnspecifiedString = T("Common.Unspecified"); - model.AddNewString = T("Common.AddNew"); - model.DeleteString = T("Common.Delete"); - model.IgnoreString = T("Admin.Common.Ignore"); + model.EntityTypeName = profile.EntityType.GetLocalizedEnum(Services.Localization, Services.WorkContext); model.ExistingFileNames = profile.GetImportFiles() .Select(x => Path.GetFileName(x)) @@ -99,18 +154,22 @@ private void PrepareProfileModel(ImportProfileModel model, ImportProfile profile csvConfiguration = CsvConfiguration.ExcelFriendlyConfiguration; } + // common configuration + var extraData = XmlHelper.Deserialize(profile.ExtraData); + model.ExtraData.NumberOfPictures = extraData.NumberOfPictures; + // column mapping model.AvailableSourceColumns = new List(); - model.AvailableEntityProperties = new List(); + model.AvailableEntityProperties = new List(); model.AvailableKeyFieldNames = new List(); model.ColumnMappings = new List(); try { string[] availableKeyFieldNames = null; + string[] disabledDefaultFieldNames = GetDisabledDefaultFieldNames(profile); var mapConverter = new ColumnMapConverter(); var storedMap = mapConverter.ConvertFrom(profile.ColumnMapping); - var hasStoredMappings = (storedMap != null && storedMap.Mappings.Any()); var map = (invalidMap ?? storedMap) ?? new ColumnMap(); // property name to localized property name @@ -133,8 +192,17 @@ private void PrepareProfileModel(ImportProfileModel model, ImportProfile profile } model.AvailableEntityProperties = allProperties - .Select(x => new SelectListItem { Value = x.Key, Text = x.Value }) - .OrderBy(x => x.Text) + .Select(x => + { + var mapping = new ColumnMappingItemModel + { + Property = x.Key, + PropertyDescription = x.Value, + IsDefaultDisabled = IsDefaultValueDisabled(x.Key, x.Key, disabledDefaultFieldNames) + }; + + return mapping; + }) .ToList(); model.AvailableKeyFieldNames = availableKeyFieldNames @@ -156,17 +224,21 @@ private void PrepareProfileModel(ImportProfileModel model, ImportProfile profile { var mapping = new ColumnMappingItemModel { - Column = (x.Value.Property.IsEmpty() ? null : x.Key), - Property = (x.Value.Property.IsEmpty() ? x.Key : x.Value.Property), + Column = x.Value.MappedName, + Property = x.Key, Default = x.Value.Default }; - // add localized to make mappings sortable - if (allProperties.ContainsKey(mapping.Property)) + if (x.Value.IgnoreProperty) { - mapping.ColumnLocalized = allProperties[mapping.Property]; + // explicitly ignore the property + mapping.Column = null; + mapping.Default = null; } + mapping.PropertyDescription = GetPropertyDescription(allProperties, mapping.Property); + mapping.IsDefaultDisabled = IsDefaultValueDisabled(mapping.Column, mapping.Property, disabledDefaultFieldNames); + return mapping; }) .ToList(); @@ -184,58 +256,51 @@ private void PrepareProfileModel(ImportProfileModel model, ImportProfile profile foreach (var column in dataTable.Columns.Where(x => x.Name.HasValue())) { string columnWithoutIndex, columnIndex; - ColumnMap.ParseSourceColumn(column.Name, out columnWithoutIndex, out columnIndex); + ColumnMap.ParseSourceName(column.Name, out columnWithoutIndex, out columnIndex); - var mapModel = new ColumnMappingItemModel + model.AvailableSourceColumns.Add(new ColumnMappingItemModel { Index = dataTable.Columns.IndexOf(column), Column = column.Name, ColumnWithoutIndex = columnWithoutIndex, ColumnIndex = columnIndex, - ColumnLocalized = (allProperties.ContainsKey(column.Name) ? allProperties[column.Name] : column.Name) - }; - - model.AvailableSourceColumns.Add(mapModel); + PropertyDescription = GetPropertyDescription(allProperties, column.Name) + }); // auto map where field equals property name - if (!hasStoredMappings && !model.ColumnMappings.Any(x => x.Column == column.Name)) + if (!model.ColumnMappings.Any(x => x.Column == column.Name)) { var kvp = allProperties.FirstOrDefault(x => x.Key.IsCaseInsensitiveEqual(column.Name)); + if (kvp.Key.IsEmpty()) + { + var alternativeName = LightweightDataTable.GetAlternativeColumnNameFor(column.Name); + kvp = allProperties.FirstOrDefault(x => x.Key.IsCaseInsensitiveEqual(alternativeName)); + } - if (kvp.Key.HasValue()) + if (kvp.Key.HasValue() && !model.ColumnMappings.Any(x => x.Property == kvp.Key)) { model.ColumnMappings.Add(new ColumnMappingItemModel { Column = column.Name, Property = kvp.Key, - ColumnLocalized = kvp.Value + PropertyDescription = kvp.Value, + IsDefaultDisabled = IsDefaultValueDisabled(column.Name, kvp.Key, disabledDefaultFieldNames) }); } - else - { - var alternativeName = LightweightDataTable.GetAlternativeColumnNameFor(column.Name); - kvp = allProperties.FirstOrDefault(x => x.Key.IsCaseInsensitiveEqual(alternativeName)); - - if (kvp.Key.HasValue()) - { - model.ColumnMappings.Add(new ColumnMappingItemModel - { - Column = column.Name, - Property = kvp.Key, - ColumnLocalized = kvp.Value - }); - } - } } } // sorting model.AvailableSourceColumns = model.AvailableSourceColumns - .OrderBy(x => x.ColumnLocalized) + .OrderBy(x => x.PropertyDescription) + .ToList(); + + model.AvailableEntityProperties = model.AvailableEntityProperties + .OrderBy(x => x.PropertyDescription) .ToList(); model.ColumnMappings = model.ColumnMappings - .OrderBy(x => x.ColumnLocalized) + .OrderBy(x => x.PropertyDescription) .ToList(); } } @@ -254,7 +319,7 @@ public ActionResult Index() public ActionResult List() { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) return AccessDeniedView(); var model = new ImportProfileListModel @@ -271,7 +336,7 @@ public ActionResult List() PrepareProfileModel(profileModel, profile, false); - profileModel.TaskModel = profile.ScheduleTask.ToScheduleTaskModel(_services.Localization, _dateTimeHelper, Url); + profileModel.TaskModel = profile.ScheduleTask.ToScheduleTaskModel(Services.Localization, _dateTimeHelper, Url); model.Profiles.Add(profileModel); } @@ -281,7 +346,7 @@ public ActionResult List() public ActionResult ProfileListDetails(int profileId) { - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) { var profile = _importProfileService.GetImportProfileById(profileId); if (profile != null) @@ -302,7 +367,7 @@ public ActionResult ProfileListDetails(int profileId) [HttpPost] public ActionResult Create(ImportProfileModel model) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) return AccessDeniedView(); var importFile = Path.Combine(FileSystemHelper.TempDir(), model.TempFileName.EmptyNull()); @@ -330,7 +395,7 @@ public ActionResult Create(ImportProfileModel model) public ActionResult Edit(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) return AccessDeniedView(); var profile = _importProfileService.GetImportProfileById(id); @@ -347,14 +412,13 @@ public ActionResult Edit(int id) [FormValueRequired("save", "save-continue")] public ActionResult Edit(ImportProfileModel model, bool continueEditing, FormCollection form) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) return AccessDeniedView(); var profile = _importProfileService.GetImportProfileById(model.Id); if (profile == null) return RedirectToAction("List"); - var multipleMapped = new List(); var map = new ColumnMap(); var hasErrors = false; var resetMappings = false; @@ -374,18 +438,18 @@ public ActionResult Edit(ImportProfileModel model, bool continueEditing, FormCol var property = form[key]; var column = form["ColumnMapping.Column." + index]; var defaultValue = form["ColumnMapping.Default." + index]; - var result = true; - // ignored properties: column is empty means swap column and property (otherwise mapping impossible) if (column.IsEmpty()) - result = map.AddMapping(property, null, null); - else - result = map.AddMapping(column, null, property, defaultValue); - - if (!result) { - // add warning for ignored, multiple mapped properties - multipleMapped.Add("{0} ({1})".FormatInvariant(entityProperties.ContainsKey(property) ? entityProperties[property] : "".NaIfEmpty(), property)); + // tell mapper to explicitly ignore the property + map.AddMapping(property, null, property, "[IGNOREPROPERTY]"); + } + else if (!column.IsCaseInsensitiveEqual(property) || defaultValue.HasValue()) + { + if (defaultValue.HasValue() && GetDisabledDefaultFieldNames(profile).Contains(property)) + defaultValue = null; + + map.AddMapping(property, null, column, defaultValue); } } } @@ -405,8 +469,8 @@ public ActionResult Edit(ImportProfileModel model, bool continueEditing, FormCol profile.Name = model.Name; profile.EntityType = model.EntityType; profile.Enabled = model.Enabled; - profile.Skip = model.Skip; - profile.Take = model.Take; + profile.Skip = model.Skip ?? 0; + profile.Take = model.Take ?? 0; profile.UpdateOnly = model.UpdateOnly; profile.KeyFieldNames = (model.KeyFieldNames == null ? null : string.Join(",", model.KeyFieldNames)); @@ -438,6 +502,16 @@ public ActionResult Edit(ImportProfileModel model, bool continueEditing, FormCol var mapConverter = new ColumnMapConverter(); profile.ColumnMapping = mapConverter.ConvertTo(map); } + + if (model.ExtraData != null) + { + var extraData = new ImportExtraData + { + NumberOfPictures = model.ExtraData.NumberOfPictures + }; + + profile.ExtraData = XmlHelper.Serialize(extraData); + } } catch (Exception exception) { @@ -455,10 +529,6 @@ public ActionResult Edit(ImportProfileModel model, bool continueEditing, FormCol { NotifyWarning(T("Admin.DataExchange.ColumnMapping.Validate.MappingsReset")); } - else if (multipleMapped.Any()) - { - NotifyWarning(T("Admin.DataExchange.ColumnMapping.Validate.MultipleMappedIgnored", "

" + string.Join("
", multipleMapped) + "

")); - } } return (continueEditing ? RedirectToAction("Edit", new { id = profile.Id }) : RedirectToAction("List")); @@ -467,7 +537,7 @@ public ActionResult Edit(ImportProfileModel model, bool continueEditing, FormCol [HttpPost] public ActionResult ResetColumnMappings(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) return AccessDeniedView(); var profile = _importProfileService.GetImportProfileById(id); @@ -485,7 +555,7 @@ public ActionResult ResetColumnMappings(int id) [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) return AccessDeniedView(); var profile = _importProfileService.GetImportProfileById(id); @@ -515,7 +585,7 @@ public JsonResult FileUpload(int id) string error = null; string tempFile = ""; - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) { var postedFile = Request.ToPostedFileResult(); if (postedFile != null) @@ -526,9 +596,12 @@ public JsonResult FileUpload(int id) FileSystemHelper.Delete(path); success = postedFile.Stream.ToFile(path); - if (success) - tempFile = postedFile.FileName; + { + success = IsValidImportFile(path, out error); + if (success) + tempFile = postedFile.FileName; + } } else { @@ -548,16 +621,20 @@ public JsonResult FileUpload(int id) { var folder = profile.GetImportFolder(true, true); var fileName = Path.GetFileName(postedFile.FileName); + var path = Path.Combine(folder, fileName); - success = postedFile.Stream.ToFile(Path.Combine(folder, fileName)); - + success = postedFile.Stream.ToFile(path); if (success) { - var fileType = (Path.GetExtension(fileName).IsCaseInsensitiveEqual(".xlsx") ? ImportFileType.XLSX : ImportFileType.CSV); - if (fileType != profile.FileType) + success = IsValidImportFile(path, out error); + if (success) { - profile.FileType = fileType; - _importProfileService.UpdateImportProfile(profile); + var fileType = (Path.GetExtension(fileName).IsCaseInsensitiveEqual(".xlsx") ? ImportFileType.XLSX : ImportFileType.CSV); + if (fileType != profile.FileType) + { + profile.FileType = fileType; + _importProfileService.UpdateImportProfile(profile); + } } } } @@ -589,18 +666,22 @@ public ActionResult Execute(int id) return RedirectToAction("List"); var taskParams = new Dictionary(); - taskParams.Add(TaskExecutor.CurrentCustomerIdParamName, _services.WorkContext.CurrentCustomer.Id.ToString()); + taskParams.Add(TaskExecutor.CurrentCustomerIdParamName, Services.WorkContext.CurrentCustomer.Id.ToString()); _taskScheduler.RunSingleTask(profile.SchedulingTaskId, taskParams); NotifyInfo(T("Admin.System.ScheduleTasks.RunNow.Progress.DataImportTask")); + var referrer = Services.WebHelper.GetUrlReferrer(); + if (referrer.HasValue()) + return Redirect(referrer); + return RedirectToAction("List"); } public ActionResult DownloadLogFile(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) return AccessDeniedView(); var profile = _importProfileService.GetImportProfileById(id); @@ -609,10 +690,17 @@ public ActionResult DownloadLogFile(int id) var path = profile.GetImportLogPath(); if (System.IO.File.Exists(path)) { - var stream = new FileStream(path, FileMode.Open); - var result = new FileStreamResult(stream, MediaTypeNames.Text.Plain); + try + { + var stream = new FileStream(path, FileMode.Open); + var result = new FileStreamResult(stream, MediaTypeNames.Text.Plain); - return result; + return result; + } + catch (IOException) + { + NotifyWarning(T("Admin.Common.FileInUse")); + } } } @@ -623,7 +711,7 @@ public ActionResult DownloadImportFile(int id, string name) { string message = null; - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) { var profile = _importProfileService.GetImportProfileById(id); if (profile != null) @@ -635,11 +723,19 @@ public ActionResult DownloadImportFile(int id, string name) if (System.IO.File.Exists(path)) { - var stream = new FileStream(path, FileMode.Open); - var result = new FileStreamResult(stream, MimeTypes.MapNameToMimeType(path)); - result.FileDownloadName = Path.GetFileName(path); + try + { + var stream = new FileStream(path, FileMode.Open); - return result; + var result = new FileStreamResult(stream, MimeTypes.MapNameToMimeType(path)); + result.FileDownloadName = Path.GetFileName(path); + + return result; + } + catch (IOException) + { + NotifyWarning(T("Admin.Common.FileInUse")); + } } } } @@ -659,7 +755,7 @@ public ActionResult DownloadImportFile(int id, string name) [HttpPost] public ActionResult DeleteImportFile(int id, string name) { - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) { var profile = _importProfileService.GetImportProfileById(id); if (profile != null) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/MediaController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/MediaController.cs index aa64078b5f..b55656e0cc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/MediaController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/MediaController.cs @@ -53,7 +53,7 @@ private UploadFileResult UploadImageInternal() var postedFile = Request.ToPostedFileResult(); if (postedFile == null) { - return new UploadFileResult { Message = "No file uploaded" }; + return new UploadFileResult { Message = T("Common.NoFileUploaded") }; } if (postedFile.FileName.IsEmpty()) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/PackagingController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/PackagingController.cs index 9cbc47f5f6..12b1cb92dd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/PackagingController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/PackagingController.cs @@ -45,11 +45,6 @@ public ActionResult UploadPackage(bool isTheme) [HttpPost] public ActionResult UploadPackage(FormCollection form, string returnUrl = "") { - if (returnUrl.IsEmpty()) - { - returnUrl = _services.WebHelper.GetUrlReferrer(); - } - bool isTheme = false; try @@ -69,7 +64,7 @@ public ActionResult UploadPackage(FormCollection form, string returnUrl = "") if (!file.FileExtension.IsCaseInsensitiveEqual(".nupkg")) { NotifyError(T("Admin.Packaging.NotAPackage")); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } var location = CommonHelper.MapPath("~/App_Data"); @@ -102,7 +97,7 @@ public ActionResult UploadPackage(FormCollection form, string returnUrl = "") else { NotifyError(T("Admin.Common.UploadFile")); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } if (!isTheme) @@ -110,13 +105,13 @@ public ActionResult UploadPackage(FormCollection form, string returnUrl = "") _services.WebHelper.RestartAppDomain(); } NotifySuccess(T("Admin.Packaging.InstallSuccess")); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } catch (Exception exc) { NotifyError(exc); Logger.Error(exc); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs index c3a1a7c9f9..971c96eaa9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs @@ -255,8 +255,8 @@ public ActionResult ReloadList() if (!_permissionService.Authorize(StandardPermissionProvider.ManagePlugins)) return AccessDeniedView(); - //restart application - _services.WebHelper.RestartAppDomain(); + // restart application + _services.WebHelper.RestartAppDomain(aggressive: true); return RedirectToAction("List"); } @@ -574,12 +574,7 @@ public ActionResult UpdateStringResources(string systemName, string returnUrl = NotifySuccess(T("Admin.Configuration.Plugins.Resources.UpdateSuccess")); } - if (returnUrl.IsEmpty()) - { - return RedirectToAction("List"); - } - - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl, () => RedirectToAction("List")); } public ActionResult UpdateAllStringResources() diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs index 46fc08645f..752a19c41c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Data; using System.Linq; +using System.Web; using System.Web.Mvc; using Autofac; using Newtonsoft.Json; @@ -215,7 +216,7 @@ protected void UpdateProductGeneralInfo(Product product, ProductModel model) p.GiftCardTypeId = m.GiftCardTypeId; p.IsDownload = m.IsDownload; - p.DownloadId = m.DownloadId; + p.DownloadId = m.DownloadId ?? 0; p.UnlimitedDownloads = m.UnlimitedDownloads; p.MaxNumberOfDownloads = m.MaxNumberOfDownloads; p.DownloadExpirationDays = m.DownloadExpirationDays; @@ -2313,7 +2314,7 @@ public ActionResult ProductSpecAttrList(GridCommand command, int productId) }); } - attr.SpecificationAttributeOptionsJsonString = JsonConvert.SerializeObject(attr.SpecificationAttributeOptions); + attr.SpecificationAttributeOptionsJsonString = HttpUtility.HtmlEncode(JsonConvert.SerializeObject(attr.SpecificationAttributeOptions)); } model.Data = productrSpecsModel; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs index 6bc007e811..7814196042 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs @@ -279,6 +279,18 @@ public ActionResult DownloadAttachment(int id) { path = CommonHelper.MapPath(VirtualPathUtility.ToAppRelative(path), false); } + + if (!System.IO.File.Exists(path)) + { + NotifyError(string.Concat(T("Admin.Common.FileNotFound"), ": ", path)); + + var referrer = Services.WebHelper.GetUrlReferrer(); + if (referrer.HasValue()) + return Redirect(referrer); + + return RedirectToAction("List"); + } + return File(path, qea.MimeType, qea.Name); } @@ -290,7 +302,6 @@ public ActionResult DownloadAttachment(int id) NotifyError(T("Admin.System.QueuedEmails.CouldNotDownloadAttachment")); return RedirectToAction("List"); - } } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs new file mode 100644 index 0000000000..e0c2ca36c2 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs @@ -0,0 +1,828 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.IO.Compression; +using System.Text.RegularExpressions; +using System.Web; +using SmartStore.Services.Security; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; + +namespace SmartStore.Admin.Controllers +{ + [AdminAuthorize] + public class RoxyFileManagerController : AdminControllerBase + { + private const string CONFIG_FILE = "~/Administration/Content/filemanager/conf.json"; + + private Dictionary _lang = null; + private Dictionary _settings = null; + + private readonly IPermissionService _permissionService; + private readonly HttpContextBase _context; + private readonly HttpResponseBase _response; + + public RoxyFileManagerController( + IPermissionService permissionService, + HttpContextBase context) + { + _permissionService = permissionService; + _context = context; + _response = _context.Response; + } + + #region Utilities + + private string FixPath(string path) + { + if (path == null) + { + path = ""; + } + + if (!path.StartsWith("~")) + { + if (!path.StartsWith("/")) + path = "/" + path; + path = "~" + path; + } + + //var filesRoot = GetSetting("FILES_ROOT"); + //if (!path.ToLowerInvariant().Contains(filesRoot.ToLowerInvariant())) + //{ + // path = filesRoot; + //} + + return _context.Server.MapPath(path); + } + + private string GetLangFile() + { + //return "../lang/en.json"; + + var filename = "../lang/" + GetSetting("LANG") + ".json"; + + if (!System.IO.File.Exists(_context.Server.MapPath(filename))) + filename = "../lang/en.json"; + + return filename; + } + + protected string LangRes(string name) + { + string ret = name; + if (_lang == null) + _lang = ParseJSON(GetLangFile()); + if (_lang.ContainsKey(name)) + ret = _lang[name]; + + return ret; + } + + protected string GetFileType(string ext) + { + string ret = "file"; + ext = ext.ToLower(); + if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif") + ret = "image"; + else if (ext == ".swf" || ext == ".flv") + ret = "flash"; + return ret; + } + + protected bool CanHandleFile(string filename) + { + bool ret = false; + FileInfo file = new FileInfo(filename); + string ext = file.Extension.Replace(".", "").ToLower(); + string setting = GetSetting("FORBIDDEN_UPLOADS").Trim().ToLower(); + if (setting != "") + { + ArrayList tmp = new ArrayList(); + tmp.AddRange(Regex.Split(setting, "\\s+")); + if (!tmp.Contains(ext)) + ret = true; + } + setting = GetSetting("ALLOWED_UPLOADS").Trim().ToLower(); + if (setting != "") + { + ArrayList tmp = new ArrayList(); + tmp.AddRange(Regex.Split(setting, "\\s+")); + if (!tmp.Contains(ext)) + ret = false; + } + + return ret; + } + + protected Dictionary ParseJSON(string file) + { + Dictionary ret = new Dictionary(); + string json = ""; + try + { + json = System.IO.File.ReadAllText(_context.Server.MapPath(file), System.Text.Encoding.UTF8); + } + catch { } + + json = json.Trim(); + if (json != "") + { + if (json.StartsWith("{")) + json = json.Substring(1, json.Length - 2); + json = json.Trim(); + json = json.Substring(1, json.Length - 2); + string[] lines = Regex.Split(json, "\"\\s*,\\s*\""); + foreach (string line in lines) + { + string[] tmp = Regex.Split(line, "\"\\s*:\\s*\""); + try + { + if (tmp[0] != "" && !ret.ContainsKey(tmp[0])) + { + ret.Add(tmp[0], tmp[1]); + } + } + catch { } + } + } + return ret; + } + + protected string GetFilesRoot() + { + string ret = GetSetting("FILES_ROOT"); + if (GetSetting("SESSION_PATH_KEY") != "" && _context.Session[GetSetting("SESSION_PATH_KEY")] != null) + ret = (string)_context.Session[GetSetting("SESSION_PATH_KEY")]; + + if (ret == "") + ret = _context.Server.MapPath("../Uploads"); + else + ret = FixPath(ret); + return ret; + } + + protected void LoadConf() + { + if (_settings == null) + _settings = ParseJSON(CONFIG_FILE); + } + + protected string GetSetting(string name) + { + string ret = ""; + LoadConf(); + if (_settings.ContainsKey(name)) + ret = _settings[name]; + + return ret; + } + + protected void CheckPath(string path) + { + if (FixPath(path).IndexOf(GetFilesRoot()) != 0) + { + throw new Exception("Access to " + path + " is denied"); + } + } + + protected void VerifyAction(string action) + { + string setting = GetSetting(action); + if (setting.IndexOf("?") > -1) + setting = setting.Substring(0, setting.IndexOf("?")); + if (!setting.StartsWith("/")) + setting = "/" + setting; + setting = ".." + setting; + + if (_context.Server.MapPath(setting) != _context.Server.MapPath(_context.Request.Url.LocalPath)) + throw new Exception(LangRes("E_ActionDisabled")); + } + + protected string GetResultStr(string type, string msg) + { + return "{\"res\":\"" + type + "\",\"msg\":\"" + msg.Replace("\"", "\\\"") + "\"}"; + } + + protected string GetSuccessRes(string msg) + { + return GetResultStr("ok", msg); + } + + protected string GetSuccessRes() + { + return GetSuccessRes(""); + } + + protected string GetErrorRes(string msg) + { + return GetResultStr("error", msg); + } + + private void _copyDir(string path, string dest) + { + if (!Directory.Exists(dest)) + Directory.CreateDirectory(dest); + foreach (string f in Directory.GetFiles(path)) + { + FileInfo file = new FileInfo(f); + if (!System.IO.File.Exists(Path.Combine(dest, file.Name))) + { + System.IO.File.Copy(f, Path.Combine(dest, file.Name)); + } + } + foreach (string d in Directory.GetDirectories(path)) + { + DirectoryInfo dir = new DirectoryInfo(d); + _copyDir(d, Path.Combine(dest, dir.Name)); + } + } + + protected void CopyDir(string path, string newPath) + { + CheckPath(path); + CheckPath(newPath); + DirectoryInfo dir = new DirectoryInfo(FixPath(path)); + DirectoryInfo newDir = new DirectoryInfo(FixPath(newPath + "/" + dir.Name)); + + if (!dir.Exists) + { + throw new Exception(LangRes("E_CopyDirInvalidPath")); + } + else if (newDir.Exists) + { + throw new Exception(LangRes("E_DirAlreadyExists")); + } + else + { + _copyDir(dir.FullName, newDir.FullName); + } + _response.Write(GetSuccessRes()); + } + + protected string MakeUniqueFilename(string dir, string filename) + { + string ret = filename; + int i = 0; + while (System.IO.File.Exists(Path.Combine(dir, ret))) + { + i++; + ret = Path.GetFileNameWithoutExtension(filename) + " - Copy " + i.ToString() + Path.GetExtension(filename); + } + return ret; + } + + protected void CopyFile(string path, string newPath) + { + CheckPath(path); + FileInfo file = new FileInfo(FixPath(path)); + newPath = FixPath(newPath); + if (!file.Exists) + throw new Exception(LangRes("E_CopyFileInvalisPath")); + else + { + string newName = MakeUniqueFilename(newPath, file.Name); + try + { + System.IO.File.Copy(file.FullName, Path.Combine(newPath, newName)); + _response.Write(GetSuccessRes()); + } + catch + { + throw new Exception(LangRes("E_CopyFile")); + } + } + } + + protected void CreateDir(string path, string name) + { + CheckPath(path); + path = FixPath(path); + if (!Directory.Exists(path)) + throw new Exception(LangRes("E_CreateDirInvalidPath")); + else + { + try + { + path = Path.Combine(path, name); + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + _response.Write(GetSuccessRes()); + } + catch + { + throw new Exception(LangRes("E_CreateDirFailed")); + } + } + } + + protected void DeleteDir(string path) + { + CheckPath(path); + path = FixPath(path); + if (!Directory.Exists(path)) + throw new Exception(LangRes("E_DeleteDirInvalidPath")); + else if (path == GetFilesRoot()) + throw new Exception(LangRes("E_CannotDeleteRoot")); + else if (Directory.GetDirectories(path).Length > 0 || Directory.GetFiles(path).Length > 0) + throw new Exception(LangRes("E_DeleteNonEmpty")); + else + { + try + { + Directory.Delete(path); + _response.Write(GetSuccessRes()); + } + catch + { + throw new Exception(LangRes("E_CannotDeleteDir")); + } + } + } + + protected void DeleteFile(string path) + { + CheckPath(path); + path = FixPath(path); + if (!System.IO.File.Exists(path)) + throw new Exception(LangRes("E_DeleteFileInvalidPath")); + else + { + try + { + System.IO.File.Delete(path); + _response.Write(GetSuccessRes()); + } + catch + { + throw new Exception(LangRes("E_DeletеFile")); + } + } + } + + private List GetFiles(string path, string type) + { + List ret = new List(); + if (type == "#") + type = ""; + string[] files = Directory.GetFiles(path); + foreach (string f in files) + { + if ((GetFileType(new FileInfo(f).Extension) == type) || (type == "")) + ret.Add(f); + } + return ret; + } + + private ArrayList ListDirs(string path) + { + string[] dirs = Directory.GetDirectories(path); + ArrayList ret = new ArrayList(); + foreach (string dir in dirs) + { + ret.Add(dir); + ret.AddRange(ListDirs(dir)); + } + return ret; + } + + protected void ListDirTree(string type) + { + DirectoryInfo d = new DirectoryInfo(GetFilesRoot()); + if (!d.Exists) + throw new Exception("Invalid files root directory. Check your configuration."); + + ArrayList dirs = ListDirs(d.FullName); + dirs.Insert(0, d.FullName); + + string localPath = _context.Server.MapPath("~/"); + _response.Write("["); + for (int i = 0; i < dirs.Count; i++) + { + string dir = (string)dirs[i]; + _response.Write("{\"p\":\"/" + dir.Replace(localPath, "").Replace("\\", "/") + "\",\"f\":\"" + GetFiles(dir, type).Count.ToString() + "\",\"d\":\"" + Directory.GetDirectories(dir).Length.ToString() + "\"}"); + if (i < dirs.Count - 1) + _response.Write(","); + } + _response.Write("]"); + } + + protected double LinuxTimestamp(DateTime d) + { + DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0).ToLocalTime(); + TimeSpan timeSpan = (d.ToLocalTime() - epoch); + + return timeSpan.TotalSeconds; + + } + + protected void ListFiles(string path, string type) + { + CheckPath(path); + string fullPath = FixPath(path); + List files = GetFiles(fullPath, type); + _response.Write("["); + for (int i = 0; i < files.Count; i++) + { + FileInfo f = new FileInfo(files[i]); + int w = 0, h = 0; + if (GetFileType(f.Extension) == "image") + { + try + { + FileStream fs = new FileStream(f.FullName, FileMode.Open); + Image img = Image.FromStream(fs); + w = img.Width; + h = img.Height; + fs.Close(); + fs.Dispose(); + img.Dispose(); + } + catch (Exception ex) + { + throw ex; + } + } + _response.Write("{"); + _response.Write("\"p\":\"" + path + "/" + f.Name + "\""); + _response.Write(",\"t\":\"" + Math.Ceiling(LinuxTimestamp(f.LastWriteTime)).ToString() + "\""); + _response.Write(",\"s\":\"" + f.Length.ToString() + "\""); + _response.Write(",\"w\":\"" + w.ToString() + "\""); + _response.Write(",\"h\":\"" + h.ToString() + "\""); + _response.Write("}"); + if (i < files.Count - 1) + _response.Write(","); + } + _response.Write("]"); + } + + public void DownloadDir(string path) + { + path = FixPath(path); + if (!Directory.Exists(path)) + throw new Exception(LangRes("E_CreateArchive")); + string dirName = new FileInfo(path).Name; + string tmpZip = _context.Server.MapPath("../tmp/" + dirName + ".zip"); + if (System.IO.File.Exists(tmpZip)) + System.IO.File.Delete(tmpZip); + ZipFile.CreateFromDirectory(path, tmpZip, CompressionLevel.Fastest, true); + _response.Clear(); + _response.Headers.Add("Content-Disposition", "attachment; filename=\"" + dirName + ".zip\""); + _response.ContentType = "application/force-download"; + _response.TransmitFile(tmpZip); + _response.Flush(); + System.IO.File.Delete(tmpZip); + _response.End(); + } + + protected void DownloadFile(string path) + { + CheckPath(path); + FileInfo file = new FileInfo(FixPath(path)); + if (file.Exists) + { + _response.Clear(); + _response.Headers.Add("Content-Disposition", "attachment; filename=\"" + file.Name + "\""); + _response.ContentType = "application/force-download"; + _response.TransmitFile(file.FullName); + _response.Flush(); + _response.End(); + } + } + + protected void MoveDir(string path, string newPath) + { + CheckPath(path); + CheckPath(newPath); + DirectoryInfo source = new DirectoryInfo(FixPath(path)); + DirectoryInfo dest = new DirectoryInfo(FixPath(Path.Combine(newPath, source.Name))); + if (dest.FullName.IndexOf(source.FullName) == 0) + throw new Exception(LangRes("E_CannotMoveDirToChild")); + else if (!source.Exists) + throw new Exception(LangRes("E_MoveDirInvalisPath")); + else if (dest.Exists) + throw new Exception(LangRes("E_DirAlreadyExists")); + else + { + try + { + source.MoveTo(dest.FullName); + _response.Write(GetSuccessRes()); + } + catch + { + throw new Exception(LangRes("E_MoveDir") + " \"" + path + "\""); + } + } + } + + protected void MoveFile(string path, string newPath) + { + CheckPath(path); + CheckPath(newPath); + FileInfo source = new FileInfo(FixPath(path)); + FileInfo dest = new FileInfo(FixPath(newPath)); + if (!source.Exists) + throw new Exception(LangRes("E_MoveFileInvalisPath")); + else if (dest.Exists) + throw new Exception(LangRes("E_MoveFileAlreadyExists")); + else + { + try + { + source.MoveTo(dest.FullName); + _response.Write(GetSuccessRes()); + } + catch + { + throw new Exception(LangRes("E_MoveFile") + " \"" + path + "\""); + } + } + } + + protected void RenameDir(string path, string name) + { + CheckPath(path); + DirectoryInfo source = new DirectoryInfo(FixPath(path)); + DirectoryInfo dest = new DirectoryInfo(Path.Combine(source.Parent.FullName, name)); + if (source.FullName == GetFilesRoot()) + throw new Exception(LangRes("E_CannotRenameRoot")); + else if (!source.Exists) + throw new Exception(LangRes("E_RenameDirInvalidPath")); + else if (dest.Exists) + throw new Exception(LangRes("E_DirAlreadyExists")); + else + { + try + { + source.MoveTo(dest.FullName); + _response.Write(GetSuccessRes()); + } + catch + { + throw new Exception(LangRes("E_RenameDir") + " \"" + path + "\""); + } + } + } + + protected void RenameFile(string path, string name) + { + CheckPath(path); + FileInfo source = new FileInfo(FixPath(path)); + FileInfo dest = new FileInfo(Path.Combine(source.Directory.FullName, name)); + if (!source.Exists) + throw new Exception(LangRes("E_RenameFileInvalidPath")); + else if (!CanHandleFile(name)) + throw new Exception(LangRes("E_FileExtensionForbidden")); + else + { + try + { + source.MoveTo(dest.FullName); + _response.Write(GetSuccessRes()); + } + catch (Exception ex) + { + throw new Exception(ex.Message + "; " + LangRes("E_RenameFile") + " \"" + path + "\""); + } + } + } + + public bool ThumbnailCallback() + { + return false; + } + + protected void ShowThumbnail(string path, int width, int height) + { + CheckPath(path); + FileStream fs = new FileStream(FixPath(path), FileMode.Open); + Bitmap img = new Bitmap(Bitmap.FromStream(fs)); + fs.Close(); + fs.Dispose(); + int cropWidth = img.Width, cropHeight = img.Height; + int cropX = 0, cropY = 0; + + double imgRatio = (double)img.Width / (double)img.Height; + + if (height == 0) + height = Convert.ToInt32(Math.Floor((double)width / imgRatio)); + + if (width > img.Width) + width = img.Width; + if (height > img.Height) + height = img.Height; + + double cropRatio = (double)width / (double)height; + cropWidth = Convert.ToInt32(Math.Floor((double)img.Height * cropRatio)); + cropHeight = Convert.ToInt32(Math.Floor((double)cropWidth / cropRatio)); + if (cropWidth > img.Width) + { + cropWidth = img.Width; + cropHeight = Convert.ToInt32(Math.Floor((double)cropWidth / cropRatio)); + } + if (cropHeight > img.Height) + { + cropHeight = img.Height; + cropWidth = Convert.ToInt32(Math.Floor((double)cropHeight * cropRatio)); + } + if (cropWidth < img.Width) + { + cropX = Convert.ToInt32(Math.Floor((double)(img.Width - cropWidth) / 2)); + } + if (cropHeight < img.Height) + { + cropY = Convert.ToInt32(Math.Floor((double)(img.Height - cropHeight) / 2)); + } + + Rectangle area = new Rectangle(cropX, cropY, cropWidth, cropHeight); + Bitmap cropImg = img.Clone(area, System.Drawing.Imaging.PixelFormat.DontCare); + img.Dispose(); + Image.GetThumbnailImageAbort imgCallback = new Image.GetThumbnailImageAbort(ThumbnailCallback); + + _response.AddHeader("Content-Type", "image/png"); + cropImg.GetThumbnailImage(width, height, imgCallback, IntPtr.Zero).Save(_response.OutputStream, ImageFormat.Png); + _response.OutputStream.Close(); + cropImg.Dispose(); + } + + private ImageFormat GetImageFormat(string filename) + { + ImageFormat ret = ImageFormat.Jpeg; + switch (new FileInfo(filename).Extension.ToLower()) + { + case ".png": + ret = ImageFormat.Png; + break; + case ".gif": + ret = ImageFormat.Gif; + break; + } + return ret; + } + + protected void ImageResize(string path, string dest, int width, int height) + { + FileStream fs = new FileStream(path, FileMode.Open); + Image img = Image.FromStream(fs); + fs.Close(); + fs.Dispose(); + float ratio = (float)img.Width / (float)img.Height; + if ((img.Width <= width && img.Height <= height) || (width == 0 && height == 0)) + return; + + int newWidth = width; + int newHeight = Convert.ToInt16(Math.Floor((float)newWidth / ratio)); + if ((height > 0 && newHeight > height) || (width == 0)) + { + newHeight = height; + newWidth = Convert.ToInt16(Math.Floor((float)newHeight * ratio)); + } + Bitmap newImg = new Bitmap(newWidth, newHeight); + Graphics g = Graphics.FromImage((Image)newImg); + g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + g.DrawImage(img, 0, 0, newWidth, newHeight); + img.Dispose(); + g.Dispose(); + if (dest != "") + { + newImg.Save(dest, GetImageFormat(dest)); + } + newImg.Dispose(); + } + + protected void Upload(string path) + { + CheckPath(path); + path = FixPath(path); + string res = GetSuccessRes(); + try + { + for (int i = 0; i < Request.Files.Count; i++) + { + if (CanHandleFile(Request.Files[i].FileName)) + { + string filename = MakeUniqueFilename(path, Request.Files[i].FileName); + string dest = Path.Combine(path, filename); + Request.Files[i].SaveAs(dest); + if (GetFileType(new FileInfo(filename).Extension) == "image") + { + int w = 0; + int h = 0; + int.TryParse(GetSetting("MAX_IMAGE_WIDTH"), out w); + int.TryParse(GetSetting("MAX_IMAGE_HEIGHT"), out h); + ImageResize(dest, dest, w, h); + } + } + else + res = GetSuccessRes(LangRes("E_UploadNotAll")); + } + } + catch (Exception ex) + { + res = GetErrorRes(ex.Message); + } + _response.Write(""); + } + + public bool IsReusable + { + get + { + return false; + } + } + + #endregion + + public void ProcessRequest() + { + if (!_permissionService.Authorize(StandardPermissionProvider.UploadPictures)) + { + _response.Write(GetErrorRes("You do not have the required permission")); + return; + } + + string action = "DIRLIST"; + try + { + if (_context.Request["a"] != null) + action = (string)_context.Request["a"]; + + //VerifyAction(action); + switch (action.ToUpper()) + { + case "DIRLIST": + ListDirTree(_context.Request["type"]); + break; + case "FILESLIST": + ListFiles(_context.Request["d"], _context.Request["type"]); + break; + case "COPYDIR": + CopyDir(_context.Request["d"], _context.Request["n"]); + break; + case "COPYFILE": + CopyFile(_context.Request["f"], _context.Request["n"]); + break; + case "CREATEDIR": + CreateDir(_context.Request["d"], _context.Request["n"]); + break; + case "DELETEDIR": + DeleteDir(_context.Request["d"]); + break; + case "DELETEFILE": + DeleteFile(_context.Request["f"]); + break; + case "DOWNLOAD": + DownloadFile(_context.Request["f"]); + break; + case "DOWNLOADDIR": + DownloadDir(_context.Request["d"]); + break; + case "MOVEDIR": + MoveDir(_context.Request["d"], _context.Request["n"]); + break; + case "MOVEFILE": + MoveFile(_context.Request["f"], _context.Request["n"]); + break; + case "RENAMEDIR": + RenameDir(_context.Request["d"], _context.Request["n"]); + break; + case "RENAMEFILE": + RenameFile(_context.Request["f"], _context.Request["n"]); + break; + case "GENERATETHUMB": + int w = 140, h = 0; + int.TryParse(_context.Request["width"].Replace("px", ""), out w); + int.TryParse(_context.Request["height"].Replace("px", ""), out h); + ShowThumbnail(_context.Request["f"], w, h); + break; + case "UPLOAD": + Upload(_context.Request["d"]); + break; + default: + _response.Write(GetErrorRes("This action is not implemented.")); + break; + } + } + catch (Exception ex) + { + if (action == "UPLOAD") + { + _response.Write(""); + } + else + { + _response.Write(GetErrorRes(ex.Message)); + } + } + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ScheduleTaskController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ScheduleTaskController.cs index 0239e8603f..453ae689c8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ScheduleTaskController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ScheduleTaskController.cs @@ -172,7 +172,7 @@ public ActionResult RunJob(int id, string returnUrl = "") } } - return Redirect(returnUrl.NullEmpty() ?? Request.UrlReferrer.ToString()); + return RedirectToReferrer(returnUrl); } public ActionResult CancelJob(int id /* scheduleTaskId */, string returnUrl = "") @@ -187,8 +187,7 @@ public ActionResult CancelJob(int id /* scheduleTaskId */, string returnUrl = "" NotifyWarning(T("Admin.System.ScheduleTasks.CancellationRequested")); } - returnUrl = returnUrl.NullEmpty() ?? Request.UrlReferrer.ToString(); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } public ActionResult Edit(int id /* taskId */, string returnUrl = null) @@ -223,7 +222,7 @@ public ActionResult Edit(ScheduleTaskModel model, bool continueEditing, string r } var reloadResult = RedirectToAction("Edit", new { id = model.Id, returnUrl = returnUrl }); - var returnResult = Redirect(returnUrl.NullEmpty() ?? Url.Action("List")); + var returnResult = RedirectToReferrer(returnUrl, () => RedirectToAction("List")); var scheduleTask = _scheduleTaskService.GetTaskById(model.Id); if (scheduleTask == null) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs index ab728985e2..d26e835c28 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs @@ -139,14 +139,14 @@ public ActionResult StoreScopeConfiguration() if (allStores.Count < 2) return Content(""); - var model = new StoreScopeConfigurationModel() + var model = new StoreScopeConfigurationModel { StoreId = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext) }; foreach (var store in allStores) { - model.AllStores.Add(new SelectListItem() + model.AllStores.Add(new SelectListItem { Text = store.Name, Selected = (store.Id == model.StoreId), @@ -154,7 +154,7 @@ public ActionResult StoreScopeConfiguration() }); } - model.AllStores.Insert(0, new SelectListItem() + model.AllStores.Insert(0, new SelectListItem { Text = _services.Localization.GetResource("Admin.Common.StoresAll"), Selected = (0 == model.StoreId), @@ -172,16 +172,8 @@ public ActionResult ChangeStoreScopeConfiguration(int storeid, string returnUrl _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, SystemCustomerAttributeNames.AdminAreaStoreScopeConfiguration, storeid); } - - //url referrer - if (String.IsNullOrEmpty(returnUrl)) - returnUrl = _services.WebHelper.GetUrlReferrer(); - - //home page - if (String.IsNullOrEmpty(returnUrl)) - returnUrl = Url.Action("Index", "Home", new { area = "Admin" }); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl, () => RedirectToAction("Index", "Home", new { area = "Admin" })); } public ActionResult Blog() @@ -953,8 +945,8 @@ public ActionResult CustomerUser() StoreDependingSettings.GetOverrideKeys(externalAuthenticationSettings, model.ExternalAuthenticationSettings, storeScope, _services.Settings, false); - model.CustomerSettings.AvailableCustomerNumberMethods = customerSettings.CustomerNumberMethod.ToSelectList(); - model.CustomerSettings.AvailableCustomerNumberVisibilities = customerSettings.CustomerNumberVisibility.ToSelectList(); + model.CustomerSettings.AvailableCustomerNumberMethods = customerSettings.CustomerNumberMethod.ToSelectList(false); + model.CustomerSettings.AvailableCustomerNumberVisibilities = customerSettings.CustomerNumberVisibility.ToSelectList(false); model.CustomerSettings.AvailableRegisterCustomerRoles = allCustomerRoles .Where(x => x.SystemName != SystemCustomerRoleNames.Registered && x.SystemName != SystemCustomerRoleNames.Guests) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ShippingController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ShippingController.cs index 887b5dabbc..ed497b0175 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ShippingController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ShippingController.cs @@ -73,10 +73,17 @@ private void PrepareShippingMethodModel(ShippingMethodModel model, ShippingMetho { var allFilters = _shippingService.GetAllShippingMethodFilters(); - model.FilterConfigurationUrls = allFilters - .Select(x => "'" + x.GetConfigurationUrl(shippingMethod.Id) + "'") - .OrderBy(x => x) - .ToList(); + if (shippingMethod != null) + { + model.FilterConfigurationUrls = allFilters + .Select(x => "'" + x.GetConfigurationUrl(shippingMethod.Id) + "'") + .OrderBy(x => x) + .ToList(); + } + else + { + model.FilterConfigurationUrls = new List(); + } } #endregion diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ThemeController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ThemeController.cs index 0e4129951d..c47332e250 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ThemeController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ThemeController.cs @@ -305,8 +305,7 @@ private string ValidateLess(ThemeManifest manifest, int storeId) storeId, manifest.ThemeName); - HttpWebRequest request = WebRequest.CreateHttp(url); - request.UserAgent = "SmartStore.NET {0}".FormatInvariant(SmartStoreVersion.CurrentFullVersion); + var request = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(url)); WebResponse response = null; try @@ -541,7 +540,7 @@ public ActionResult PreviewToolPost(string theme, int storeId, string returnUrl) _services.StoreContext.SetPreviewStore(storeId); } - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } [HttpPost, ActionName("PreviewTool"), FormValueRequired("PreviewMode.Exit")] @@ -556,12 +555,7 @@ public ActionResult ExitPreview() } var returnUrl = (string)TempData["PreviewModeReturnUrl"]; - if (returnUrl.IsEmpty()) - { - returnUrl = Url.Action("Index", "Home", new { area = (string)null }); - } - - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } [HttpPost, ActionName("PreviewTool"), FormValueRequired("PreviewMode.Apply")] diff --git a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperStartupTask.cs b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperStartupTask.cs index a1b5004361..bee55cf932 100644 --- a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperStartupTask.cs +++ b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperStartupTask.cs @@ -137,11 +137,13 @@ public void Execute() .ForMember(dest => dest.FlagFileNames, mo => mo.Ignore()); Mapper.CreateMap() .ForMember(dest => dest.LocaleStringResources, mo => mo.Ignore()); - //email account - Mapper.CreateMap() - .ForMember(dest => dest.IsDefaultEmailAccount, mo => mo.Ignore()) - .ForMember(dest => dest.SendTestEmailTo, mo => mo.Ignore()); - Mapper.CreateMap(); + //email account + Mapper.CreateMap() + .ForMember(dest => dest.IsDefaultEmailAccount, mo => mo.Ignore()) + .ForMember(dest => dest.SendTestEmailTo, mo => mo.Ignore()) + .ForMember(dest => dest.TestEmailShortErrorMessage, mo => mo.Ignore()) + .ForMember(dest => dest.TestEmailFullErrorMessage, mo => mo.Ignore()); + Mapper.CreateMap(); //message template Mapper.CreateMap() .ForMember(dest => dest.TokensTree, mo => mo.Ignore()) @@ -518,12 +520,14 @@ public void Execute() Mapper.CreateMap() .ForMember(dest => dest.SpecificationAttribute, mo => mo.Ignore()) .ForMember(dest => dest.ProductSpecificationAttributes, mo => mo.Ignore()); - //checkout attributes - Mapper.CreateMap() - .ForMember(dest => dest.AvailableTaxCategories, mo => mo.Ignore()) - .ForMember(dest => dest.AttributeControlTypeName, mo => mo.Ignore()) - .ForMember(dest => dest.Locales, mo => mo.Ignore()); - Mapper.CreateMap() + //checkout attributes + Mapper.CreateMap() + .ForMember(dest => dest.AvailableTaxCategories, mo => mo.Ignore()) + .ForMember(dest => dest.AttributeControlTypeName, mo => mo.Ignore()) + .ForMember(dest => dest.Locales, mo => mo.Ignore()) + .ForMember(dest => dest.SelectedStoreIds, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()); + Mapper.CreateMap() .ForMember(dest => dest.AttributeControlType, mo => mo.Ignore()) .ForMember(dest => dest.CheckoutAttributeValues, mo => mo.Ignore()); Mapper.CreateMap() @@ -666,7 +670,7 @@ public void Execute() //.ForMember(dest => dest.DefaultPictureZoomEnabled, mo => mo.Ignore()) .ForMember(dest => dest.DefaultImageQuality, mo => mo.Ignore()) .ForMember(dest => dest.MultipleThumbDirectories, mo => mo.Ignore()) - .ForMember(dest => dest.AutoCompleteSearchThumbPictureSize, mo => mo.Ignore()); + .ForMember(dest => dest.VariantValueThumbPictureSize, mo => mo.Ignore()); Mapper.CreateMap() .ForMember(dest => dest.AvailableCustomerNumberMethods, mo => mo.Ignore()) .ForMember(dest => dest.AvailableCustomerNumberVisibilities, mo => mo.Ignore()) diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductModel.cs index d7f5f66e1f..d24020eaea 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductModel.cs @@ -140,7 +140,7 @@ public ProductModel() [SmartResourceDisplayName("Admin.Catalog.Products.Fields.Download")] [UIHint("Download")] - public int DownloadId { get; set; } + public int? DownloadId { get; set; } [SmartResourceDisplayName("Admin.Catalog.Products.Fields.UnlimitedDownloads")] public bool UnlimitedDownloads { get; set; } @@ -159,7 +159,7 @@ public ProductModel() [SmartResourceDisplayName("Admin.Catalog.Products.Fields.SampleDownload")] [UIHint("Download")] - public int SampleDownloadId { get; set; } + public int? SampleDownloadId { get; set; } [SmartResourceDisplayName("Admin.Catalog.Products.Fields.HasUserAgreement")] public bool HasUserAgreement { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportDeploymentModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportDeploymentModel.cs index d60f2b8f5f..f5cb8f82c6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportDeploymentModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportDeploymentModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using FluentValidation.Attributes; @@ -13,6 +14,8 @@ namespace SmartStore.Admin.Models.DataExchange public class ExportDeploymentModel : EntityModelBase { public int ProfileId { get; set; } + public bool CreateZip { get; set; } + public string PublicFolderUrl { get; set; } [SmartResourceDisplayName("Common.Image")] public string ThumbnailUrl { get; set; } @@ -30,32 +33,6 @@ public class ExportDeploymentModel : EntityModelBase [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.DeploymentType")] public string DeploymentTypeName { get; set; } - public string DeploymentTypeIconClass - { - get - { - switch (DeploymentType) - { - case ExportDeploymentType.FileSystem: - return "fa-folder-open-o"; - case ExportDeploymentType.Email: - return "fa-envelope-o"; - case ExportDeploymentType.Http: - return "fa-globe"; - case ExportDeploymentType.Ftp: - return "fa-files-o"; - default: - return "fa-question"; - } - } - } - - [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.CreateZip")] - public bool CreateZip { get; set; } - - [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.IsPublic")] - public bool IsPublic { get; set; } - [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.Username")] public string Username { get; set; } @@ -73,6 +50,9 @@ public string DeploymentTypeIconClass [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.FileSystemPath")] public string FileSystemPath { get; set; } + [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.SubFolder")] + public string SubFolder { get; set; } + [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.EmailAddresses")] public string EmailAddresses { get; set; } public string SerializedEmailAddresses { get; set; } @@ -90,6 +70,20 @@ public string DeploymentTypeIconClass [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.UseSsl")] public bool UseSsl { get; set; } - public ExportProfileDetailsModel ProfileDetails { get; set; } + public LastResultInfo LastResult { get; set; } + + public int FileCount { get; set; } + + public class LastResultInfo + { + public DateTime Execution { get; set; } + public string ExecutionPretty { get; set; } + public string Error { get; set; } + + public bool Succeeded + { + get { return Error.IsEmpty(); } + } + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProfileModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProfileModel.cs index 3e073117c8..5c2e77b6ef 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProfileModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProfileModel.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Web; using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Admin.Models.Tasks; @@ -89,7 +91,7 @@ public partial class ExportProfileModel : EntityModelBase public ScheduleTaskModel TaskModel { get; set; } - public ExportProfileDetailsModel Details { get; set; } + public int FileCount { get; set; } public class ProviderModel { @@ -147,30 +149,54 @@ public class ProviderSelectItem } - public partial class ExportProfileDetailsModel : EntityModelBase + public partial class ExportFileDetailsModel : EntityModelBase { - [SmartResourceDisplayName("Admin.DataExchange.Export.ExportFiles")] - public List ExportFiles { get; set; } - - public string ZipPath { get; set; } - - [SmartResourceDisplayName("Admin.DataExchange.Export.ExportFiles")] - public int ExportFileCount + public int FileCount { get { - return (ExportFiles.Count + (ZipPath.HasValue() ? 1 : 0)); + var result = ExportFiles.Count; + + if (result == 0) + result = PublicFiles.Count; + + return result; } } - public List PublicFiles { get; set; } + public List ExportFiles { get; set; } + public List PublicFiles { get; set; } + + public bool IsForDeployment { get; set; } - public class PublicFile + public class FileInfo { public int StoreId { get; set; } public string StoreName { get; set; } - public string FileName { get; set; } + + public int DisplayOrder { get; set; } + + public string FilePath { get; set; } public string FileUrl { get; set; } + + public string FileName { get; set; } + public string FileExtension { get; set; } + + public string FileRootPath + { + get + { + var rootPath = ""; + var appPath = HttpRuntime.AppDomainAppPath; + + if (FilePath.StartsWith(appPath)) + rootPath = FilePath.Replace(appPath, "~/"); + + rootPath = rootPath.Replace('\\', '/'); + + return rootPath; + } + } } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileModel.cs index 16bf2a9675..5baf801477 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileModel.cs @@ -13,6 +13,11 @@ namespace SmartStore.Admin.Models.DataExchange [Validator(typeof(ImportProfileValidator))] public partial class ImportProfileModel : EntityModelBase { + public ImportProfileModel() + { + ExtraData = new ExtraDataModel(); + } + [SmartResourceDisplayName("Admin.DataExchange.Import.Name")] public string Name { get; set; } @@ -29,10 +34,10 @@ public partial class ImportProfileModel : EntityModelBase public bool Enabled { get; set; } [SmartResourceDisplayName("Admin.Common.RecordsSkip")] - public int Skip { get; set; } + public int? Skip { get; set; } [SmartResourceDisplayName("Admin.Common.RecordsTake")] - public int Take { get; set; } + public int? Take { get; set; } [SmartResourceDisplayName("Admin.DataExchange.Import.UpdateOnly")] public bool UpdateOnly { get; set; } @@ -54,16 +59,22 @@ public partial class ImportProfileModel : EntityModelBase public bool LogFileExists { get; set; } public string TempFileName { get; set; } - public string UnspecifiedString { get; set; } - public string AddNewString { get; set; } - public string DeleteString { get; set; } - public string IgnoreString { get; set; } public CsvConfigurationModel CsvConfiguration { get; set; } + public ExtraDataModel ExtraData { get; set; } + public List ColumnMappings { get; set; } public List AvailableSourceColumns { get; set; } - public List AvailableEntityProperties { get; set; } + public List AvailableEntityProperties { get; set; } + + public int MaxMappingLabelTextLength { get { return 42; } } + + public class ExtraDataModel + { + [SmartResourceDisplayName("Admin.DataExchange.Import.NumberOfPictures")] + public int? NumberOfPictures { get; set; } + } } @@ -74,9 +85,11 @@ public class ColumnMappingItemModel public string Column { get; set; } public string ColumnWithoutIndex { get; set; } public string ColumnIndex { get; set; } - public string ColumnLocalized { get; set; } public string Property { get; set; } + public string PropertyDescription { get; set; } + public string Default { get; set; } + public bool IsDefaultDisabled { get; set; } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Messages/EmailAccountModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Messages/EmailAccountModel.cs index 0604e18a0c..8a3fa54a2d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Messages/EmailAccountModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Messages/EmailAccountModel.cs @@ -48,5 +48,7 @@ public class EmailAccountModel : EntityModelBase [AllowHtml] public string SendTestEmailTo { get; set; } - } + public string TestEmailShortErrorMessage { get; set; } + public string TestEmailFullErrorMessage { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeListModel.cs new file mode 100644 index 0000000000..6084bd6210 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeListModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.Admin.Models.Orders +{ + public class CheckoutAttributeListModel : ModelBase + { + public int GridPageSize { get; set; } + + public IList AvailableStores { get; set; } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeModel.cs index 5e26016af9..6f6a404473 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeModel.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using FluentValidation.Attributes; +using SmartStore.Admin.Models.Stores; using SmartStore.Admin.Validators.Orders; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Localization; @@ -51,9 +52,16 @@ public CheckoutAttributeModel() public int DisplayOrder { get; set; } public IList Locales { get; set; } - } - public class CheckoutAttributeLocalizedModel : ILocalizedModelLocal + [SmartResourceDisplayName("Admin.Common.Store.LimitedTo")] + public bool LimitedToStores { get; set; } + + [SmartResourceDisplayName("Admin.Common.Store.AvailableFor")] + public List AvailableStores { get; set; } + public int[] SelectedStoreIds { get; set; } + } + + public class CheckoutAttributeLocalizedModel : ILocalizedModelLocal { public int LanguageId { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs index 2e076d6471..f395584d6c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs @@ -26,12 +26,12 @@ public partial class CustomerSettingsModel [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.UsernamesEnabled")] public bool UsernamesEnabled { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.DefaultSortOrderMode")] + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.CustomerNumberMethod")] public CustomerNumberMethod CustomerNumberMethod { get; set; } public SelectList AvailableCustomerNumberMethods { get; set; } public IList AvailableRegisterCustomerRoles { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.DefaultSortOrderMode")] + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.CustomerNumberVisibility")] public CustomerNumberVisibility CustomerNumberVisibility { get; set; } public SelectList AvailableCustomerNumberVisibilities { get; set; } @@ -89,6 +89,9 @@ public partial class CustomerSettingsModel [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.StoreLastVisitedPage")] public bool StoreLastVisitedPage { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.DisplayPrivacyAgreementOnContactUs")] + public bool DisplayPrivacyAgreementOnContactUs { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.GenderEnabled")] public bool GenderEnabled { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/OrderSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/OrderSettingsModel.cs index c54a264eb5..c0e73f70fb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/OrderSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/OrderSettingsModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Admin.Validators.Settings; @@ -66,6 +67,9 @@ public OrderSettingsModel() [SmartResourceDisplayName("Admin.Configuration.Settings.Order.DisplayOrdersOfAllStores")] public bool DisplayOrdersOfAllStores { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.Order.OrderListPageSize")] + public int OrderListPageSize { get; set; } + public IList Locales { get; set; } } diff --git a/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj index 112d13d833..603f623aa1 100644 --- a/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj +++ b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj @@ -92,9 +92,9 @@ True ..\..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - False - ..\..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True False @@ -102,6 +102,7 @@ + False @@ -213,6 +214,7 @@ + @@ -265,6 +267,7 @@ + @@ -428,6 +431,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -460,17 +724,19 @@ + + + + + + - - Always - - - Always - + + Designer @@ -511,7 +777,6 @@ - diff --git a/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ExportProfileValidator.cs b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ExportProfileValidator.cs index 8cbe632a14..dc15762b5c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ExportProfileValidator.cs +++ b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ExportProfileValidator.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Linq; -using FluentValidation; +using FluentValidation; using SmartStore.Admin.Models.DataExchange; using SmartStore.Services.Localization; @@ -15,12 +13,12 @@ public ExportProfileValidator(ILocalizationService localization) .WithMessage(localization.GetResource("Admin.Validation.Name")); RuleFor(x => x.FolderName) - .Must(x => x.HasValue() && !x.IsCaseInsensitiveEqual("con") && !Path.GetInvalidFileNameChars().Any(y => x.Contains(y))) - .WithMessage(localization.GetResource("Admin.DataExchange.Export.FolderAndFileName.Validate")); + .Must(x => x.HasValue() && !x.IsCaseInsensitiveEqual("con")) + .WithMessage(localization.GetResource("Admin.DataExchange.Export.FolderName.Validate")); RuleFor(x => x.FileNamePattern) .NotEmpty() - .WithMessage(localization.GetResource("Admin.DataExchange.Export.FolderAndFileName.Validate")); + .WithMessage(localization.GetResource("Admin.DataExchange.Export.FileNamePattern.Validate")); RuleFor(x => x.Offset) .GreaterThanOrEqualTo(0) diff --git a/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ImportProfileValidator.cs b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ImportProfileValidator.cs index 15fa25f6f6..95277d0d85 100644 --- a/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ImportProfileValidator.cs +++ b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ImportProfileValidator.cs @@ -16,6 +16,14 @@ public ImportProfileValidator(ILocalizationService localization) .NotEmpty() .When(x => x.Id != 0) .WithMessage(localization.GetResource("Admin.DataExchange.Import.Validate.OneKeyFieldRequired")); + + RuleFor(x => x.Skip) + .GreaterThanOrEqualTo(0) + .WithMessage(localization.GetResource("Admin.Common.SkipAndTakeGreaterThanOrEqualZero")); + + RuleFor(x => x.Take) + .GreaterThanOrEqualTo(0) + .WithMessage(localization.GetResource("Admin.Common.SkipAndTakeGreaterThanOrEqualZero")); } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Validators/Settings/OrderSettingsValidator.cs b/src/Presentation/SmartStore.Web/Administration/Validators/Settings/OrderSettingsValidator.cs index eab92160eb..547872cac6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Validators/Settings/OrderSettingsValidator.cs +++ b/src/Presentation/SmartStore.Web/Administration/Validators/Settings/OrderSettingsValidator.cs @@ -11,8 +11,13 @@ public OrderSettingsValidator(ILocalizationService localizationService) { RuleFor(x => x.GiftCards_Activated_OrderStatusId).NotEqual((int)OrderStatus.Pending) .WithMessage(localizationService.GetResource("Admin.Configuration.Settings.RewardPoints.PointsForPurchases_Awarded.Pending")); + RuleFor(x => x.GiftCards_Deactivated_OrderStatusId).NotEqual((int)OrderStatus.Pending) .WithMessage(localizationService.GetResource("Admin.Configuration.Settings.RewardPoints.PointsForPurchases_Canceled.Pending")); - } + + RuleFor(x => x.OrderListPageSize) + .GreaterThan(0) + .WithMessage(localizationService.GetResource("Admin.Validation.ValueGreaterZero")); + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml index 507546d242..f78aa0983e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml @@ -456,7 +456,9 @@ } else { - @T("Admin.Catalog.Categories.Discounts.NoDiscounts") +
+ @T("Admin.Catalog.Categories.Discounts.NoDiscounts") +
} } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/List.cshtml index d80b3f8bfb..b3707ed7fe 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/List.cshtml @@ -1,5 +1,5 @@ @using Telerik.Web.Mvc.UI - +@model CheckoutAttributeListModel @{ ViewBag.Title = T("Admin.Catalog.Attributes.CheckoutAttributes").Text; } @@ -8,7 +8,9 @@ @T("Admin.Catalog.Attributes.CheckoutAttributes") - - - - } - else - { - @T("Admin.Catalog.Attributes.CheckoutAttributes.Values.SaveBeforeEdit") - } - } \ No newline at end of file + //return false to don't reload a page + return false; + }); + }); + + + + + } + else + { + @T("Admin.Catalog.Attributes.CheckoutAttributes.Values.SaveBeforeEdit") + } +} + +@helper TabStores() +{ + + + + + + + + + + + +
+ @Html.SmartLabelFor(model => model.LimitedToStores) + + @Html.EditorFor(model => model.LimitedToStores) + @Html.ValidationMessageFor(model => model.LimitedToStores) +
+ @Html.SmartLabelFor(model => model.AvailableStores) + + @if (Model.AvailableStores != null && Model.AvailableStores.Count > 0) + { + foreach (var store in Model.AvailableStores) + { + + } + } + else + { +
@T("Admin.Configuration.Stores.NoStoresDefined")
+ } +
+} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml index 9028265175..64d56e6030 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml @@ -100,6 +100,7 @@ +   - } + } + +@if (Model.TestEmailShortErrorMessage.HasValue()) +{ +

+

@Model.TestEmailShortErrorMessage
+
@Model.TestEmailFullErrorMessage
+

+} + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/CreateDeployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/CreateDeployment.cshtml index bf7866d3df..875a7268f5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/CreateDeployment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/CreateDeployment.cshtml @@ -2,7 +2,7 @@ @using SmartStore.Core.Domain.DataExchange; @using SmartStore.Admin.Models.DataExchange; @{ - var title = string.Concat(T("Common.AddNew"), " ", T("Common.Deployment")); + var title = string.Concat(T("Common.Publishing"), " - ", T("Common.AddNew")); ViewBag.Title = title; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml index 26d12f432b..f47ae86481 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml @@ -2,7 +2,7 @@ @using SmartStore.Core.Domain.DataExchange; @using SmartStore.Admin.Models.DataExchange; @{ - var title = string.Concat(T("Admin.Common.Edit"), " ", T("Common.Profile")); + var title = string.Concat(T("Common.Profile"), " - ", T("Admin.Common.Edit")); ViewBag.Title = title; } @@ -19,6 +19,13 @@
@if (Model.HasActiveProvider) { + if (Model.Id != 0) + { + + } +  @T("Admin.Common.Preview") @@ -289,7 +296,18 @@ viewData.TemplateInfo.HtmlFieldPrefix = "CustomProperties[ProviderConfigData]"; viewData.ModelState.Merge(ViewData.ModelState); @Html.Hidden("CustomProperties[ProviderConfigData].__Type__", Model.Provider.ConfigDataType.AssemblyQualifiedName) - @Html.Partial(Model.Provider.ConfigPartialViewName, Model.Provider.ConfigData, viewData) + + try + { + @Html.Partial(Model.Provider.ConfigPartialViewName, Model.Provider.ConfigData, viewData) + } + catch (Exception exception) + { +
+ @T("Admin.Configuration.Plugins.UnknownError") +
+
@exception.ToString()
+ } } else { @@ -367,5 +385,14 @@ } }); }); + + // execute export + $('#ProfileExportButton').click(function (e) { + e.preventDefault(); + $(this).doPostData({ + ask: '@T("Admin.Common.AskToProceed")' + }); + return false; + }); }); \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml index 07d42f416f..7e6a01c1ca 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml @@ -2,7 +2,7 @@ @using SmartStore.Core.Domain.DataExchange; @using SmartStore.Admin.Models.DataExchange; @{ - var title = string.Concat(T("Admin.Common.Edit"), " ", T("Common.Deployment")); + var title = string.Concat(T("Common.Publishing"), " - ", T("Admin.Common.Edit")); ViewBag.Title = title; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoDeployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoDeployment.cshtml deleted file mode 100644 index d48d8eeaa0..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoDeployment.cshtml +++ /dev/null @@ -1,54 +0,0 @@ -@using SmartStore.Core.Domain.DataExchange -@using SmartStore.Admin.Models.DataExchange -@model ExportDeploymentModel -@{ - Layout = null; -} -@helper FileDownloadLink(string path, string fileName) -{ -
- @Html.IconForFileExtension(Path.GetExtension(fileName), false) - @fileName -
-} -
-  @Model.DeploymentTypeName - @if (Model.DeploymentType == ExportDeploymentType.FileSystem && Model.IsPublic) - { - (@T("Common.Public")) - } -
-
- @if (Model.DeploymentType == ExportDeploymentType.FileSystem) - { - foreach (var path in Model.ProfileDetails.ExportFiles) - { - @FileDownloadLink(path, Path.GetFileName(path)) - } - - if (Model.ProfileDetails.PublicFiles.Count > 0) - { - foreach (var grp in Model.ProfileDetails.PublicFiles.OrderBy(x => x.StoreId).GroupBy(x => x.StoreId)) - { -

-
@(grp.First().StoreName):
- foreach (var file in grp) - { - - } - } - } - } - else if (Model.DeploymentType == ExportDeploymentType.Email) - { -
@Model.EmailAddresses
- } - else if (Model.DeploymentType == ExportDeploymentType.Http) - { -
@Model.Url
- } - else if (Model.DeploymentType == ExportDeploymentType.Ftp) - { -
@Model.Url
- } -
\ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoProfile.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoProfile.cshtml index 54799e977b..912809feb9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoProfile.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoProfile.cshtml @@ -16,15 +16,15 @@ } @foreach (var profile in Model.Profiles) { - - - @Html.SmartLabelFor(m => m.Profiles) - - - - @profile.Name - - + + + @Html.SmartLabelFor(m => m.Profiles) + + + + @profile.Name + + if (profile.ScheduleTaskId.HasValue) { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/List.cshtml index 683a397cd9..05bd582199 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/List.cshtml @@ -13,36 +13,11 @@
-@if (!hasUserProfiles && !hasSystemProfiles) -{ -
- - @T("Admin.DataExchange.Export.NoProfiles") -
-} -
-
- @Html.Partial("_ProfileList", Model.Where(x => !x.IsSystemProfile), new ViewDataDictionary { { "IsSystemProfile", false } }) -
- - @if (hasSystemProfiles) - { -
- - @T("Admin.DataExchange.Export.SystemProfileNote") -
- -
- @Html.Partial("_ProfileList", Model.Where(x => x.IsSystemProfile), new ViewDataDictionary { { "IsSystemProfile", true } }) -
- } + @Html.Partial("_ProfileList", Model.Where(x => !x.IsSystemProfile), new ViewDataDictionary { { "IsSystemProfile", false } }) + @Html.Partial("_ProfileList", Model.Where(x => x.IsSystemProfile), new ViewDataDictionary { { "IsSystemProfile", true } })
-
- -
- - } + + @helper FileNamePatternDescription() { @@ -186,7 +252,7 @@ else -
@Model.FileNamePatternDescriptions.SafeGet(7)
+ } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml index d0616dfc93..1adf73df3b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml @@ -1,7 +1,7 @@ @using SmartStore.Admin.Models.DataExchange; @model ImportProfileModel @{ - var title = string.Concat(T("Admin.Common.Edit"), " ", T("Common.Profile")); + var title = string.Concat(T("Common.Profile"), " - ", T("Admin.Common.Edit")); ViewBag.Title = title; } @@ -14,6 +14,12 @@ @title - @Model.Name @Html.ActionLink("(" + T("Admin.Common.BackToList") + ")", "List")
- + @@ -105,10 +105,6 @@ else } -
- -
-