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("
", downloadUrl + HttpUtility.UrlDecode(file.FileName), file.FileName);
+ body.AppendFormat("", 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