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/changelog.md b/changelog.md index 826a9778de..0dac88a9fd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,13 +1,16 @@ # Release Notes -## SmartStore.NET 2.5.1 +## SmartStore.NET 2.6 ### Highlights -* #637 Integrated PayPal PLUS payment Provider -* Major improvements in Importer: better field mapping, higher performance, bug fixes etc. +* 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 -* #637 Integrated PayPal PLUS payment Provider * #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) @@ -15,11 +18,15 @@ * 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 @@ -53,6 +60,10 @@ * #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/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/ExportEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs index a5b5a48763..6ea68ec73c 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs @@ -126,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/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/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/WebHelper.cs b/src/Libraries/SmartStore.Core/WebHelper.cs index fee592e852..edef7863b7 100644 --- a/src/Libraries/SmartStore.Core/WebHelper.cs +++ b/src/Libraries/SmartStore.Core/WebHelper.cs @@ -34,6 +34,7 @@ public partial class WebHelper : IWebHelper private bool? _isCurrentConnectionSecured; private string _storeHost; private string _storeHostSsl; + private string _ipAddress; private bool? _appPathPossiblyAppended; private bool? _appPathPossiblyAppendedSsl; @@ -56,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) 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/Migrations/201605201911421_ExportRevision.cs b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs index 485fa9fdec..d168932260 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs @@ -5,6 +5,7 @@ namespace SmartStore.Data.Migrations using Core.Domain; using Core.Domain.DataExchange; using Setup; + using Utilities; public partial class ExportRevision : DbMigration, ILocaleResourcesProvider, IDataSeeder { @@ -58,6 +59,9 @@ public void Seed(SmartObjectContext context) context.ColumnDelete("ExportDeployment", "IsPublic"); } + + var oldFileManagerPath = CommonHelper.MapPath("~/Content/filemanager"); + FileSystemHelper.ClearDirectory(oldFileManagerPath, true); } public void MigrateLocaleResources(LocaleResourcesBuilder builder) @@ -170,6 +174,23 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "This function is not available for guests.", "Diese Funktion steht für Gäste nicht zur Verfügung."); + builder.AddOrUpdate("ContactUs.PrivacyAgreement", + "Privacy consent", + "Einwilligungserklärung 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 Datenschutzerklärung 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", + "Einwilligungserklärung 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( 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 715e87aaf6..ff7fd39e2b 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -404,6 +404,10 @@ 201605201911421_ExportRevision.cs + + + 201607111548571_FixExportDeploymentIsPublic.cs + @@ -752,6 +756,9 @@ 201605201911421_ExportRevision.cs + + 201607111548571_FixExportDeploymentIsPublic.cs + 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/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/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/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/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/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index b910cf23df..e6b182c270 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -293,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), @@ -535,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); @@ -553,7 +553,7 @@ 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.Any()) @@ -561,7 +561,7 @@ private void SendCompletionEmail(DataExporterContext ctx, string zipPath) body.Append("

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

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

"); } @@ -571,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)); @@ -689,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 }; @@ -1068,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)); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index f9e4bd158a..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 { @@ -567,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); @@ -578,14 +593,19 @@ 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); @@ -595,21 +615,31 @@ private dynamic ToDynamic( else dynObject._ProductTemplateViewPath = ""; - var detailUrl = ctx.Store.Url.EnsureEndsWith("/") + (string)dynObject.SeName; - 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( @@ -621,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 @@ -871,7 +896,9 @@ private dynamic ToDynamic( { 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; } } @@ -912,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; @@ -1005,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); } @@ -1046,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); @@ -1077,11 +1107,14 @@ 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 diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs index 67e3c090a1..70170c0fd3 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs @@ -209,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/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 a14c25e424..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()); + } } } 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/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/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/Orders/OrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs index d92e6212a0..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; 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/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.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.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.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.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 3709846816..85992eb451 100644 --- a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj +++ b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj @@ -180,6 +180,7 @@ Properties\AssemblyVersionInfo.cs + 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.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 8187db5c15..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.1 +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 bc9047d888..3edd31e361 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -202,14 +202,18 @@ protected override void Export(ExportExecuteContext context) string category = (gmc == null ? null : gmc.Taxonomy); string productType = product._CategoryPath; string mainImageUrl = product._MainPictureUrl; - var futureSpecialPrice = product._FutureSpecialPrice 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(ExportExecuteContext 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(ExportExecuteContext context) writer.WriteElementString("g", "availability_date", _googleNamespace, availabilityDate); } - if (config.SpecialPrice && futureSpecialPrice.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, futureSpecialPrice.Value.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/changelog.md b/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md index c368c87dd7..2c1f7000cf 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md @@ -1,5 +1,10 @@ #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 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.PayPal/Controllers/PayPalPlusController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs index 1c20f4b032..7a835f3d19 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs @@ -194,6 +194,9 @@ public ActionResult Configure(PayPalPlusConfigurationModel model, FormCollection 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 => { @@ -209,6 +212,15 @@ public ActionResult Configure(PayPalPlusConfigurationModel model, FormCollection 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); @@ -276,44 +288,60 @@ public ActionResult PaymentWall() model.ThirdPartyFees = sb.ToString(); - if (session.PaymentId.IsEmpty() || session.ApprovalUrl.IsEmpty()) + // 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 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); + 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) + result = PayPalService.CreatePayment(settings, session, cart, PayPalPlusProvider.SystemName, returnUrl, cancelUrl); + if (result.Success && result.Json != null) + { + foreach (var link in result.Json.links) { - foreach (var link in result.Json.links) + if (((string)link.rel).IsCaseInsensitiveEqual("approval_url")) { - if (((string)link.rel).IsCaseInsensitiveEqual("approval_url")) - { - session.PaymentId = result.Id; - session.ApprovalUrl = link.href; - break; - } + session.PaymentId = result.Id; + session.ApprovalUrl = link.href; + break; } } - else - { - model.ErrorMessage = result.ErrorMessage; - } } 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; diff --git a/src/Plugins/SmartStore.PayPal/Description.txt b/src/Plugins/SmartStore.PayPal/Description.txt index c78ff1bd32..b96ae172bb 100644 --- a/src/Plugins/SmartStore.PayPal/Description.txt +++ b/src/Plugins/SmartStore.PayPal/Description.txt @@ -2,7 +2,7 @@ Description: Provides the PayPal payment methods PayPal Standard, PayPal Direct, PayPal Express and PayPal PLUS. SystemName: SmartStore.PayPal Group: Payment -Version: 2.5.0.2 +Version: 2.6.0.5 MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.PayPal.dll diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs index db1bb64542..7c47079830 100644 --- a/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs @@ -1,6 +1,9 @@ -using System.Web.Mvc; +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 @@ -9,13 +12,16 @@ public class PayPalPlusCheckoutFilter : IActionFilter { private readonly ICommonServices _services; private readonly IPaymentService _paymentService; + private readonly Lazy _genericAttributeService; public PayPalPlusCheckoutFilter( ICommonServices services, - IPaymentService paymentService) + IPaymentService paymentService, + Lazy genericAttributeService) { _services = services; _paymentService = paymentService; + _genericAttributeService = genericAttributeService; } public void OnActionExecuting(ActionExecutingContext filterContext) @@ -28,6 +34,8 @@ public void OnActionExecuting(ActionExecutingContext filterContext) 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); diff --git a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs index d7752e61f5..f81f6c2d7f 100644 --- a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs +++ b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Net; using System.Web.Mvc; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; @@ -164,6 +163,7 @@ public void Copy(PayPalPlusPaymentSettings settings, bool fromSettings) { SecurityProtocol = settings.SecurityProtocol; UseSandbox = settings.UseSandbox; + TransactMode = (int)Settings.TransactMode.AuthorizeAndCapture; AdditionalFee = settings.AdditionalFee; AdditionalFeePercentage = settings.AdditionalFeePercentage; @@ -179,6 +179,7 @@ public void Copy(PayPalPlusPaymentSettings settings, bool fromSettings) { settings.SecurityProtocol = SecurityProtocol; settings.UseSandbox = UseSandbox; + settings.TransactMode = Settings.TransactMode.AuthorizeAndCapture; settings.AdditionalFee = AdditionalFee; settings.AdditionalFeePercentage = AdditionalFeePercentage; diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs index ca69a8e17c..98c4e8e223 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs @@ -8,6 +8,7 @@ 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; @@ -20,6 +21,7 @@ using SmartStore.Services.Orders; using SmartStore.Services.Payments; using SmartStore.Services.Shipping; +using SmartStore.Services.Tax; namespace SmartStore.PayPal { @@ -30,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 { @@ -180,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 = 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); @@ -253,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 = 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 @@ -294,14 +304,14 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq decimal amountToSubtract = -amountCanBeUsed; - cartItems.Add(new PaymentDetailsItemType() + cartItems.Add(new PaymentDetailsItemType { 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() } }); @@ -317,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 @@ -332,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(), diff --git a/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs b/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs index cfb377fabb..2f4b94c1c9 100644 --- a/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs +++ b/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs @@ -12,7 +12,7 @@ namespace SmartStore.PayPal.Services { public interface IPayPalService { - void AddOrderNote(PayPalSettingsBase settings, Order order, string anyString); + 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); @@ -36,6 +36,12 @@ PayPalResponse CreatePayment( 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); diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs index 0b96289c19..62e53f01d5 100644 --- a/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs +++ b/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs @@ -44,7 +44,7 @@ public class PayPalService : IPayPalService private readonly IPaymentService _paymentService; private readonly IPriceCalculationService _priceCalculationService; private readonly ITaxService _taxService; - private readonly Lazy _currencyService; + private readonly ICurrencyService _currencyService; private readonly Lazy _pictureService; private readonly Lazy _companyInfoSettings; @@ -57,7 +57,7 @@ public PayPalService( IPaymentService paymentService, IPriceCalculationService priceCalculationService, ITaxService taxService, - Lazy currencyService, + ICurrencyService currencyService, Lazy pictureService, Lazy companyInfoSettings) { @@ -113,6 +113,118 @@ private Dictionary CreateAddress(Address addr, bool addRecipient 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(); @@ -184,7 +296,7 @@ public static Dictionary GetSecurityProtocols() return dic; } - public void AddOrderNote(PayPalSettingsBase settings, Order order, string anyString) + public void AddOrderNote(PayPalSettingsBase settings, Order order, string anyString, bool isIpn = false) { try { @@ -201,6 +313,9 @@ public void AddOrderNote(PayPalSettingsBase settings, Order order, string anyStr sb.AppendFormat("{0}", note); + if (isIpn) + order.HasNewPaymentNotification = true; + _orderService.AddOrderNote(order, sb.ToString()); } catch { } @@ -318,7 +433,7 @@ public string CreatePaymentInstruction(PayPalPaymentInstruction instruct) try { - var currency = _currencyService.Value.GetCurrencyByCode(instruct.AmountCurrencyCode); + var currency = _currencyService.GetCurrencyByCode(instruct.AmountCurrencyCode); var format = (currency != null && currency.CustomFormatting.HasValue() ? currency.CustomFormatting : "C"); amount = instruct.Amount.ToString(format, culture); @@ -332,10 +447,11 @@ public string CreatePaymentInstruction(PayPalPaymentInstruction instruct) var intro = T("Plugins.SmartStore.PayPal.PayUponInvoiceLegalNote", _companyInfoSettings.Value.CompanyName.NaIfEmpty()); - if (instruct.Link.HasValue()) - { - intro = "{0} {2}.".FormatInvariant(intro, instruct.Link, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Details)); - } + // /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); @@ -409,7 +525,7 @@ public PaymentStatus GetPaymentStatus(string state, string reasonCode, PaymentSt public PayPalResponse CallApi(string method, string path, string accessToken, PayPalApiSettingsBase settings, string data) { - var isJson = (data.HasValue() && data.StartsWith("{")); + var isJson = (data.HasValue() && (data.StartsWith("{") || data.StartsWith("["))); var encoding = (isJson ? Encoding.UTF8 : Encoding.ASCII); var result = new PayPalResponse(); HttpWebResponse webResponse = null; @@ -448,6 +564,7 @@ public PayPalResponse CallApi(string method, string path, string accessToken, Pa 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"))) { @@ -492,7 +609,7 @@ public PayPalResponse CallApi(string method, string path, string accessToken, Pa var rawResponse = reader.ReadToEnd(); if (rawResponse.HasValue()) { - if (webResponse.ContentType.IsCaseInsensitiveEqual("application/json")) + try { if (rawResponse.StartsWith("[")) result.Json = JArray.Parse(rawResponse); @@ -516,9 +633,10 @@ public PayPalResponse CallApi(string method, string path, string accessToken, Pa } } } - else if (!result.Success) - { - result.ErrorMessage = rawResponse; + catch + { + if (!result.Success) + result.ErrorMessage = rawResponse; } } } @@ -528,7 +646,7 @@ public PayPalResponse CallApi(string method, string path, string accessToken, Pa if (result.ErrorMessage.IsEmpty()) result.ErrorMessage = webResponse.StatusDescription; - LogError(null, result.ErrorMessage, result.Json == null ? null : result.Json.ToString(), false); + LogError(null, result.ErrorMessage, string.Concat(data.NaIfEmpty(), "\r\n\r\n", result.Json == null ? "" : result.Json.ToString()), false); } } } @@ -596,34 +714,14 @@ public PayPalResponse CreatePayment( { var store = _services.StoreContext.CurrentStore; var customer = _services.WorkContext.CurrentCustomer; - var language = _services.WorkContext.WorkingLanguage; - var currencyCode = store.PrimaryStoreCurrency.CurrencyCode; - - var dateOfBirth = customer.GetAttribute(SystemCustomerAttributeNames.DateOfBirth); - - Discount orderAppliedDiscount; - List appliedGiftCards; - int redeemedRewardPoints = 0; - decimal redeemedRewardPointsAmount; - decimal orderDiscountInclTax; - decimal totalOrderItems = decimal.Zero; - - var includingTax = (_services.WorkContext.GetTaxDisplayTypeFor(customer, store.Id) == TaxDisplayType.IncludingTax); - - var shipping = (_orderTotalCalculationService.GetShoppingCartShippingTotal(cart) ?? decimal.Zero); - - var paymentFee = _paymentService.GetAdditionalHandlingFee(cart, providerSystemName); - var total = (_orderTotalCalculationService.GetShoppingCartTotal(cart, out orderDiscountInclTax, out orderAppliedDiscount, out appliedGiftCards, - out redeemedRewardPoints, out redeemedRewardPointsAmount) ?? decimal.Zero); + //var dateOfBirth = customer.GetAttribute(SystemCustomerAttributeNames.DateOfBirth); var data = new Dictionary(); var redirectUrls = new Dictionary(); var payer = new Dictionary(); - var payerInfo = new Dictionary(); + //var payerInfo = new Dictionary(); var transaction = new Dictionary(); - var amount = new Dictionary(); - var amountDetails = new Dictionary(); var items = new List>(); var itemList = new Dictionary(); @@ -647,80 +745,23 @@ public PayPalResponse CreatePayment( data.Add("redirect_urls", redirectUrls); // payer, payer_info - 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)); - } + // 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); + //payer.Add("payer_info", payerInfo); data.Add("payer", payer); - // 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); - - 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 += (productPrice * item.Item.Quantity); - } - - var itemsPlusMisc = (totalOrderItems + shipping + paymentFee); - - if (total != itemsPlusMisc) - { - var line = new Dictionary(); - line.Add("quantity", "1"); - line.Add("name", T("Plugins.SmartStore.PayPal.Other").Text.Truncate(127)); - line.Add("price", (total - itemsPlusMisc).FormatInvariant()); - line.Add("currency", currencyCode); - items.Add(line); - - totalOrderItems += (total - itemsPlusMisc); - } + var amount = CreateAmount(store, customer, cart, providerSystemName, items); itemList.Add("items", items); - if (customer.ShippingAddress != null) - { - itemList.Add("shipping_address", CreateAddress(customer.ShippingAddress, true)); - } - - // transactions - amountDetails.Add("shipping", shipping.FormatInvariant()); - amountDetails.Add("subtotal", totalOrderItems.FormatInvariant()); - 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; - var taxTotal = _orderTotalCalculationService.GetTaxTotal(cart, out taxRates); - - amountDetails.Add("tax", taxTotal.FormatInvariant()); - } - if (paymentFee != decimal.Zero) - { - amountDetails.Add("handling_fee", paymentFee.FormatInvariant()); - } - - amount.Add("total", total.FormatInvariant()); - amount.Add("currency", currencyCode); - amount.Add("details", amountDetails); transaction.Add("amount", amount); transaction.Add("item_list", itemList); @@ -740,6 +781,42 @@ public PayPalResponse CreatePayment( 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(); @@ -1034,9 +1111,7 @@ public HttpStatusCode ProcessWebhook( break; } - order.HasNewPaymentNotification = true; - - AddOrderNote(settings, order, (string)ToInfoString(json)); + AddOrderNote(settings, order, (string)ToInfoString(json), true); return HttpStatusCode.OK; } diff --git a/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj b/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj index 2690f97cb2..e82c4a4097 100644 --- a/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj +++ b/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj @@ -70,7 +70,7 @@ ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True + False diff --git a/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs b/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs index 54fa987d14..b7355b4299 100644 --- a/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs +++ b/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs @@ -25,7 +25,7 @@ public PayPalPlusConfigValidator(ILocalizationService localize, Func x.ThirdPartyPaymentMethods) - .Must(x => x.Count <= 5) + .Must(x => x == null || x.Count <= 5) .WithMessage(localize.GetResource("Plugins.Payments.PayPalPlus.ValidateThirdPartyPaymentMethods")); } } diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml index 212ae653cc..c1f2cdf6f0 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml @@ -1,4 +1,5 @@ -@using SmartStore.PayPal.Models; +@using SmartStore.PayPal +@using SmartStore.PayPal.Models @model PayPalPlusCheckoutModel "); + } + + 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/Models/Settings/CustomerUserSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs index bfbb495345..f395584d6c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs @@ -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/SmartStore.Admin.csproj b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj index 15852be9a4..603f623aa1 100644 --- a/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj +++ b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj @@ -102,6 +102,7 @@ + False @@ -213,6 +214,7 @@ + @@ -429,6 +431,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -461,6 +724,12 @@ + + + + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml index e3cd585606..f47ae86481 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml @@ -296,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 { 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/_ProfileList.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/_ProfileList.cshtml index ffa740bd96..7ae123c0a9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/_ProfileList.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/_ProfileList.cshtml @@ -87,7 +87,10 @@ } else { -
@T("Admin.DataExchange.Export.NoProfiles")
+
+ + @T("Admin.DataExchange.Export.NoProfiles") +
} @if (!isSystemProfiles) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Deployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Deployment.cshtml index 1db883b2d3..d77bc3cf54 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Deployment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Deployment.cshtml @@ -104,7 +104,7 @@ {
- @T("Admin.DataExchange.Export.NoProfiles") + @T("Admin.DataExchange.Export.Deployment.NoProfiles")
} diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml index 6749179a34..75fefead92 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml @@ -1775,7 +1775,7 @@ @Html.SmartLabelFor(model => model.AddOrderNoteMessage) - @Html.TextAreaFor(model => model.AddOrderNoteMessage, new { style = "Width: 350px; Height: 150px;" }) + @Html.TextAreaFor(model => model.AddOrderNoteMessage, new { @class = "control-xlarge", style = "height: 120px;" }) @Html.ValidationMessageFor(model => model.AddOrderNoteMessage) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml index 92de51ade3..9e07dff329 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml @@ -29,12 +29,13 @@ @if (!ViewBag.IsEdit) { -
+
@if (Model.Warnings.Count > 0) { -
- @foreach (var warning in Model.Warnings) { -

 @warning

+
+ @foreach (var warning in Model.Warnings) + { +

 @warning

}
} diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml index 1d868a640d..ccb0eb2876 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml @@ -259,6 +259,15 @@ @Html.ValidationMessageFor(model => model.CustomerSettings.StoreLastVisitedPage) + + + @Html.SmartLabelFor(model => model.CustomerSettings.DisplayPrivacyAgreementOnContactUs) + + + @Html.SettingEditorFor(model => model.CustomerSettings.DisplayPrivacyAgreementOnContactUs) + @Html.ValidationMessageFor(model => model.CustomerSettings.DisplayPrivacyAgreementOnContactUs) + + } @helper TabCustomerFormFields() diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml index 1ba69f79da..04db8e0e26 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml @@ -29,7 +29,7 @@ "); - } - else{ - _r.Write(GetErrorRes(ex.Message)); - } - } - - } - private string FixPath(string path) - { - if (!path.StartsWith("~")){ - if (!path.StartsWith("/")) - path = "/" + path; - path = "~" + path; - } - return _context.Server.MapPath(path); - } - private string GetLangFile(){ - return "../lang/en.json"; - } - 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 = 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(confFile); - } - 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 (!File.Exists(Path.Combine(dest, file.Name))){ - 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); - } - _r.Write(GetSuccessRes()); - } - protected string MakeUniqueFilename(string dir, string filename){ - string ret = filename; - int i = 0; - while (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{ - File.Copy(file.FullName, Path.Combine(newPath, newName)); - _r.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); - _r.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); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_CannotDeleteDir")); - } - } - } - protected void DeleteFile(string path) - { - CheckPath(path); - path = FixPath(path); - if (!File.Exists(path)) - throw new Exception(LangRes("E_DeleteFileInvalidPath")); - else - { - try - { - File.Delete(path); - _r.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("~/"); - _r.Write("["); - for(int i = 0; i files = GetFiles(fullPath, type); - _r.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; - } - } - _r.Write("{"); - _r.Write("\"p\":\""+path + "/" + f.Name+"\""); - _r.Write(",\"t\":\"" + Math.Ceiling(LinuxTimestamp(f.LastWriteTime)).ToString() + "\""); - _r.Write(",\"s\":\""+f.Length.ToString()+"\""); - _r.Write(",\"w\":\""+w.ToString()+"\""); - _r.Write(",\"h\":\""+h.ToString()+"\""); - _r.Write("}"); - if (i < files.Count - 1) - _r.Write(","); - } - _r.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(File.Exists(tmpZip)) - File.Delete(tmpZip); - ZipFile.CreateFromDirectory(path, tmpZip,CompressionLevel.Fastest, true); - _r.Clear(); - _r.Headers.Add("Content-Disposition", "attachment; filename=\"" + dirName + ".zip\""); - _r.ContentType = "application/force-download"; - _r.TransmitFile(tmpZip); - _r.Flush(); - File.Delete(tmpZip); - _r.End(); - } - protected void DownloadFile(string path) - { - CheckPath(path); - FileInfo file = new FileInfo(FixPath(path)); - if(file.Exists){ - _r.Clear(); - _r.Headers.Add("Content-Disposition", "attachment; filename=\"" + file.Name + "\""); - _r.ContentType = "application/force-download"; - _r.TransmitFile(file.FullName); - _r.Flush(); - _r.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); - _r.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); - _r.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); - _r.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); - _r.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); - - _r.AddHeader("Content-Type", "image/png"); - cropImg.GetThumbnailImage(width, height, imgCallback, IntPtr.Zero).Save(_r.OutputStream, ImageFormat.Png); - _r.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 < HttpContext.Current.Request.Files.Count; i++){ - if (CanHandleFile(HttpContext.Current.Request.Files[i].FileName)) - { - string filename = MakeUniqueFilename(path, HttpContext.Current.Request.Files[i].FileName); - string dest = Path.Combine(path, filename); - HttpContext.Current.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); - } - _r.Write(""); - } - - public bool IsReusable { - get { - return false; - } - } - -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/conf.json b/src/Presentation/SmartStore.Web/Content/filemanager/conf.json deleted file mode 100644 index 3df29bf505..0000000000 --- a/src/Presentation/SmartStore.Web/Content/filemanager/conf.json +++ /dev/null @@ -1,34 +0,0 @@ -{ -"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": "asp_net/main.ashx?a=DIRLIST", -"CREATEDIR": "asp_net/main.ashx?a=CREATEDIR", -"DELETEDIR": "asp_net/main.ashx?a=DELETEDIR", -"MOVEDIR": "asp_net/main.ashx?a=MOVEDIR", -"COPYDIR": "asp_net/main.ashx?a=COPYDIR", -"RENAMEDIR": "asp_net/main.ashx?a=RENAMEDIR", -"FILESLIST": "asp_net/main.ashx?a=FILESLIST", -"UPLOAD": "asp_net/main.ashx?a=UPLOAD", -"DOWNLOAD": "asp_net/main.ashx?a=DOWNLOAD", -"DOWNLOADDIR": "asp_net/main.ashx?a=DOWNLOADDIR", -"DOWNLOADDIR": "asp_net/main.ashx?a=DOWNLOADDIR", -"DELETEFILE": "asp_net/main.ashx?a=DELETEFILE", -"MOVEFILE": "asp_net/main.ashx?a=MOVEFILE", -"COPYFILE": "asp_net/main.ashx?a=COPYFILE", -"RENAMEFILE": "asp_net/main.ashx?a=RENAMEFILE", -"GENERATETHUMB": "asp_net/main.ashx?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/Controllers/CatalogController.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs index 6c75df166f..9db9fa78bb 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs @@ -133,10 +133,13 @@ public ActionResult Category(int categoryId, CatalogPagingFilteringModel command return HttpNotFound(); //'Continue shopping' URL - _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, - SystemCustomerAttributeNames.LastContinueShoppingPage, - _services.WebHelper.GetThisPageUrl(false), - _services.StoreContext.CurrentStore.Id); + if (!_services.WorkContext.CurrentCustomer.IsSystemAccount) + { + _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, + SystemCustomerAttributeNames.LastContinueShoppingPage, + _services.WebHelper.GetThisPageUrl(false), + _services.StoreContext.CurrentStore.Id); + } if (command.PageNumber <= 0) command.PageNumber = 1; @@ -440,11 +443,14 @@ public ActionResult Manufacturer(int manufacturerId, CatalogPagingFilteringModel if (!_storeMappingService.Authorize(manufacturer)) return HttpNotFound(); - //'Continue shopping' URL - _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, - SystemCustomerAttributeNames.LastContinueShoppingPage, - _services.WebHelper.GetThisPageUrl(false), - _services.StoreContext.CurrentStore.Id); + //'Continue shopping' URL + if (!_services.WorkContext.CurrentCustomer.IsSystemAccount) + { + _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, + SystemCustomerAttributeNames.LastContinueShoppingPage, + _services.WebHelper.GetThisPageUrl(false), + _services.StoreContext.CurrentStore.Id); + } if (command.PageNumber <= 0) command.PageNumber = 1; @@ -1086,13 +1092,15 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command model = new SearchModel(); // 'Continue shopping' URL - _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, - SystemCustomerAttributeNames.LastContinueShoppingPage, - _services.WebHelper.GetThisPageUrl(false), - _services.StoreContext.CurrentStore.Id); + if (!_services.WorkContext.CurrentCustomer.IsSystemAccount) + { + _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, + SystemCustomerAttributeNames.LastContinueShoppingPage, + _services.WebHelper.GetThisPageUrl(false), + _services.StoreContext.CurrentStore.Id); + } if (command.PageSize <= 0) - command.PageSize = _catalogSettings.SearchPageProductsPerPage; if (command.PageNumber <= 0) command.PageNumber = 1; diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs index 55793edc65..ebc506887c 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; -using System.Linq.Expressions; using System.Web; using System.Web.Mvc; using SmartStore.Collections; @@ -25,14 +25,12 @@ using SmartStore.Services.Security; using SmartStore.Services.Seo; using SmartStore.Services.Tax; +using SmartStore.Services.Topics; using SmartStore.Web.Framework.UI; using SmartStore.Web.Framework.UI.Captcha; using SmartStore.Web.Infrastructure.Cache; using SmartStore.Web.Models.Catalog; using SmartStore.Web.Models.Media; -using SmartStore.Core.Data; -using System.Collections.Specialized; -using SmartStore.Services.Topics; namespace SmartStore.Web.Controllers { @@ -152,7 +150,8 @@ public ProductDetailsModel PrepareProductDetailsPageModel( bool isAssociatedProduct = false, ProductBundleItemData productBundleItem = null, IList productBundleItems = null, - NameValueCollection selectedAttributes = null) + NameValueCollection selectedAttributes = null, + NameValueCollection queryData = null) { if (product == null) throw new ArgumentNullException("product"); @@ -187,6 +186,30 @@ public ProductDetailsModel PrepareProductDetailsPageModel( IsCurrentCustomerRegistered = _services.WorkContext.CurrentCustomer.IsRegistered() }; + // get gift card values from query string + if (queryData != null && queryData.Count > 0) + { + var giftCardItems = queryData.AllKeys + .Where(x => x.EmptyNull().StartsWith("giftcard_")) + .SelectMany(queryData.GetValues, (k, v) => new { key = k, value = v.TrimSafe() }); + + foreach (var item in giftCardItems) + { + var key = item.key.EmptyNull().ToLower(); + + if (key.EndsWith("recipientname")) + model.GiftCard.RecipientName = item.value; + else if (key.EndsWith("recipientemail")) + model.GiftCard.RecipientEmail = item.value; + else if (key.EndsWith("sendername")) + model.GiftCard.SenderName = item.value; + else if (key.EndsWith("senderemail")) + model.GiftCard.SenderEmail = item.value; + else if (key.EndsWith("message")) + model.GiftCard.Message = item.value; + } + } + // Back in stock subscriptions if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock && product.BackorderMode == BackorderMode.NoBackorders && @@ -505,9 +528,31 @@ public ProductDetailsModel PrepareProductDetailModel( pvaModel.BeginYear = match.Groups[1].Value.ToInt(); pvaModel.EndYear = match.Groups[2].Value.ToInt(); } + + if (hasSelectedAttributes) + { + var attributeKey = "product_attribute_{0}_{1}_{2}_{3}".FormatInvariant(product.Id, bundleItemId, attribute.ProductAttributeId, attribute.Id); + var day = selectedAttributes[attributeKey + "_day"].ToInt(); + var month = selectedAttributes[attributeKey + "_month"].ToInt(); + var year = selectedAttributes[attributeKey + "_year"].ToInt(); + if (day > 0 && month > 0 && year > 0) + { + pvaModel.SelectedDay = day; + pvaModel.SelectedMonth = month; + pvaModel.SelectedYear = year; + } + } + } + else if (attribute.AttributeControlType == AttributeControlType.TextBox || attribute.AttributeControlType == AttributeControlType.MultilineTextbox) + { + if (hasSelectedAttributes) + { + var attributeKey = "product_attribute_{0}_{1}_{2}_{3}".FormatInvariant(product.Id, bundleItemId, attribute.ProductAttributeId, attribute.Id); + pvaModel.TextValue = selectedAttributes[attributeKey]; + } } - int preSelectedValueId = 0; + var preSelectedValueId = 0; var pvaValues = (attribute.ShouldHaveValues() ? _productAttributeService.GetProductVariantAttributeValues(attribute.Id) : new List()); foreach (var pvaValue in pvaValues) diff --git a/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs b/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs index cd3eb7dd5d..a72b4fc872 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs @@ -732,8 +732,8 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha } case UserRegistrationType.Standard: { - // send customer welcome message - _workflowMessageService.SendCustomerWelcomeMessage(customer, _workContext.WorkingLanguage.Id); + // send customer welcome message + _workflowMessageService.SendCustomerWelcomeMessage(customer, _workContext.WorkingLanguage.Id); var redirectUrl = Url.RouteUrl("RegisterResult", new { resultId = (int)UserRegistrationType.Standard }); if (!String.IsNullOrEmpty(returnUrl)) @@ -915,8 +915,8 @@ public ActionResult AccountActivation(string token, string email) _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.AccountActivationToken, ""); //send welcome message _workflowMessageService.SendCustomerWelcomeMessage(customer, _workContext.WorkingLanguage.Id); - - var model = new AccountActivationModel(); + + var model = new AccountActivationModel(); model.Result = _localizationService.GetResource("Account.AccountActivation.Activated"); return View(model); } diff --git a/src/Presentation/SmartStore.Web/Controllers/HomeController.cs b/src/Presentation/SmartStore.Web/Controllers/HomeController.cs index e16d5e80b7..58e6eb4ace 100644 --- a/src/Presentation/SmartStore.Web/Controllers/HomeController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/HomeController.cs @@ -6,6 +6,7 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Cms; using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Infrastructure; using SmartStore.Core.Localization; @@ -41,6 +42,7 @@ public partial class HomeController : PublicControllerBase private readonly Lazy _sitemapGenerator; private readonly Lazy _captchaSettings; private readonly Lazy _commonSettings; + private readonly Lazy _customerSettings; #endregion @@ -56,7 +58,8 @@ public HomeController( Lazy emailAccountService, Lazy sitemapGenerator, Lazy captchaSettings, - Lazy commonSettings) + Lazy commonSettings, + Lazy customerSettings) { this._services = services; this._categoryService = categoryService; @@ -68,6 +71,7 @@ public HomeController( this._sitemapGenerator = sitemapGenerator; this._captchaSettings = captchaSettings; this._commonSettings = commonSettings; + this._customerSettings = customerSettings; } #endregion @@ -119,7 +123,9 @@ public ActionResult ContactUs() { Email = _services.WorkContext.CurrentCustomer.Email, FullName = _services.WorkContext.CurrentCustomer.GetFullName(), - DisplayCaptcha = _captchaSettings.Value.Enabled && _captchaSettings.Value.ShowOnContactUsPage + DisplayCaptcha = _captchaSettings.Value.Enabled && _captchaSettings.Value.ShowOnContactUsPage, + DisplayPrivacyAgreement = _customerSettings.Value.DisplayPrivacyAgreementOnContactUs + }; return View(model); diff --git a/src/Presentation/SmartStore.Web/Controllers/ProductController.cs b/src/Presentation/SmartStore.Web/Controllers/ProductController.cs index 60bd17bca2..79497418a9 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ProductController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ProductController.cs @@ -8,7 +8,6 @@ using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Localization; using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Common; @@ -31,7 +30,7 @@ namespace SmartStore.Web.Controllers { - public partial class ProductController : PublicControllerBase + public partial class ProductController : PublicControllerBase { #region Fields @@ -168,14 +167,16 @@ public ActionResult ProductDetails(int productId, string attributes) } } - //prepare the model var selectedAttributes = new NameValueCollection(); - selectedAttributes.ConvertAttributeQueryData( + // get selected attributes from query string + selectedAttributes.GetSelectedAttributes( + Request.QueryString, _productAttributeParser.DeserializeQueryData(attributes), product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing ? 0 : product.Id); - var model = _helper.PrepareProductDetailsPageModel(product, selectedAttributes: selectedAttributes); + // prepare the view model + var model = _helper.PrepareProductDetailsPageModel(product, selectedAttributes: selectedAttributes, queryData: Request.QueryString); //save as recently viewed _recentlyViewedProductsService.AddProductToRecentlyViewedList(product.Id); diff --git a/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs b/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs index f194738cb8..e4ff2cb6a7 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs @@ -996,7 +996,6 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() //products. sort descending (recently added products) foreach (var sci in cart - .OrderByDescending(x => x.Item.Id) .Take(_shoppingCartSettings.MiniShoppingCartProductNumber) .ToList()) { @@ -2633,7 +2632,7 @@ public ActionResult FlyoutWishlist() { Customer customer = _workContext.CurrentCustomer; - var cart = customer.GetCartItems(ShoppingCartType.Wishlist, _storeContext.CurrentStore.Id, true); + var cart = customer.GetCartItems(ShoppingCartType.Wishlist, _storeContext.CurrentStore.Id); var model = new WishlistModel(); PrepareWishlistModel(model, cart, true); diff --git a/src/Presentation/SmartStore.Web/Global.asax.cs b/src/Presentation/SmartStore.Web/Global.asax.cs index 6353e1e31a..4297e54186 100644 --- a/src/Presentation/SmartStore.Web/Global.asax.cs +++ b/src/Presentation/SmartStore.Web/Global.asax.cs @@ -1,18 +1,20 @@ -using System.Linq; +using System; +using System.Linq; using System.Web; using System.Web.Hosting; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; +using System.Web.Security; using System.Web.WebPages; using FluentValidation.Mvc; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Events; using SmartStore.Core.Infrastructure; +using SmartStore.Services.Customers; using SmartStore.Services.Tasks; using SmartStore.Web.Framework.Bundling; -using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Localization; using SmartStore.Web.Framework.Modelling; @@ -21,15 +23,12 @@ using SmartStore.Web.Framework.Theming; using SmartStore.Web.Framework.Validators; - namespace SmartStore.Web { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 - public class MvcApplication : System.Web.HttpApplication { - public static void RegisterGlobalFilters(GlobalFilterCollection filters) { var eventPublisher = EngineContext.Current.Resolve(); @@ -162,6 +161,19 @@ public override string GetVaryByCustomString(HttpContext context, string custom) return base.GetVaryByCustomString(context, custom); } - } - + public void AnonymousIdentification_Creating(object sender, AnonymousIdentificationEventArgs args) + { + try + { + var customerService = EngineContext.Current.Resolve(); + var customer = customerService.FindGuestCustomerByClientIdent(maxAgeSeconds: 180); + if (customer != null) + { + // We found our anonymous visitor: don't let ASP.NET create a new id. + args.AnonymousID = customer.CustomerGuid.ToString(); + } + } + catch { } + } + } } diff --git a/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs b/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs index f20fd69129..db9ab838bc 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs @@ -10,6 +10,11 @@ namespace SmartStore.Web.Models.Common [Validator(typeof(ContactUsValidator))] public partial class ContactUsModel : ModelBase { + [SmartResourceDisplayName("ContactUs.PrivacyAgreement")] + public bool PrivacyAgreement { get; set; } + + public bool DisplayPrivacyAgreement { get; set; } + [AllowHtml] [SmartResourceDisplayName("ContactUs.Email")] [DataType(DataType.EmailAddress)] diff --git a/src/Presentation/SmartStore.Web/SmartStore.Web.csproj b/src/Presentation/SmartStore.Web/SmartStore.Web.csproj index 2bd35ba917..8340a62bfb 100644 --- a/src/Presentation/SmartStore.Web/SmartStore.Web.csproj +++ b/src/Presentation/SmartStore.Web/SmartStore.Web.csproj @@ -645,267 +645,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -919,13 +658,6 @@ - - - - - - - diff --git a/src/Presentation/SmartStore.Web/Validators/Common/ContactUsValidator.cs b/src/Presentation/SmartStore.Web/Validators/Common/ContactUsValidator.cs index 7e9b037037..9e42f7ed02 100644 --- a/src/Presentation/SmartStore.Web/Validators/Common/ContactUsValidator.cs +++ b/src/Presentation/SmartStore.Web/Validators/Common/ContactUsValidator.cs @@ -8,6 +8,11 @@ public class ContactUsValidator : AbstractValidator { public ContactUsValidator(ILocalizationService localizationService) { + RuleFor(x => x.PrivacyAgreement) + .Must(x => x == true) + .WithMessage(localizationService.GetResource("ContactUs.PrivacyAgreement.MustBeAccepted")) + .When(x => x.DisplayPrivacyAgreement == true); + RuleFor(x => x.Email).NotEmpty().WithMessage(localizationService.GetResource("ContactUs.Email.Required")); RuleFor(x => x.Email).EmailAddress().WithMessage(localizationService.GetResource("Common.WrongEmail")); RuleFor(x => x.FullName).NotEmpty().WithMessage(localizationService.GetResource("ContactUs.FullName.Required")); diff --git a/src/Presentation/SmartStore.Web/Views/Home/ContactUs.cshtml b/src/Presentation/SmartStore.Web/Views/Home/ContactUs.cshtml index d0b173f3f2..7105b3d794 100644 --- a/src/Presentation/SmartStore.Web/Views/Home/ContactUs.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Home/ContactUs.cshtml @@ -24,11 +24,6 @@ {
- @if (!ViewData.ModelState.IsValid) - { - @Html.ValidationSummary(true) - } - @T("PageTitle.ContactUs")
@@ -55,6 +50,28 @@
+ @if(Model.DisplayPrivacyAgreement) + { + @Html.HiddenFor(model => model.DisplayPrivacyAgreement) + +
+ @Html.LabelFor(model => model.PrivacyAgreement, new { @class = "control-label required", @for = "PrivacyAgreement" }) +
+
+ +
+
+ @Html.ValidationMessageFor(model => model.PrivacyAgreement) +
+
+
+ } + @if (Model.DisplayCaptcha) {
diff --git a/src/Presentation/SmartStore.Web/Views/Product/Reviews.cshtml b/src/Presentation/SmartStore.Web/Views/Product/Reviews.cshtml index 0d8f9ba9a8..f819e40646 100644 --- a/src/Presentation/SmartStore.Web/Views/Product/Reviews.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Product/Reviews.cshtml @@ -6,6 +6,11 @@ //title Html.AddTitleParts(Model.ProductName); Html.AddTitleParts(T("PageTitle.ProductReviews").Text); + + if (Model.Items.Count == 0) + { + Html.AddCustomHeadParts(""); + } }

@T("Reviews.ProductReviewsFor")

diff --git a/src/Presentation/SmartStore.Web/Web.config b/src/Presentation/SmartStore.Web/Web.config index a08b655642..eeab4dda13 100644 --- a/src/Presentation/SmartStore.Web/Web.config +++ b/src/Presentation/SmartStore.Web/Web.config @@ -121,6 +121,16 @@ + diff --git a/src/SmartStoreNET.sln b/src/SmartStoreNET.sln index 15bd2323aa..6eab37e315 100644 --- a/src/SmartStoreNET.sln +++ b/src/SmartStoreNET.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A340B280-C12B-47E7-90B7-89C650F8AAFC}" EndProject diff --git a/src/Tests/SmartStore.Services.Tests/Customers/CustomerRegistrationServiceTests.cs b/src/Tests/SmartStore.Services.Tests/Customers/CustomerRegistrationServiceTests.cs index 990c27b02f..81be79200c 100644 --- a/src/Tests/SmartStore.Services.Tests/Customers/CustomerRegistrationServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Customers/CustomerRegistrationServiceTests.cs @@ -9,6 +9,7 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Security; using SmartStore.Core.Events; +using SmartStore.Core.Fakes; using SmartStore.Services.Common; using SmartStore.Services.Customers; using SmartStore.Services.Messages; @@ -34,8 +35,10 @@ public class CustomerRegistrationServiceTests : ServiceTest RewardPointsSettings _rewardPointsSettings; SecuritySettings _securitySettings; IStoreContext _storeContext; + ICommonServices _services; + IUserAgent _userAgent; - [SetUp] + [SetUp] public new void SetUp() { _customerSettings = new CustomerSettings(); @@ -106,19 +109,24 @@ public class CustomerRegistrationServiceTests : ServiceTest _eventPublisher = MockRepository.GenerateMock(); _eventPublisher.Expect(x => x.Publish(Arg.Is.Anything)); - _customerRepo.Expect(x => x.Table).Return(new List() { customer1, customer2, customer3, customer4, customer5 }.AsQueryable()); + _customerRepo.Expect(x => x.Table).Return(new List() { customer1, customer2, customer3, customer4, customer5 }.AsQueryable()); _customerRoleRepo = MockRepository.GenerateMock>(); _genericAttributeRepo = MockRepository.GenerateMock>(); _rewardPointsHistoryRepo = MockRepository.GenerateMock>(); + _userAgent = MockRepository.GenerateMock(); ; - _genericAttributeService = MockRepository.GenerateMock(); + _genericAttributeService = MockRepository.GenerateMock(); _newsLetterSubscriptionService = MockRepository.GenerateMock(); _storeContext = MockRepository.GenerateMock(); - _customerService = new CustomerService(new NullCache(), _customerRepo, _customerRoleRepo, - _genericAttributeRepo, _rewardPointsHistoryRepo, _genericAttributeService, _eventPublisher, _rewardPointsSettings); + _services = MockRepository.GenerateMock(); + _services.Expect(x => x.StoreContext).Return(_storeContext); + _services.Expect(x => x.Cache).Return(NullCache.Instance); + + _customerService = new CustomerService(_customerRepo, _customerRoleRepo, + _genericAttributeRepo, _rewardPointsHistoryRepo, _genericAttributeService, _services, NullCache.Instance, _rewardPointsSettings, new FakeHttpContext("~/"), _userAgent); _customerRegistrationService = new CustomerRegistrationService(_customerService, _encryptionService, _newsLetterSubscriptionService, _rewardPointsSettings, _customerSettings, _storeContext, _eventPublisher);