From 898d25f3cb6c4d036f685e9d9afbfca62ebda47e Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 31 Jul 2013 18:31:39 +0200 Subject: [PATCH 01/21] Include lang files in project so they'll actually end up in the final build --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 709c257b86..4fe113f747 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -916,6 +916,7 @@ + @@ -936,6 +937,7 @@ + @@ -958,6 +960,7 @@ + @@ -977,6 +980,7 @@ + @@ -1029,6 +1033,7 @@ + @@ -1058,6 +1063,7 @@ + @@ -1091,6 +1097,7 @@ + @@ -1140,6 +1147,7 @@ + @@ -1170,6 +1178,7 @@ + @@ -1194,6 +1203,7 @@ + @@ -1223,6 +1233,7 @@ + @@ -1246,6 +1257,7 @@ + @@ -1260,8 +1272,12 @@ + + + + @@ -1270,6 +1286,8 @@ + + @@ -1278,8 +1296,12 @@ + + + + @@ -1287,8 +1309,10 @@ + + @@ -1297,6 +1321,7 @@ + @@ -1312,6 +1337,8 @@ + + @@ -1353,6 +1380,7 @@ + From bdb6c5768aee71b6f3560c38b97db8f8ded38b05 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 1 Aug 2013 13:40:23 +0200 Subject: [PATCH 02/21] U4-2553 Double tabs when installing package that makes use of Dictionary Items to translate document types Added GetRawCaptionById method for the packager so the generated XML is correct --- src/umbraco.cms/businesslogic/ContentType.cs | 129 +++++++++++-------- 1 file changed, 73 insertions(+), 56 deletions(-) diff --git a/src/umbraco.cms/businesslogic/ContentType.cs b/src/umbraco.cms/businesslogic/ContentType.cs index ebacf89fa1..71d676f54d 100644 --- a/src/umbraco.cms/businesslogic/ContentType.cs +++ b/src/umbraco.cms/businesslogic/ContentType.cs @@ -70,7 +70,7 @@ namespace umbraco.cms.businesslogic _alias = alias; _iconurl = icon; _thumbnail = thumbnail; - + if (masterContentType.HasValue) MasterContentType = masterContentType.Value; @@ -92,7 +92,7 @@ namespace umbraco.cms.businesslogic allowAtRoot, isContainer, Alias,icon,thumbnail,description FROM umbracoNode INNER JOIN cmsContentType ON umbracoNode.id = cmsContentType.nodeId WHERE nodeObjectType = @nodeObjectType"; - + #endregion #region Static Methods @@ -113,12 +113,12 @@ namespace umbraco.cms.businesslogic /// internal static IDictionary GetAliasesAndNames(string contentTypeAlias) { - return AliasToNames.GetOrAdd(contentTypeAlias, s => - { - var ct = GetByAlias(contentTypeAlias); - var userFields = ct.PropertyTypes.ToDictionary(x => x.Alias, x => x.Name); - return userFields; - }); + return AliasToNames.GetOrAdd(contentTypeAlias, s => + { + var ct = GetByAlias(contentTypeAlias); + var userFields = ct.PropertyTypes.ToDictionary(x => x.Alias, x => x.Name); + return userFields; + }); } /// @@ -150,21 +150,21 @@ namespace umbraco.cms.businesslogic //propertyTypeAlias needs to be invariant, so we will store uppercase var key = new System.Tuple(contentTypeAlias, propertyTypeAlias.ToUpper()); - + return PropertyTypeCache.GetOrAdd( key, tuple => - { - // With 4.10 we can't do this via direct SQL as we have content type mixins - var controlId = Guid.Empty; - var ct = GetByAlias(contentTypeAlias); + { + // With 4.10 we can't do this via direct SQL as we have content type mixins + var controlId = Guid.Empty; + var ct = GetByAlias(contentTypeAlias); var pt = ct.getPropertyType(propertyTypeAlias); if (pt != null) { controlId = pt.DataTypeDefinition.DataType.Id; } - return controlId; - }); + return controlId; + }); } /// @@ -244,7 +244,7 @@ namespace umbraco.cms.businesslogic private bool _isContainerContentType; private List _allowedChildContentTypeIDs; private List _virtualTabs; - + protected internal IContentTypeComposition ContentTypeItem; #endregion @@ -264,7 +264,7 @@ namespace umbraco.cms.businesslogic foreach (var i in GetContentIdsForContentType()) { RebuildXmlStructureForContentItem(i); - } + } } /// @@ -292,7 +292,7 @@ namespace umbraco.cms.businesslogic dr.Close(); } return ids; - } + } /// /// Rebuilds the xml structure for the content item by id @@ -325,7 +325,7 @@ namespace umbraco.cms.businesslogic INNER JOIN cmsContentType ON cmsContent.contentType = cmsContentType.nodeId WHERE cmsContentType.nodeId = @nodeId)", SqlHelper.CreateParameter("@nodeId", this.Id)); - } + } #endregion @@ -443,7 +443,7 @@ namespace umbraco.cms.businesslogic } } } - + /// /// Gets or sets the description. /// @@ -580,41 +580,41 @@ namespace umbraco.cms.businesslogic cacheKey, TimeSpan.FromMinutes(15), () => + { + //MCH NOTE: For the timing being I have changed this to a dictionary to ensure that property types + //aren't added multiple times through the MasterContentType structure, because each level loads + //its own + inherited property types, which is wrong. Once we are able to fully switch to the new api + //this should no longer be a problem as the composition always contains a correct list of property types. + var result = new Dictionary(); + using (IRecordsReader dr = + SqlHelper.ExecuteReader( + "select id from cmsPropertyType where contentTypeId = @ctId order by sortOrder", + SqlHelper.CreateParameter("@ctId", Id))) { - //MCH NOTE: For the timing being I have changed this to a dictionary to ensure that property types - //aren't added multiple times through the MasterContentType structure, because each level loads - //its own + inherited property types, which is wrong. Once we are able to fully switch to the new api - //this should no longer be a problem as the composition always contains a correct list of property types. - var result = new Dictionary(); - using (IRecordsReader dr = - SqlHelper.ExecuteReader( - "select id from cmsPropertyType where contentTypeId = @ctId order by sortOrder", - SqlHelper.CreateParameter("@ctId", Id))) + while (dr.Read()) { - while (dr.Read()) + int id = dr.GetInt("id"); + PropertyType pt = PropertyType.GetPropertyType(id); + if (pt != null) + result.Add(pt.Id, pt); + } + } + + // Get Property Types from the master content type + if (MasterContentTypes.Count > 0) + { + foreach (var mct in MasterContentTypes) + { + var pts = GetContentType(mct).PropertyTypes; + foreach (var pt in pts) { - int id = dr.GetInt("id"); - PropertyType pt = PropertyType.GetPropertyType(id); - if (pt != null) + if (result.ContainsKey(pt.Id) == false) result.Add(pt.Id, pt); } } - - // Get Property Types from the master content type - if (MasterContentTypes.Count > 0) - { - foreach (var mct in MasterContentTypes) - { - var pts = GetContentType(mct).PropertyTypes; - foreach (var pt in pts) - { - if (result.ContainsKey(pt.Id) == false) - result.Add(pt.Id, pt); - } - } - } - return result.Select(x => x.Value).ToList(); - }); + } + return result.Select(x => x.Value).ToList(); + }); } } @@ -850,7 +850,7 @@ namespace umbraco.cms.businesslogic foreach (var i in value) { int id = i; - list.Add(new ContentTypeSort{Id = new Lazy(() => id), SortOrder = sort}); + list.Add(new ContentTypeSort { Id = new Lazy(() => id), SortOrder = sort }); sort++; } @@ -963,7 +963,7 @@ namespace umbraco.cms.businesslogic { ContentTypeItem.RemovePropertyType(pt.Alias); } - + // Remove from cache FlushFromCache(Id); } @@ -1163,7 +1163,7 @@ namespace umbraco.cms.businesslogic /// /// The id. public static void FlushFromCache(int id) - { + { //Ensure that MediaTypes are reloaded from db by clearing cache InMemoryCacheProvider.Current.Clear(); @@ -1428,12 +1428,12 @@ namespace umbraco.cms.businesslogic public List GetAllPropertyTypes() { var db = ApplicationContext.Current.DatabaseContext.Database; - var propertyTypeDtos = db.Fetch("WHERE propertyTypeGroupId = @Id", new {Id = _id}); + var propertyTypeDtos = db.Fetch("WHERE propertyTypeGroupId = @Id", new { Id = _id }); var tmp = propertyTypeDtos .Select(propertyTypeDto => PropertyType.GetPropertyType(propertyTypeDto.Id)) .ToList(); - var propertyTypeGroupDtos = db.Fetch("WHERE parentGroupId = @Id", new {Id = _id}); + var propertyTypeGroupDtos = db.Fetch("WHERE parentGroupId = @Id", new { Id = _id }); foreach (var propertyTypeGroupDto in propertyTypeGroupDtos) { var inheritedPropertyTypeDtos = db.Fetch("WHERE propertyTypeGroupId = @Id", new { Id = propertyTypeGroupDto.Id }); @@ -1504,6 +1504,24 @@ namespace umbraco.cms.businesslogic } } + /// + /// Gets the tab caption by id. + /// + /// The id. + /// + internal static string GetRawCaptionById(int id) + { + try + { + var tempCaption = SqlHelper.ExecuteScalar("Select text from cmsPropertyTypeGroup where id = " + id); + return tempCaption; + } + catch + { + return null; + } + } + private readonly int _id; private int? _sortOrder; @@ -1629,7 +1647,7 @@ namespace umbraco.cms.businesslogic { if (!_caption.StartsWith("#")) return _caption; - + var lang = Language.GetByCultureCode(Thread.CurrentThread.CurrentCulture.Name); if (lang != null) { @@ -1645,6 +1663,5 @@ namespace umbraco.cms.businesslogic } } #endregion - } } From 3435a75e9e0aa54d29d52a158311b78f739aff7e Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 1 Aug 2013 14:39:22 +0200 Subject: [PATCH 03/21] U4-2553 Double tabs when installing package that makes use of Dictionary Items to translate document types Forgto to commit a file :) --- src/umbraco.cms/businesslogic/web/DocumentType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/umbraco.cms/businesslogic/web/DocumentType.cs b/src/umbraco.cms/businesslogic/web/DocumentType.cs index 1a9e203ada..31d5d89e14 100644 --- a/src/umbraco.cms/businesslogic/web/DocumentType.cs +++ b/src/umbraco.cms/businesslogic/web/DocumentType.cs @@ -527,7 +527,7 @@ namespace umbraco.cms.businesslogic.web //Datatype definition guid was added in v4 to enable datatype imports ptx.AppendChild(XmlHelper.AddTextNode(xd, "Definition", pt.DataTypeDefinition.UniqueId.ToString())); - ptx.AppendChild(XmlHelper.AddTextNode(xd, "Tab", Tab.GetCaptionById(pt.TabId))); + ptx.AppendChild(XmlHelper.AddTextNode(xd, "Tab", Tab.GetRawCaptionById(pt.TabId))); ptx.AppendChild(XmlHelper.AddTextNode(xd, "Mandatory", pt.Mandatory.ToString())); ptx.AppendChild(XmlHelper.AddTextNode(xd, "Validation", pt.ValidationRegExp)); ptx.AppendChild(XmlHelper.AddCDataNode(xd, "Description", pt.GetRawDescription())); From c4b44ea0e391a53381d1e47e67625245e316c2f7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 2 Aug 2013 15:16:04 +1000 Subject: [PATCH 04/21] Fixes: U4-2577 Can't save umbraco user - without re-filling in the password Fixes: U4-541 Wrong dictionary key when using in backend template names This changes the way that the value that is stored in the auth cookie. Previously we just stored a GUID which was the user's contextid stored in the db, now we store encrypted values of a few necessary user objects. In 6.2 we'll actually set a real .Net user object on the HttpContext. For now, the http module will simply just ensure that the culture is set correctly for the currently logged in user. --- .../Configuration/UmbracoSettings.cs | 27 +++ .../Security/AuthenticationExtensions.cs | 199 ++++++++++++++++++ .../Security/UmbracoBackOfficeIdentity.cs | 110 ++++++++++ src/Umbraco.Core/Security/UserData.cs | 51 +++++ src/Umbraco.Core/Umbraco.Core.csproj | 3 + src/Umbraco.Core/UriExtensions.cs | 4 + .../config/metablogConfig.config | 4 +- .../umbraco/controls/passwordChanger.ascx | 44 ++-- src/Umbraco.Web/Security/WebSecurity.cs | 73 +++---- .../UI/Pages/UmbracoEnsuredPage.cs | 8 - src/Umbraco.Web/UmbracoModule.cs | 60 +++++- .../umbraco/users/EditUser.aspx.cs | 28 +-- .../BasePages/BasePage.cs | 73 ++++--- .../BasePages/UmbracoEnsuredPage.cs | 3 - 14 files changed, 564 insertions(+), 123 deletions(-) create mode 100644 src/Umbraco.Core/Security/AuthenticationExtensions.cs create mode 100644 src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs create mode 100644 src/Umbraco.Core/Security/UserData.cs diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings.cs b/src/Umbraco.Core/Configuration/UmbracoSettings.cs index 4ed5027539..9292fd21e9 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Web; using System.Web.Caching; +using System.Web.Security; using System.Xml; using System.Configuration; @@ -299,6 +300,32 @@ namespace Umbraco.Core.Configuration } } + internal static string AuthCookieName + { + get + { + var value = GetKey("/settings/security/authCookieName"); + if (string.IsNullOrEmpty(value) == false) + { + return value; + } + return "UMB_UCONTEXT"; + } + } + + internal static string AuthCookieDomain + { + get + { + var value = GetKey("/settings/security/authCookieDomain"); + if (string.IsNullOrEmpty(value) == false) + { + return value; + } + return FormsAuthentication.CookieDomain; + } + } + /// /// Enables the experimental canvas (live) editing on the frontend of the website /// diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs new file mode 100644 index 0000000000..87d09aecf5 --- /dev/null +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -0,0 +1,199 @@ +using System; +using System.Web; +using System.Web.Security; +using Newtonsoft.Json; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.Security +{ + internal static class FormsAuthenticationTicketExtensions + { + public static UmbracoBackOfficeIdentity CreateUmbracoIdentity(this FormsAuthenticationTicket ticket) + { + try + { + //create the Umbraco user identity + return new UmbracoBackOfficeIdentity(ticket); + } + catch (Exception ex) + { + //This might occur if we cannot decrypt the value in which case we'll just ignore it, it will + // be handled by the base pages + LogHelper.Error(typeof(FormsAuthenticationTicketExtensions), "An error occurred decrypting the user's ticket", ex); + return null; + } + } + } + + /// + /// Extensions to create and renew and remove authentication tickets for the Umbraco back office + /// + internal static class AuthenticationExtensions + { + //public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http) + //{ + // return http.User.Identity as UmbracoBackOfficeIdentity; + //} + + //internal static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContext http) + //{ + // return new HttpContextWrapper(http).GetCurrentIdentity(); + //} + + /// + /// This clears the authentication cookie + /// + public static void UmbracoLogout(this HttpContextBase http) + { + Logout(http, UmbracoSettings.AuthCookieName); + } + + internal static void UmbracoLogout(this HttpContext http) + { + new HttpContextWrapper(http).UmbracoLogout(); + } + + /// + /// Creates the umbraco authentication ticket + /// + /// + /// + public static void CreateUmbracoAuthTicket(this HttpContextBase http, UserData userdata) + { + CreateAuthTicket( + http, + userdata, + //This is one full day... this is how Umbraco has always created this cookie, it is setup to always + //expire one day from issue and it never gets updated. + 1440, + "/", + UmbracoSettings.AuthCookieName, + UmbracoSettings.AuthCookieDomain); + } + + internal static void CreateUmbracoAuthTicket(this HttpContext http, UserData userdata) + { + new HttpContextWrapper(http).CreateUmbracoAuthTicket(userdata); + } + + /// + /// Gets the umbraco auth ticket + /// + /// + /// + public static FormsAuthenticationTicket GetUmbracoAuthTicket(this HttpContextBase http) + { + return GetAuthTicket(http, UmbracoSettings.AuthCookieName); + } + + internal static FormsAuthenticationTicket GetUmbracoAuthTicket(this HttpContext http) + { + return new HttpContextWrapper(http).GetUmbracoAuthTicket(); + } + + /// + /// This clears the authentication cookie + /// + /// + /// + private static void Logout(this HttpContextBase http, string cookieName) + { + //remove from the request + http.Request.Cookies.Remove(cookieName); + + //expire from the response + var formsCookie = http.Response.Cookies[cookieName]; + if (formsCookie != null) + { + //this will expire immediately and be removed from the browser + formsCookie.Expires = DateTime.Now.AddYears(-1); + } + else + { + //ensure there's def an expired cookie + http.Response.Cookies.Add(new HttpCookie(cookieName) { Expires = DateTime.Now.AddYears(-1) }); + } + } + + /// + /// In v6 this is a custom cookie, in v7 this is a real formsauth cookie. + /// + /// + /// + /// + private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) + { + var formsCookie = http.Request.Cookies[cookieName]; + if (formsCookie == null) + { + return null; + } + + try + { + //get the cookie value + var cookieVal = formsCookie.Value.DecryptWithMachineKey(); + + //here we need to see if the cookie val can be serialized into UserData, if not it means it's probably an old cookie + var deserialized = JsonConvert.DeserializeObject(cookieVal); + + //in v6, we're not using real FormsAuth but our own custom cookie and then we just return a custom FormsAuth ticket + // for this request. + return new FormsAuthenticationTicket( + 4, + deserialized.RealName, + DateTime.Now, + DateTime.Now.AddMinutes(GlobalSettings.TimeOutInMinutes), + false, + cookieVal, + "/"); + + } + catch (Exception) + { + //occurs when decryption fails + http.Logout(cookieName); + return null; + } + } + + /// + /// Creates a custom umbraco auth cookie with the data specified + /// + /// The HTTP. + /// The user data. + /// The minutes persisted. + /// The cookie path. + /// Name of the cookie. + /// The cookie domain. + private static void CreateAuthTicket(this HttpContextBase http, + UserData userData, + int minutesPersisted, + string cookiePath, + string cookieName, + string cookieDomain) + { + var cookie = new HttpCookie(cookieName); + + if (GlobalSettings.UseSSL) + cookie.Secure = true; + + //ensure http only, this should only be able to be accessed via the server + cookie.HttpOnly = true; + cookie.Path = cookiePath; + cookie.Domain = cookieDomain; + cookie.Expires = DateTime.Now.AddMinutes(minutesPersisted); + + //serialize the user data + var json = JsonConvert.SerializeObject(userData); + //encrypt it + var encTicket = json.EncryptWithMachineKey(); + + //set the cookie value + cookie.Value = encTicket; + + http.Response.Cookies.Set(cookie); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs new file mode 100644 index 0000000000..bd48b12116 --- /dev/null +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -0,0 +1,110 @@ +using System; +using System.Web; +using System.Web.Security; +using Newtonsoft.Json; + +namespace Umbraco.Core.Security +{ + /// + /// A custom user identity for the Umbraco backoffice + /// + /// + /// All values are lazy loaded for performance reasons as the constructor is called for every single request + /// + internal class UmbracoBackOfficeIdentity : FormsIdentity + { + public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket) + : base(ticket) + { + UserData = ticket.UserData; + EnsureDeserialized(); + } + + protected readonly string UserData; + internal UserData DeserializedData; + + public string UserContextId + { + get { return DeserializedData.UserContextId; } + } + + public int StartContentNode + { + get { return DeserializedData.StartContentNode; } + } + + public int StartMediaNode + { + get { return DeserializedData.StartMediaNode; } + } + + public string[] AllowedApplications + { + get { return DeserializedData.AllowedApplications; } + } + + public object Id + { + get { return DeserializedData.Id; } + } + + public string RealName + { + get { return DeserializedData.RealName; } + } + + public string Culture + { + get { return DeserializedData.Culture; } + } + + //public int SessionTimeout + //{ + // get + // { + // EnsureDeserialized(); + // return DeserializedData.SessionTimeout; + // } + //} + + public string[] Roles + { + get { return DeserializedData.Roles; } + } + + /// + /// This will ensure we only deserialize once + /// + /// + /// For performance reasons, we'll also check if there's an http context available, + /// if so, we'll chuck our instance in there so that we only deserialize once per request. + /// + protected void EnsureDeserialized() + { + if (DeserializedData != null) + return; + + if (HttpContext.Current != null) + { + //check if we've already done this in this request + var data = HttpContext.Current.Items[typeof(UmbracoBackOfficeIdentity)] as UserData; + if (data != null) + { + DeserializedData = data; + return; + } + } + + if (string.IsNullOrEmpty(UserData)) + { + throw new NullReferenceException("The " + typeof(UserData) + " found in the ticket cannot be empty"); + } + DeserializedData = JsonConvert.DeserializeObject(UserData); + + if (HttpContext.Current != null) + { + HttpContext.Current.Items[typeof (UmbracoBackOfficeIdentity)] = DeserializedData; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs new file mode 100644 index 0000000000..feee9c9dfa --- /dev/null +++ b/src/Umbraco.Core/Security/UserData.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Security +{ + /// + /// Data structure used to store information in the authentication cookie + /// + [DataContract(Name = "userData", Namespace = "")] + internal class UserData + { + public UserData() + { + AllowedApplications = new string[] {}; + Roles = new string[] {}; + } + + ///// + ///// When their session is going to expire (in ticks) + ///// + //[DataMember(Name = "timeout")] + //public long Timeout { get; set; } + + [DataMember(Name = "userContextId")] + public string UserContextId { get; set; } + + [DataMember(Name = "id")] + public object Id { get; set; } + + [DataMember(Name = "roles")] + public string[] Roles { get; set; } + + [DataMember(Name = "username")] + public string Username { get; set; } + + [DataMember(Name = "name")] + public string RealName { get; set; } + + [DataMember(Name = "startContent")] + public int StartContentNode { get; set; } + + [DataMember(Name = "startMedia")] + public int StartMediaNode { get; set; } + + [DataMember(Name = "allowedApps")] + public string[] AllowedApplications { get; set; } + + [DataMember(Name = "culture")] + public string Culture { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b1b83ace68..f4011e90a8 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -666,6 +666,9 @@ + + + diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index e5de22456b..6ecb8d67e8 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -12,6 +12,7 @@ namespace Umbraco.Core /// public static class UriExtensions { + /// /// Checks if the current uri is a back office request /// @@ -19,11 +20,14 @@ namespace Umbraco.Core /// internal static bool IsBackOfficeRequest(this Uri url) { + var authority = url.GetLeftPart(UriPartial.Authority); var afterAuthority = url.GetLeftPart(UriPartial.Query) .TrimStart(authority) .TrimStart("/"); + + //check if this is in the umbraco back office return afterAuthority.InvariantStartsWith(GlobalSettings.Path.TrimStart("/")); } diff --git a/src/Umbraco.Web.UI/config/metablogConfig.config b/src/Umbraco.Web.UI/config/metablogConfig.config index 83cc2c1aa1..5621dbee75 100644 --- a/src/Umbraco.Web.UI/config/metablogConfig.config +++ b/src/Umbraco.Web.UI/config/metablogConfig.config @@ -5,7 +5,7 @@ 0 1080 False - Home + Base @@ -14,6 +14,6 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx b/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx index fccd948dec..b9da0cbac7 100644 --- a/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx +++ b/src/Umbraco.Web.UI/umbraco/controls/passwordChanger.ascx @@ -1,16 +1,34 @@ <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="passwordChanger.ascx.cs" Inherits="umbraco.controls.passwordChanger" %> -Change password
+ - \ No newline at end of file +Change password
+ + diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 08f5b0b67a..f45ff9c30e 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -5,12 +5,15 @@ using System.Web; using System.Web.Security; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Security; +using umbraco; using umbraco.BusinessLogic; using umbraco.DataLayer; using umbraco.businesslogic.Exceptions; using umbraco.cms.businesslogic.member; +using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; +using UmbracoSettings = Umbraco.Core.Configuration.UmbracoSettings; namespace Umbraco.Web.Security { @@ -369,53 +372,51 @@ namespace Umbraco.Web.Security ///
/// The umbraco user context ID. public string UmbracoUserContextId - { + { get { - if (StateHelper.Cookies.HasCookies && StateHelper.Cookies.UserContext.HasValue) + var authTicket = HttpContext.Current.GetUmbracoAuthTicket(); + if (authTicket == null) { - try - { - var encTicket = StateHelper.Cookies.UserContext.GetValue(); - if (string.IsNullOrEmpty(encTicket) == false) - { - return encTicket.DecryptWithMachineKey(); - } - } - catch (Exception ex) - { - if (ex is ArgumentException || ex is FormatException || ex is HttpException) - { - StateHelper.Cookies.UserContext.Clear(); - } - else - { - throw; - } - } + return ""; } - return ""; + var identity = authTicket.CreateUmbracoIdentity(); + if (identity == null) + { + HttpContext.Current.UmbracoLogout(); + return ""; + } + return identity.UserContextId; } set { - // zb-00004 #29956 : refactor cookies names & handling - if (StateHelper.Cookies.HasCookies) + if (value.IsNullOrWhiteSpace()) { - // Clearing all old cookies before setting a new one. - if (StateHelper.Cookies.UserContext.HasValue) - StateHelper.Cookies.ClearAll(); - - if (string.IsNullOrEmpty(value) == false) + HttpContext.Current.UmbracoLogout(); + } + else + { + var uid = GetUserId(value); + if (uid == -1) { - // Encrypt the value - var encTicket = value.EncryptWithMachineKey(); - - // Create new cookie. - StateHelper.Cookies.UserContext.SetValue(encTicket, 1); + HttpContext.Current.UmbracoLogout(); } else { - StateHelper.Cookies.UserContext.Clear(); + var user = User.GetUser(uid); + HttpContext.Current.CreateUmbracoAuthTicket( + new UserData + { + Id = uid, + AllowedApplications = user.Applications.Select(x => x.alias).ToArray(), + Culture = ui.Culture(user), + RealName = user.Name, + Roles = new string[] { user.UserType.Alias }, + StartContentNode = user.StartNodeId, + StartMediaNode = user.StartMediaId, + UserContextId = value, + Username = user.LoginName + }); } } } diff --git a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs index 3979415dc7..be55343696 100644 --- a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs +++ b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs @@ -51,14 +51,6 @@ namespace Umbraco.Web.UI.Pages Response.Redirect(SystemDirectories.Umbraco + "/logout.aspx?redir=" + Server.UrlEncode(Request.RawUrl), true); } } - - protected override void OnInit(EventArgs e) - { - base.OnInit(e); - - System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(ui.Culture(Security.CurrentUser)); - System.Threading.Thread.CurrentThread.CurrentUICulture = System.Threading.Thread.CurrentThread.CurrentCulture; - } /// /// Gets/sets the app that this page is assigned to diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 604e61e55f..4ef5bc99a7 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -2,13 +2,17 @@ using System.Collections; using System.IO; using System.Linq; +using System.Threading; using System.Web; using System.Web.Routing; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Security; using Umbraco.Web.Routing; using umbraco; +using umbraco.BasePages; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using UmbracoSettings = Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Web.Configuration; @@ -29,7 +33,7 @@ namespace Umbraco.Web /// Begins to process a request. /// /// - void BeginRequest(HttpContextBase httpContext) + static void BeginRequest(HttpContextBase httpContext) { //we need to set the initial url in our ApplicationContext, this is so our keep alive service works and this must //exist on a global context because the keep alive service doesn't run in a web context. @@ -119,6 +123,58 @@ namespace Umbraco.Web RewriteToUmbracoHandler(httpContext, pcr); } + /// + /// Checks if the request is authenticated, if it is it sets the thread culture to the currently logged in user + /// + /// + /// + static void AuthenticateRequest(object sender, EventArgs e) + { + var app = (HttpApplication)sender; + var http = new HttpContextWrapper(app.Context); + + // do not process if client-side request + if (http.Request.Url.IsClientSideRequest()) + return; + + if (app.Request.Url.IsBackOfficeRequest() || app.Request.Url.IsInstallerRequest()) + { + var ticket = http.GetUmbracoAuthTicket(); + if (ticket != null) + { + //create the Umbraco user identity + var identity = ticket.CreateUmbracoIdentity(); + if (identity != null) + { + + //We'll leave setting custom identies/principals for 6.2, for now we'll just ensure that the cultures, etc.. are set + ////set the principal object + ////now we need to see if their session is still valid + //var timeout = BasePage.GetTimeout(identity.UserContextId); + //if (timeout > DateTime.Now.Ticks) + //{ + //var principal = new GenericPrincipal(identity, identity.Roles); + ////It is actually not good enough to set this on the current app Context and the thread, it also needs + //// to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually + //// an underlying fault of asp.net not propogating the User correctly. + //if (HttpContext.Current != null) + //{ + // HttpContext.Current.User = principal; + //} + //app.Context.User = principal; + //Thread.CurrentPrincipal = principal; + //} + + //This is a back office/installer request, we will also set the culture/ui culture + Thread.CurrentThread.CurrentCulture = + Thread.CurrentThread.CurrentUICulture = + new System.Globalization.CultureInfo(identity.Culture); + + } + } + } + } + // returns a value indicating whether redirection took place and the request has // been completed - because we don't want to Response.End() here to terminate // everything properly. @@ -433,6 +489,8 @@ namespace Umbraco.Web BeginRequest(new HttpContextWrapper(httpContext)); }; + app.AuthenticateRequest += AuthenticateRequest; + app.PostResolveRequestCache += (sender, e) => { var httpContext = ((HttpApplication)sender).Context; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index 55303bc219..1569a2d110 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -348,16 +348,12 @@ namespace umbraco.cms.presentation.user } } } - - #region Web Form Designer generated code - + protected override void OnInit(EventArgs e) { - // - // CODEGEN: This call is required by the ASP.NET Web Form Designer. - // - InitializeComponent(); - base.OnInit(e); + //lapps.SelectionMode = ListSelectionMode.Multiple; + lapps.RepeatLayout = RepeatLayout.Flow; + lapps.RepeatDirection = RepeatDirection.Vertical; } protected override void OnPreRender(EventArgs e) @@ -366,23 +362,9 @@ namespace umbraco.cms.presentation.user ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/CMSNode.asmx")); // ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/legacyAjaxCalls.asmx")); - - + } - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - //lapps.SelectionMode = ListSelectionMode.Multiple; - lapps.RepeatLayout = RepeatLayout.Flow; - lapps.RepeatDirection = RepeatDirection.Vertical; - } - - #endregion - /// /// Handles the Click event of the saveUser control. /// diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs index 99c1172b1c..c2aec995ce 100644 --- a/src/umbraco.businesslogic/BasePages/BasePage.cs +++ b/src/umbraco.businesslogic/BasePages/BasePage.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Services; using umbraco.BusinessLogic; using umbraco.DataLayer; using Umbraco.Core; +using Umbraco.Core.Security; namespace umbraco.BasePages { @@ -223,10 +224,10 @@ namespace umbraco.BasePages return false; } - private static long GetTimeout(string umbracoUserContextID) + internal static long GetTimeout(string umbracoUserContextId) { return ApplicationContext.Current.ApplicationCache.GetCacheItem( - CacheKeys.UserContextTimeoutCacheKey + umbracoUserContextID, + CacheKeys.UserContextTimeoutCacheKey + umbracoUserContextId, new TimeSpan(0, UmbracoTimeOutInMinutes / 10, 0), () => GetTimeout(true)); } @@ -257,50 +258,48 @@ namespace umbraco.BasePages { get { - if (StateHelper.Cookies.HasCookies && StateHelper.Cookies.UserContext.HasValue) + var authTicket = HttpContext.Current.GetUmbracoAuthTicket(); + if (authTicket == null) { - try - { - var encTicket = StateHelper.Cookies.UserContext.GetValue(); - if (string.IsNullOrEmpty(encTicket) == false) - { - return encTicket.DecryptWithMachineKey(); - } - } - catch (Exception ex) - { - if (ex is ArgumentException || ex is FormatException || ex is HttpException) - { - StateHelper.Cookies.UserContext.Clear(); - } - else - { - throw; - } - } + return ""; } - return ""; + var identity = authTicket.CreateUmbracoIdentity(); + if (identity == null) + { + HttpContext.Current.UmbracoLogout(); + return ""; + } + return identity.UserContextId; } set { - // zb-00004 #29956 : refactor cookies names & handling - if (StateHelper.Cookies.HasCookies) + if (value.IsNullOrWhiteSpace()) { - // Clearing all old cookies before setting a new one. - if (StateHelper.Cookies.UserContext.HasValue) - StateHelper.Cookies.ClearAll(); - - if (string.IsNullOrEmpty(value) == false) - { - // Encrypt the value - var encTicket = value.EncryptWithMachineKey(); - - // Create new cookie. - StateHelper.Cookies.UserContext.SetValue(encTicket, 1); + HttpContext.Current.UmbracoLogout(); + } + else + { + var uid = GetUserId(value); + if (uid == -1) + { + HttpContext.Current.UmbracoLogout(); } else { - StateHelper.Cookies.UserContext.Clear(); + var user = BusinessLogic.User.GetUser(uid); + HttpContext.Current.CreateUmbracoAuthTicket( + new UserData + { + Id = uid, + AllowedApplications = user.Applications.Select(x => x.alias).ToArray(), + Culture = ui.Culture(user), + RealName = user.Name, + Roles = new string[] {user.UserType.Alias}, + StartContentNode = user.StartNodeId, + StartMediaNode = user.StartMediaId, + UserContextId = value, + Username = user.LoginName + }); } } } diff --git a/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs b/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs index 870b1797b2..4fa4a3fea3 100644 --- a/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs +++ b/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs @@ -108,9 +108,6 @@ namespace umbraco.BasePages else Response.Redirect(SystemDirectories.Umbraco + "/logout.aspx?redir=" + Server.UrlEncode(Request.RawUrl), true); } - - System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(ui.Culture(this.getUser())); - System.Threading.Thread.CurrentThread.CurrentUICulture = System.Threading.Thread.CurrentThread.CurrentCulture; } } } \ No newline at end of file From 7e9cad34db0a1e745e7fe50e25bc2c191fb3418e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 2 Aug 2013 16:01:54 +1000 Subject: [PATCH 05/21] Creates an SQL Server version check for bulk sql imports. --- src/Umbraco.Core/DatabaseContext.cs | 49 +++++++++++++++++++ .../Persistence/PetaPocoExtensions.cs | 5 +- .../SqlSyntax/SqlServerSyntaxProvider.cs | 19 +++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index 1311593e90..ed9b7c5a9d 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -303,6 +303,8 @@ namespace Umbraco.Core connString = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName].ConnectionString; } Initialize(providerName, connString); + + DetermineSqlServerVersion(); } else if (ConfigurationManager.AppSettings.ContainsKey(GlobalSettings.UmbracoConnectionName) && string.IsNullOrEmpty(ConfigurationManager.AppSettings[GlobalSettings.UmbracoConnectionName]) == false) { @@ -339,6 +341,8 @@ namespace Umbraco.Core //Remove the legacy connection string, so we don't end up in a loop if something goes wrong. GlobalSettings.RemoveSetting(GlobalSettings.UmbracoConnectionName); + + DetermineSqlServerVersion(); } else { @@ -372,6 +376,49 @@ namespace Umbraco.Core Initialize(providerName); } + /// + /// Set the lazy resolution of determining the SQL server version if that is the db type we're using + /// + private void DetermineSqlServerVersion() + { + + var sqlServerSyntax = SqlSyntaxContext.SqlSyntaxProvider as SqlServerSyntaxProvider; + if (sqlServerSyntax != null) + { + //this will not execute now, it is lazy so will only execute when we need to actually know + // the sql server version. + sqlServerSyntax.VersionName = new Lazy(() => + { + try + { + var database = this._factory.CreateDatabase(); + + var version = database.ExecuteScalar("SELECT SERVERPROPERTY('productversion')"); + var firstPart = version.Split('.')[0]; + switch (firstPart) + { + case "11": + return SqlServerVersionName.V2012; + case "10": + return SqlServerVersionName.V2008; + case "9": + return SqlServerVersionName.V2005; + case "8": + return SqlServerVersionName.V2000; + case "7": + return SqlServerVersionName.V7; + default: + return SqlServerVersionName.Other; + } + } + catch (Exception) + { + return SqlServerVersionName.Invalid; + } + }); + } + } + internal DatabaseSchemaResult ValidateDatabaseSchema() { if (_configured == false || (string.IsNullOrEmpty(_connectionString) || string.IsNullOrEmpty(ProviderName))) @@ -463,6 +510,8 @@ namespace Umbraco.Core message = message + "

Upgrade completed!

"; } + //now that everything is done, we need to determine the version of SQL server that is executing + LogHelper.Info("Database configuration status: " + message); return new Result { Message = message, Success = true, Percentage = "100" }; diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index c962224689..eb10bf7f87 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -51,7 +51,10 @@ namespace Umbraco.Core.Persistence try { - if (SqlSyntaxContext.SqlSyntaxProvider is SqlCeSyntaxProvider) + //if it is sql ce or it is a sql server version less than 2008, we need to do individual inserts. + var sqlServerSyntax = SqlSyntaxContext.SqlSyntaxProvider as SqlServerSyntaxProvider; + if ((sqlServerSyntax != null && (int)sqlServerSyntax.VersionName.Value < (int)SqlServerVersionName.V2008) + || SqlSyntaxContext.SqlSyntaxProvider is SqlCeSyntaxProvider) { //SqlCe doesn't support bulk insert statements! diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 2de2209ab4..3e37674ddf 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -13,6 +13,20 @@ namespace Umbraco.Core.Persistence.SqlSyntax public static ISqlSyntaxProvider Provider { get { return new SqlServerSyntaxProvider(); } } } + /// + /// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) + /// + internal enum SqlServerVersionName + { + Invalid = -1, + V7 = 0, + V2000 = 1, + V2005 = 2, + V2008 = 3, + V2012 = 4, + Other = 5 + } + /// /// Represents an SqlSyntaxProvider for Sql Server /// @@ -36,6 +50,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax InitColumnTypeMap(); } + /// + /// Gets/sets the version of the current SQL server instance + /// + internal Lazy VersionName { get; set; } + public override string GetQuotedTableName(string tableName) { return string.Format("[{0}]", tableName); From b7a11233192100d18cb26cf43b9e5b5bddb7f99e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 2 Aug 2013 16:23:32 +1000 Subject: [PATCH 06/21] U4-2579 BulkPublishController causes YSOD when there are invalid child elements due to InvalidProperties having a null reference --- src/Umbraco.Core/Publishing/PublishStatus.cs | 20 +++++++++++++------- src/Umbraco.Core/Services/ContentService.cs | 7 ++++++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Publishing/PublishStatus.cs b/src/Umbraco.Core/Publishing/PublishStatus.cs index 415b758965..aee9a1fafe 100644 --- a/src/Umbraco.Core/Publishing/PublishStatus.cs +++ b/src/Umbraco.Core/Publishing/PublishStatus.cs @@ -8,15 +8,14 @@ namespace Umbraco.Core.Publishing ///
internal class PublishStatus { - public IContent ContentItem { get; private set; } - public PublishStatusType StatusType { get; internal set; } - - /// - /// Gets sets the invalid properties if the status failed due to validation. - /// - public IEnumerable InvalidProperties { get; set; } + public PublishStatus() + { + //initialize + InvalidProperties = new List(); + } public PublishStatus(IContent content, PublishStatusType statusType) + : this() { ContentItem = content; StatusType = statusType; @@ -29,6 +28,13 @@ namespace Umbraco.Core.Publishing : this(content, PublishStatusType.Success) { } + + public IContent ContentItem { get; private set; } + public PublishStatusType StatusType { get; internal set; } + /// + /// Gets sets the invalid properties if the status failed due to validation. + /// + public IEnumerable InvalidProperties { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 3dbc03fab2..c4af37c34a 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1460,7 +1460,12 @@ namespace Umbraco.Core.Services LogHelper.Info( string.Format("Content '{0}' with Id '{1}' could not be published because of invalid properties.", content.Name, content.Id)); - result.Add(new Attempt(false, new PublishStatus(content, PublishStatusType.FailedContentInvalid))); + result.Add( + new Attempt(false, + new PublishStatus(content, PublishStatusType.FailedContentInvalid) + { + InvalidProperties = ((ContentBase) content).LastInvalidProperties + })); return result; } From d78a03a6100fee7ce0bcaddf84b98235b28feb3b Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 5 Aug 2013 13:11:47 +1000 Subject: [PATCH 07/21] Massively improves performance for the packaging service (converting docs to XML) which is used in republishing the tree. --- .gitignore | 2 + src/Umbraco.Core/Models/PropertyExtensions.cs | 21 +++- .../Models/PropertyTypeExtensions.cs | 8 +- .../Models/DataValueSetterTests.cs | 102 ++++++++++++++++++ .../Services/PackagingServiceTests.cs | 41 +++++++ .../TestHelpers/BaseDatabaseFactoryTest.cs | 11 +- .../TestHelpers/BaseUmbracoApplicationTest.cs | 4 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../businesslogic/datatype/DefaultData.cs | 25 ++++- src/umbraco.interfaces/IData.cs | 9 ++ .../Properties/AssemblyInfo.cs | 9 +- src/umbraco.sln | 8 ++ 12 files changed, 229 insertions(+), 13 deletions(-) create mode 100644 src/Umbraco.Tests/Models/DataValueSetterTests.cs create mode 100644 src/Umbraco.Tests/Services/PackagingServiceTests.cs diff --git a/.gitignore b/.gitignore index 67bcafd9ca..9496a86e70 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,5 @@ src/Umbraco.Web.UI.Client/[Bb]uild/[Bb]elle/ src/Umbraco.Web.UI/[Uu]ser[Cc]ontrols/ build/_BuildOutput/ tools/NDepend/ +src/*.vspx +src/*.psess diff --git a/src/Umbraco.Core/Models/PropertyExtensions.cs b/src/Umbraco.Core/Models/PropertyExtensions.cs index b03f078a9f..ae01532c87 100644 --- a/src/Umbraco.Core/Models/PropertyExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyExtensions.cs @@ -22,10 +22,15 @@ namespace Umbraco.Core.Models /// Xml of the property and its value public static XElement ToXml(this Property property) { - string nodeName = UmbracoSettings.UseLegacyXmlSchema ? "data" : property.Alias.ToSafeAlias(); + return property.ToXml(ApplicationContext.Current.Services.DataTypeService); + } + + internal static XElement ToXml(this Property property, IDataTypeService dataTypeService) + { + var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "data" : property.Alias.ToSafeAlias(); var xd = new XmlDocument(); - XmlNode xmlNode = xd.CreateNode(XmlNodeType.Element, nodeName, ""); + var xmlNode = xd.CreateNode(XmlNodeType.Element, nodeName, ""); //Add the property alias to the legacy schema if (UmbracoSettings.UseLegacyXmlSchema) @@ -37,9 +42,17 @@ namespace Umbraco.Core.Models //This seems to fail during testing //SD: With the new null checks below, this shouldn't fail anymore. - var dt = property.PropertyType.DataType(property.Id); + var dt = property.PropertyType.DataType(property.Id, dataTypeService); if (dt != null && dt.Data != null) - { + { + //We've already got the value for the property so we're going to give it to the + // data type's data property so it doesn't go re-look up the value from the db again. + var defaultData = dt.Data as IDataValueSetter; + if (defaultData != null) + { + defaultData.SetValue(property.Value, property.PropertyType.DataTypeDatabaseType.ToString()); + } + xmlNode.AppendChild(dt.Data.ToXMl(xd)); } diff --git a/src/Umbraco.Core/Models/PropertyTypeExtensions.cs b/src/Umbraco.Core/Models/PropertyTypeExtensions.cs index b8ee04df33..304d7e7b9d 100644 --- a/src/Umbraco.Core/Models/PropertyTypeExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTypeExtensions.cs @@ -1,4 +1,5 @@ -using umbraco.interfaces; +using Umbraco.Core.Services; +using umbraco.interfaces; namespace Umbraco.Core.Models { @@ -9,6 +10,7 @@ namespace Umbraco.Core.Models /// /// PropertyType that references a DataType /// Id of the Property which references this DataType through its PropertyType + /// /// /// /// This extension method is left internal because we don't want to take @@ -16,10 +18,10 @@ namespace Umbraco.Core.Models /// be replaced by PropertyEditors. It is however needed to generate xml /// for a property/propertytype when publishing. /// - internal static IDataType DataType(this PropertyType propertyType, int propertyId) + internal static IDataType DataType(this PropertyType propertyType, int propertyId, IDataTypeService dataTypeService) { Mandate.ParameterNotNull(propertyType, "propertyType"); - var dataType = ApplicationContext.Current.Services.DataTypeService.GetDataTypeById(propertyType.DataTypeId); + var dataType = dataTypeService.GetDataTypeById(propertyType.DataTypeId); if (dataType == null) { return null; diff --git a/src/Umbraco.Tests/Models/DataValueSetterTests.cs b/src/Umbraco.Tests/Models/DataValueSetterTests.cs new file mode 100644 index 0000000000..db59217166 --- /dev/null +++ b/src/Umbraco.Tests/Models/DataValueSetterTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.Xml; +using NUnit.Framework; +using Rhino.Mocks; +using Rhino.Mocks.Interfaces; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Tests.TestHelpers; +using umbraco.cms.businesslogic.datatype; +using umbraco.interfaces; + +namespace Umbraco.Tests.Models +{ + [TestFixture] + public class DataValueSetterTests : BaseUmbracoApplicationTest + { + protected override void FreezeResolution() + { + ShortStringHelperResolver.Current = new ShortStringHelperResolver(new DefaultShortStringHelper()); + base.FreezeResolution(); + } + + [Test] + public void LoadValueFromDatabase_Is_Not_Called_When_SetValue_Is_Used() + { + // Arrange + var baseDataType = MockRepository.GenerateStub(); + var dataTypeData = MockRepository.GenerateMock(baseDataType); + dataTypeData.Stub(x => x.Value).CallOriginalMethod(OriginalCallOptions.NoExpectation); + + // Act + + ((IDataValueSetter)dataTypeData).SetValue("Hello world", DataTypeDatabaseType.Nvarchar.ToString()); + var val = dataTypeData.Value; + + // Assert + + dataTypeData.AssertWasNotCalled(data => data.LoadValueFromDatabase()); + } + + [Test] + public void LoadValueFromDatabase_Is_Called_When_SetValue_Is_Not_Used() + { + // Arrange + var baseDataType = MockRepository.GenerateStub(); + var dataTypeData = MockRepository.GenerateMock(baseDataType); + dataTypeData + .Stub(data => data.LoadValueFromDatabase()).WhenCalled(invocation => Debug.WriteLine("asdf")); + dataTypeData.Stub(x => x.Value).CallOriginalMethod(OriginalCallOptions.NoExpectation); + + // Act + + var val = dataTypeData.Value; + + // Assert + + dataTypeData.AssertWasCalled(data => data.LoadValueFromDatabase()); + } + + [Test] + public void SetValue_Is_Called_When_Executing_ToXml_On_A_Property_With_DataType_That_Implements_IDataValueSetter() + { + // Arrange + var dataTypeId = Guid.NewGuid(); + + var dataTypeData = MockRepository.GenerateMock(); + dataTypeData + .Stub(data => data.ToXMl(Arg.Is.Anything)) + .Return(null) // you have to call Return() even though we're about to override it + .WhenCalled(invocation => + { + var xmlDoc = (XmlDocument) invocation.Arguments[0]; + invocation.ReturnValue = xmlDoc.CreateElement("test"); + }); + + var dataType = MockRepository.GenerateStub(); + dataType.Stub(type => type.Data).Return(dataTypeData); + + var dataTypeSvc = MockRepository.GenerateStub(); + dataTypeSvc.Stub(service => service.GetDataTypeById(dataTypeId)).Return(dataType); + + var property = new Property( + 1234, + Guid.NewGuid(), + new PropertyType(dataTypeId, DataTypeDatabaseType.Nvarchar) + { + Alias = "test" + }, "Hello world"); + + // Act + + var xml = property.ToXml(dataTypeSvc); + + // Assert + + ((IDataValueSetter)dataTypeData).AssertWasCalled(setter => setter.SetValue("Hello world", DataTypeDatabaseType.Nvarchar.ToString())); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/PackagingServiceTests.cs b/src/Umbraco.Tests/Services/PackagingServiceTests.cs new file mode 100644 index 0000000000..abdfb2f9f3 --- /dev/null +++ b/src/Umbraco.Tests/Services/PackagingServiceTests.cs @@ -0,0 +1,41 @@ +using System; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Tests.TestHelpers.Entities; + +namespace Umbraco.Tests.Services +{ + //[TestFixture] + //public class PackagingServiceTests : BaseServiceTest + //{ + // [Test] + // public void Export_Content() + // { + // var yesNo = DataTypesResolver.Current.GetById(new Guid(Constants.PropertyEditors.TrueFalse)); + // var txtField = DataTypesResolver.Current.GetById(new Guid(Constants.PropertyEditors.Textbox)); + + // var contentWithDataType = MockedContentTypes.CreateSimpleContentType( + // "test", + // "Test", + // new PropertyTypeCollection( + // new PropertyType[] + // { + // new PropertyType(new DataTypeDefinition(-1, txtField.Id) + // { + // Name = "Testing Textfield", DatabaseType = DataTypeDatabaseType.Ntext + // }), + // new PropertyType(new DataTypeDefinition(-1, yesNo.Id) + // { + // Name = "Testing intfield", DatabaseType = DataTypeDatabaseType.Integer + // }) + // })); + + // var content = MockedContent.CreateSimpleContent(contentWithDataType); + // content.Name = "Test"; + + // var exported = ServiceContext.PackagingService.Export(content); + + // } + //} +} \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index 5a98e303a4..b1a5b8b925 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -46,6 +46,8 @@ namespace Umbraco.Tests.TestHelpers //Used to flag if its the first test in the current fixture private bool _isFirstTestInFixture = false; + private ApplicationContext _appContext; + [SetUp] public override void Initialize() { @@ -59,7 +61,7 @@ namespace Umbraco.Tests.TestHelpers var dbFactory = new DefaultDatabaseFactory( GetDbConnectionString(), GetDbProviderName()); - ApplicationContext.Current = new ApplicationContext( + _appContext = new ApplicationContext( //assign the db context new DatabaseContext(dbFactory), //assign the service context @@ -79,7 +81,12 @@ namespace Umbraco.Tests.TestHelpers //ensure the configuration matches the current version for tests SettingsForTests.ConfigurationStatus = UmbracoVersion.Current.ToString(3); } - + + protected override void SetupApplicationContext() + { + ApplicationContext.Current = _appContext; + } + /// /// The database behavior to use for the test/fixture /// diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index dbf87b4aee..d898ef6c5f 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -25,6 +25,8 @@ namespace Umbraco.Tests.TestHelpers SetupPluginManager(); + SetupApplicationContext(); + FreezeResolution(); } @@ -40,7 +42,7 @@ namespace Umbraco.Tests.TestHelpers ApplicationContext.Current = null; ResetPluginManager(); } - + /// /// By default this returns false which means the plugin manager will not be reset so it doesn't need to re-scan /// all of the assemblies. Inheritors can override this if plugin manager resetting is required, generally needs diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 66764feae5..747cb52cee 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -185,9 +185,11 @@ + + diff --git a/src/umbraco.cms/businesslogic/datatype/DefaultData.cs b/src/umbraco.cms/businesslogic/datatype/DefaultData.cs index d72212aa5f..a974fda7c2 100644 --- a/src/umbraco.cms/businesslogic/datatype/DefaultData.cs +++ b/src/umbraco.cms/businesslogic/datatype/DefaultData.cs @@ -14,7 +14,7 @@ namespace umbraco.cms.businesslogic.datatype /// /// Default implementation of the IData interface that stores data inside the Umbraco database. /// - public class DefaultData : IData, IDataWithPreview + public class DefaultData : IData, IDataWithPreview, IDataValueSetter { private int _propertyId; private object _value; @@ -57,10 +57,29 @@ namespace umbraco.cms.businesslogic.datatype _value = InitValue; } + /// + /// This is here for performance reasons since in some cases we will have already resolved the value from the db + /// and want to just give this object the value so it doesn't go re-look it up from the database. + /// + /// + /// + void IDataValueSetter.SetValue(object val, string strDbType) + { + _value = val; + //now that we've set our value, we can update our BaseDataType object with the correct values from the db + //instead of making it query for itself. This is a peformance optimization enhancement. + var dbType = BaseDataType.GetDBType(strDbType); + var fieldName = BaseDataType.GetDataFieldName(dbType); + _dataType.SetDataTypeProperties(fieldName, dbType); + + //ensures that it doesn't go back to the db + _valueLoaded = true; + } + /// /// Loads the data value from the database. /// - protected virtual void LoadValueFromDatabase() + protected internal virtual void LoadValueFromDatabase() { var sql = new Sql(); sql.Select("*") @@ -243,5 +262,7 @@ namespace umbraco.cms.businesslogic.datatype } #endregion + + } } diff --git a/src/umbraco.interfaces/IData.cs b/src/umbraco.interfaces/IData.cs index c0580f0fee..17a374ff05 100644 --- a/src/umbraco.interfaces/IData.cs +++ b/src/umbraco.interfaces/IData.cs @@ -3,6 +3,15 @@ using System.Xml; namespace umbraco.interfaces { + /// + /// Internal interface used to decorate any IData that can be optimized when exporting + /// XML like in the packaging service. Instead of relying on the IData to go get the value + /// from the db, any IData that implements this can have it's value set from the packaging service. + /// + internal interface IDataValueSetter + { + void SetValue(object val, string strDbType); + } /// /// The IData is part of the IDataType interface for creating new data types in the umbraco backoffice. diff --git a/src/umbraco.interfaces/Properties/AssemblyInfo.cs b/src/umbraco.interfaces/Properties/AssemblyInfo.cs index 06f4dbdf64..709f231e00 100644 --- a/src/umbraco.interfaces/Properties/AssemblyInfo.cs +++ b/src/umbraco.interfaces/Properties/AssemblyInfo.cs @@ -9,4 +9,11 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("umbraco.interfaces")] [assembly: AssemblyDescription("Core assembly containing legacy interfaces")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("Umbraco CMS")] \ No newline at end of file +[assembly: AssemblyProduct("Umbraco CMS")] + +[assembly: InternalsVisibleTo("cms")] +[assembly: InternalsVisibleTo("Umbraco.Core")] +[assembly: InternalsVisibleTo("Umbraco.Tests")] + +//allow this to be mocked in our unit tests +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/umbraco.sln b/src/umbraco.sln index fee2486039..eba2ad7b9b 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -67,6 +67,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C ..\build\NuSpecs\UmbracoCms.nuspec = ..\build\NuSpecs\UmbracoCms.nuspec EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{99794542-89EC-43BA-88BE-E31A9D61423B}" + ProjectSection(SolutionItems) = preProject + Performance1.psess = Performance1.psess + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -157,4 +162,7 @@ Global {73529637-28F5-419C-A6BB-D094E39DE614} = {DD32977B-EF54-475B-9A1B-B97A502C6E58} {B555AAE6-0F56-442F-AC9F-EF497DB38DE7} = {DD32977B-EF54-475B-9A1B-B97A502C6E58} EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection EndGlobal From 226a13c6e14c0e241a9e8c7bcad0311464046b5d Mon Sep 17 00:00:00 2001 From: Morten Christensen Date: Mon, 5 Aug 2013 14:30:03 +0200 Subject: [PATCH 08/21] Minor adjustment related to U4-2560 Working with ContentService can give 2 versions of a document as "newest" --- .../Repositories/ContentRepository.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index e2c981f62d..921a7e75dd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -338,19 +338,22 @@ namespace Umbraco.Core.Persistence.Repositories } } + //Look up (newest) entries by id in cmsDocument table to set newest = false + //NOTE: This should only be done for all other versions then the current one, so we don't cause the same entry to be updated multiple times. + var documentDtos = + Database.Query( + "WHERE nodeId = @Id AND newest = @IsNewest AND NOT(versionId = @VersionId)", + new {Id = entity.Id, IsNewest = true, VersionId = dto.ContentVersionDto.VersionId}); + foreach (var documentDto in documentDtos) + { + var docDto = documentDto; + docDto.Newest = false; + Database.Update(docDto); + } + var contentVersionDto = dto.ContentVersionDto; if (shouldCreateNewVersion) { - //Look up (newest) entries by id in cmsDocument table to set newest = false - //NOTE: This is only relevant when a new version is created, which is why its done inside this if-statement. - var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); - foreach (var documentDto in documentDtos) - { - var docDto = documentDto; - docDto.Newest = false; - Database.Update(docDto); - } - //Create a new version - cmsContentVersion //Assumes a new Version guid and Version date (modified date) has been set Database.Insert(contentVersionDto); From d47b4517c106f50e8d53e56a7e83daee599577fa Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 6 Aug 2013 13:06:25 +1000 Subject: [PATCH 09/21] Fixes unit tests --- src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index b1a5b8b925..6152898f92 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -52,9 +52,7 @@ namespace Umbraco.Tests.TestHelpers public override void Initialize() { InitializeFirstRunFlags(); - - base.Initialize(); - + var path = TestHelper.CurrentAssemblyDirectory; AppDomain.CurrentDomain.SetData("DataDirectory", path); @@ -72,6 +70,8 @@ namespace Umbraco.Tests.TestHelpers IsReady = true }; + base.Initialize(); + DatabaseContext.Initialize(dbFactory.ProviderName, dbFactory.ConnectionString); CreateSqlCeDatabase(); From b11932240ca1a25a6f405f9afd6c0fe188f713fb Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 6 Aug 2013 10:24:20 +0200 Subject: [PATCH 10/21] Fixes U4-2587 Regression: Hide Preview Button on nodes that have no template Now also checks if the template still exists and if it's allowed on that document to determine when to enable the preview button --- .../umbraco/editContent.aspx.cs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/editContent.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/editContent.aspx.cs index ab8579db27..91c6a8a4e0 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/editContent.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/editContent.aspx.cs @@ -249,7 +249,7 @@ namespace umbraco.cms.presentation //update UI and set document properties PerformSaveLogic(); - + //persist the document _document.Save(); @@ -257,7 +257,7 @@ namespace umbraco.cms.presentation BusinessLogic.Actions.Action.RunActionHandlers(_document, ActionUpdate.Instance); ClientTools.ShowSpeechBubble( - speechBubbleIcon.save, ui.Text("speechBubbles", "editContentSavedHeader", null), + speechBubbleIcon.save, ui.Text("speechBubbles", "editContentSavedHeader", null), ui.Text("speechBubbles", "editContentSavedText", null)); ClientTools.SyncTree(_document.Path, true); @@ -291,7 +291,7 @@ namespace umbraco.cms.presentation { //update UI and set document properties PerformSaveLogic(); - + //the business logic here will check to see if the doc can actually be published and will return the // appropriate result so we can display the correct error messages (or success). var savePublishResult = _document.SaveAndPublishWithResult(UmbracoUser); @@ -309,7 +309,7 @@ namespace umbraco.cms.presentation _documentHasPublishedVersion = _document.Content.HasPublishedVersion(); } - + ClientTools.SyncTree(_document.Path, true); } @@ -320,8 +320,8 @@ namespace umbraco.cms.presentation case PublishStatusType.Success: case PublishStatusType.SuccessAlreadyPublished: ClientTools.ShowSpeechBubble( - speechBubbleIcon.save, - ui.Text("speechBubbles", "editContentPublishedHeader", UmbracoUser), + speechBubbleIcon.save, + ui.Text("speechBubbles", "editContentPublishedHeader", UmbracoUser), ui.Text("speechBubbles", "editContentPublishedText", UmbracoUser)); break; case PublishStatusType.FailedPathNotPublished: @@ -550,23 +550,40 @@ namespace umbraco.cms.presentation } menuItem.ImageURL = SystemDirectories.Umbraco + "/images/editor/vis.gif"; - // Fix for U4-682, if there's no template, disable the preview button - if (_document.Template != -1) + + if (EnablePreviewButton()) { menuItem.AltText = ui.Text("buttons", "showPage", UmbracoUser); menuItem.OnClickCommand = "window.open('dialogs/preview.aspx?id=" + id + "','umbPreview')"; } else { - string showPageDisabledText = ui.Text("buttons", "showPageDisabled", UmbracoUser); + var showPageDisabledText = ui.Text("buttons", "showPageDisabled", UmbracoUser); if (showPageDisabledText.StartsWith("[")) - showPageDisabledText = ui.GetText("buttons", "showPageDisabled", null, "en"); ; + showPageDisabledText = ui.GetText("buttons", "showPageDisabled", null, "en"); menuItem.AltText = showPageDisabledText; - ((Image)menuItem).Attributes.Add("style", "opacity: 0.5"); + ((Image) menuItem).Attributes.Add("style", "opacity: 0.5"); } } + private bool EnablePreviewButton() + { + // Fix for U4-862, if there's no template, disable the preview button + // Fixed again for U4-2587, apparently at some point "no template" changed from -1 to 0? -SJ + // Now also catches when template doesn't exist any more or is not allowed any more + // Don't think there's a better way to check if the template exists besides trying to instantiate it.. + try + { + var template = new businesslogic.template.Template(_document.Template); + // If template is found check if it's in the list of allowed templates for this document + return _document.Content.ContentType.AllowedTemplates.ToList().Any(t => t.Id == template.Id); + } + catch (Exception) { } + + return false; + } + /// /// JsInclude1 control. /// From 65d7e04049bb6b42270c5d89549788cc45db5cad Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 7 Aug 2013 10:34:51 +1000 Subject: [PATCH 11/21] fix up merge errors --- src/Umbraco.Core/Models/PropertyExtensions.cs | 25 ++++++++----------- .../TestHelpers/BaseDatabaseFactoryTest.cs | 9 ++++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Core/Models/PropertyExtensions.cs b/src/Umbraco.Core/Models/PropertyExtensions.cs index 8fb94e8c0c..d5a9ce8ac9 100644 --- a/src/Umbraco.Core/Models/PropertyExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyExtensions.cs @@ -3,6 +3,8 @@ using System.Xml; using System.Xml.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using umbraco.interfaces; namespace Umbraco.Core.Models { @@ -39,19 +41,6 @@ namespace Umbraco.Core.Models // XML construct from the value returned from the Property Editor. // More details discussed here: https://groups.google.com/forum/?fromgroups=#!topic/umbraco-dev/fieWZzHj7oY - //var dataType = ApplicationContext.Current.Services.DataTypeService.GetDataTypeDefinitionById(property.PropertyType.DataTypeDefinitionId); - //if (dataType == null) throw new InvalidOperationException("No data type definition found with id " + property.PropertyType.DataTypeDefinitionId); - - //We've already got the value for the property so we're going to give it to the - // data type's data property so it doesn't go re-look up the value from the db again. - var defaultData = dt.Data as IDataValueSetter; - if (defaultData != null) - { - defaultData.SetValue(property.Value, property.PropertyType.DataTypeDatabaseType.ToString()); - } - - xmlNode.AppendChild(dt.Data.ToXMl(xd)); - var propertyEditor = PropertyEditorResolver.Current.GetById(property.PropertyType.DataTypeId); if (propertyEditor != null) { @@ -86,7 +75,15 @@ namespace Umbraco.Core.Models else { //NOTE: An exception will be thrown if this doesn't exist - var legacyDataType = property.PropertyType.DataType(property.Id); + var legacyDataType = property.PropertyType.DataType(property.Id, ApplicationContext.Current.Services.DataTypeService); + + //We've already got the value for the property so we're going to give it to the + // data type's data property so it doesn't go re-look up the value from the db again. + var defaultData = legacyDataType.Data as IDataValueSetter; + if (defaultData != null) + { + defaultData.SetValue(property.Value, property.PropertyType.DataTypeDatabaseType.ToString()); + } xmlNode.AppendChild(legacyDataType.Data.ToXMl(xd)); } diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index dd75809a5f..9e653d7d29 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -57,17 +57,20 @@ namespace Umbraco.Tests.TestHelpers var path = TestHelper.CurrentAssemblyDirectory; AppDomain.CurrentDomain.SetData("DataDirectory", path); + + //disable cache + var cacheHelper = new CacheHelper(new NullCacheProvider(), false); var dbFactory = new DefaultDatabaseFactory( GetDbConnectionString(), GetDbProviderName()); + _appContext = new ApplicationContext( //assign the db context new DatabaseContext(dbFactory), //assign the service context - new ServiceContext(new PetaPocoUnitOfWorkProvider(dbFactory), new FileUnitOfWorkProvider(), new PublishingStrategy()), - //disable cache - false) + new ServiceContext(new PetaPocoUnitOfWorkProvider(dbFactory), new FileUnitOfWorkProvider(), new PublishingStrategy(), cacheHelper), + cacheHelper) { IsReady = true }; From 1b9f0715813a90bca6f227bfe35cbb4d4ea3256a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 7 Aug 2013 11:39:25 +1000 Subject: [PATCH 12/21] Fixes: U4-2589 Save and Publish is not creating version of document and adds a few unit tests --- src/Umbraco.Core/Models/ContentBase.cs | 15 +++++ src/Umbraco.Core/Models/ContentExtensions.cs | 43 ++++++++++++++ .../Repositories/ContentRepository.cs | 30 +++++----- .../Models/ContentExtensionsTests.cs | 59 +++++++++++++++++++ src/Umbraco.Tests/Models/ContentTests.cs | 15 +++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 6 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Tests/Models/ContentExtensionsTests.cs diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 6414ea2116..cc5cfcdee5 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -436,5 +436,20 @@ namespace Umbraco.Core.Models } public abstract void ChangeTrashedState(bool isTrashed, int parentId = -20); + + /// + /// We will override this method to ensure that when we reset the dirty properties that we + /// also reset the dirty changes made to the content's Properties (user defined) + /// + /// + internal override void ResetDirtyProperties(bool rememberPreviouslyChangedProperties) + { + base.ResetDirtyProperties(rememberPreviouslyChangedProperties); + + foreach (var prop in Properties) + { + prop.ResetDirtyProperties(rememberPreviouslyChangedProperties); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 0a15cf941c..c30f96c214 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -12,6 +12,7 @@ using System.Xml.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Media; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Strings; using Umbraco.Core.Persistence; @@ -23,6 +24,48 @@ namespace Umbraco.Core.Models public static class ContentExtensions { #region IContent + + /// + /// Determines if a new version should be created + /// + /// + /// + /// + /// A new version needs to be created when: + /// * Any property value is changed (to enable a rollback) + /// * The publish status is changed + /// * The language is changed + /// + internal static bool ShouldCreateNewVersion(this IContent entity) + { + var publishedState = ((Content)entity).PublishedState; + return ShouldCreateNewVersion(entity, publishedState); + } + + /// + /// Determines if a new version should be created + /// + /// + /// + /// + /// + /// A new version needs to be created when: + /// * Any property value is changed (to enable a rollback) + /// * The publish status is changed + /// * The language is changed + /// + internal static bool ShouldCreateNewVersion(this IContent entity, PublishedState publishedState) + { + var dirtyEntity = (ICanBeDirty)entity; + var contentChanged = + (dirtyEntity.IsPropertyDirty("Published") && publishedState != PublishedState.Unpublished) + || dirtyEntity.IsPropertyDirty("Language"); + + var propertyValueChanged = entity.Properties.Any(x => ((ICanBeDirty)x).IsDirty()); + + return contentChanged || propertyValueChanged; + } + /// /// Returns a list of the current contents ancestors, not including the content itself. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 921a7e75dd..ced4bfa07b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -275,8 +275,9 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistUpdatedItem(IContent entity) { var publishedState = ((Content) entity).PublishedState; - //A new version should only be created if published state (or language) has changed - bool shouldCreateNewVersion = (((ICanBeDirty)entity).IsPropertyDirty("Published") && publishedState != PublishedState.Unpublished) || ((ICanBeDirty)entity).IsPropertyDirty("Language"); + + //check if we need to create a new version + bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); if (shouldCreateNewVersion) { //Updates Modified date and Version Guid @@ -338,22 +339,19 @@ namespace Umbraco.Core.Persistence.Repositories } } - //Look up (newest) entries by id in cmsDocument table to set newest = false - //NOTE: This should only be done for all other versions then the current one, so we don't cause the same entry to be updated multiple times. - var documentDtos = - Database.Query( - "WHERE nodeId = @Id AND newest = @IsNewest AND NOT(versionId = @VersionId)", - new {Id = entity.Id, IsNewest = true, VersionId = dto.ContentVersionDto.VersionId}); - foreach (var documentDto in documentDtos) - { - var docDto = documentDto; - docDto.Newest = false; - Database.Update(docDto); - } - var contentVersionDto = dto.ContentVersionDto; if (shouldCreateNewVersion) { + //Look up (newest) entries by id in cmsDocument table to set newest = false + //NOTE: This is only relevant when a new version is created, which is why its done inside this if-statement. + var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); + foreach (var documentDto in documentDtos) + { + var docDto = documentDto; + docDto.Newest = false; + Database.Update(docDto); + } + //Create a new version - cmsContentVersion //Assumes a new Version guid and Version date (modified date) has been set Database.Insert(contentVersionDto); @@ -484,7 +482,7 @@ namespace Umbraco.Core.Persistence.Repositories } #endregion - + /// /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. /// diff --git a/src/Umbraco.Tests/Models/ContentExtensionsTests.cs b/src/Umbraco.Tests/Models/ContentExtensionsTests.cs new file mode 100644 index 0000000000..3da5b12a7f --- /dev/null +++ b/src/Umbraco.Tests/Models/ContentExtensionsTests.cs @@ -0,0 +1,59 @@ +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Tests.TestHelpers.Entities; + +namespace Umbraco.Tests.Models +{ + [TestFixture] + public class ContentExtensionsTests + { + [Test] + public void Should_Create_New_Version_When_Publish_Status_Changed() + { + var contentType = MockedContentTypes.CreateTextpageContentType(); + var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); + + content.ResetDirtyProperties(false); + + content.Published = true; + Assert.IsTrue(content.ShouldCreateNewVersion(PublishedState.Published)); + } + + [Test] + public void Should_Create_New_Version_When_Language_Changed() + { + var contentType = MockedContentTypes.CreateTextpageContentType(); + var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); + + content.ResetDirtyProperties(false); + + content.Language = "en-AU"; + Assert.IsTrue(content.ShouldCreateNewVersion(PublishedState.Unpublished)); + } + + [Test] + public void Should_Create_New_Version_When_Any_Property_Value_Changed() + { + var contentType = MockedContentTypes.CreateTextpageContentType(); + var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); + + content.ResetDirtyProperties(false); + + content.Properties.First().Value = "hello world"; + Assert.IsTrue(content.ShouldCreateNewVersion(PublishedState.Unpublished)); + } + + [Test] + public void Should_Not_Create_New_Version_When_Anything_Other_Than_Published_Language_Or_Property_Vals_Changed() + { + var contentType = MockedContentTypes.CreateTextpageContentType(); + var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); + + content.ResetDirtyProperties(false); + + Assert.IsFalse(content.ShouldCreateNewVersion(PublishedState.Unpublished)); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 33af9c30e6..292fa89a80 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -13,6 +13,21 @@ namespace Umbraco.Tests.Models [TestFixture] public class ContentTests { + [Test] + public void All_Dirty_Properties_Get_Reset() + { + var contentType = MockedContentTypes.CreateTextpageContentType(); + var content = MockedContent.CreateTextpageContent(contentType, "Textpage", -1); + + content.ResetDirtyProperties(false); + + Assert.IsFalse(content.IsDirty()); + foreach (var prop in content.Properties) + { + Assert.IsFalse(prop.IsDirty()); + } + } + [Test] public void Can_Verify_Mocked_Content() { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 747cb52cee..d95b720b73 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -185,6 +185,7 @@ + From 14e4a8061c783955896e321f13196d05529a3b9c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 7 Aug 2013 11:49:48 +1000 Subject: [PATCH 13/21] removes the vs profiling files from the sln. --- src/umbraco.sln | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/umbraco.sln b/src/umbraco.sln index eba2ad7b9b..43ec254562 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -67,11 +67,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C ..\build\NuSpecs\UmbracoCms.nuspec = ..\build\NuSpecs\UmbracoCms.nuspec EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{99794542-89EC-43BA-88BE-E31A9D61423B}" - ProjectSection(SolutionItems) = preProject - Performance1.psess = Performance1.psess - EndProjectSection -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 5fce74194077a38dbe1a60b60ababf18798bf54b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 7 Aug 2013 11:53:09 +1000 Subject: [PATCH 14/21] another sln update --- src/umbraco.sln | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/umbraco.sln b/src/umbraco.sln index 43ec254562..fee2486039 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -157,7 +157,4 @@ Global {73529637-28F5-419C-A6BB-D094E39DE614} = {DD32977B-EF54-475B-9A1B-B97A502C6E58} {B555AAE6-0F56-442F-AC9F-EF497DB38DE7} = {DD32977B-EF54-475B-9A1B-B97A502C6E58} EndGlobalSection - GlobalSection(Performance) = preSolution - HasPerformanceSessions = true - EndGlobalSection EndGlobal From 91a2ffcf1d267a98c28aaf8cb5bc0b845e985242 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 7 Aug 2013 12:26:49 +1000 Subject: [PATCH 15/21] removes js files we aren't using, if we need them later we can add them back. --- .../lib/jquery/VerticalAlign.js | 8 - .../lib/jquery/jquery-fieldselection.js | 83 - .../lib/jquery/jquery.autocomplete.js | 809 ------- .../lib/jquery/jquery.ba-bbq.min.js | 18 - .../lib/jquery/jquery.cookie.js | 96 - .../lib/jquery/jquery.hotkeys.js | 99 - .../lib/jquery/jquery.idle-timer.js | 232 -- .../lib/jquery/jquery.js | 4 - .../lib/jquery/jquery.metadata.min.js | 13 - .../lib/jquery/jquery.noconflict-invoke.js | 3 - .../lib/jquery/jquery.tagsinput.js | 354 --- .../lib/jquery/jquery.tagsinput.min.js | 1 - .../lib/require/async.js | 42 - src/Umbraco.Web.UI.Client/lib/require/css.js | 1 - .../lib/require/domReady.js | 129 -- .../lib/require/require-1.0.8.js | 33 - .../lib/require/require-2.1.0.js | 1968 ----------------- .../lib/require/require.min.js | 34 - src/Umbraco.Web.UI.Client/lib/require/text.js | 308 --- 19 files changed, 4235 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/VerticalAlign.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery-fieldselection.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.autocomplete.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.ba-bbq.min.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.cookie.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.hotkeys.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.idle-timer.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.metadata.min.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.noconflict-invoke.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.tagsinput.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/jquery/jquery.tagsinput.min.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/require/async.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/require/css.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/require/domReady.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/require/require-1.0.8.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/require/require-2.1.0.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/require/require.min.js delete mode 100644 src/Umbraco.Web.UI.Client/lib/require/text.js diff --git a/src/Umbraco.Web.UI.Client/lib/jquery/VerticalAlign.js b/src/Umbraco.Web.UI.Client/lib/jquery/VerticalAlign.js deleted file mode 100644 index 7db9a0636d..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/jquery/VerticalAlign.js +++ /dev/null @@ -1,8 +0,0 @@ -(function($) { - $.fn.VerticalAlign = function(opts) { - return this.each(function() { - var top = (($(this).parent().height() - $(this).height()) / 2); - $(this).css('margin-top', top); - }); - }; -})(jQuery); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/jquery/jquery-fieldselection.js b/src/Umbraco.Web.UI.Client/lib/jquery/jquery-fieldselection.js deleted file mode 100644 index e917c5ec04..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/jquery/jquery-fieldselection.js +++ /dev/null @@ -1,83 +0,0 @@ -// -// jQuery plugin: fieldSelection - v0.1.0 - last change: 2006-12-16 -// (c) 2006 Alex Brem - http://blog.0xab.cd -// - -(function() { - - var fieldSelection = { - - getSelection: function() { - - var e = this.jquery ? this[0] : this; - - return ( - - // mozilla or dom 3.0 - ('selectionStart' in e && function() { - var l = e.selectionEnd - e.selectionStart; - return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) }; - }) || - - // exploder - (document.selection && function() { - - e.focus(); - - var r = document.selection.createRange(); - if (r == null) { - return { start: 0, end: e.value.length, length: 0 } - } - - var re = e.createTextRange(); - var rc = re.duplicate(); - re.moveToBookmark(r.getBookmark()); - rc.setEndPoint('EndToStart', re); - - return { start: rc.text.length, end: rc.text.length + r.text.length, length: r.text.length, text: r.text }; - }) || - - // browser not supported - function() { - return { start: 0, end: e.value.length, length: 0 }; - } - - )(); - - }, - - replaceSelection: function() { - - var e = this.jquery ? this[0] : this; - var text = arguments[0] || ''; - - return ( - - // mozilla or dom 3.0 - ('selectionStart' in e && function() { - e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length); - return this; - }) || - - // exploder - (document.selection && function() { - e.focus(); - document.selection.createRange().text = text; - return this; - }) || - - // browser not supported - function() { - e.value += text; - return this; - } - - )(); - - } - - }; - - jQuery.each(fieldSelection, function(i) { jQuery.fn[i] = this; }); - -})(); diff --git a/src/Umbraco.Web.UI.Client/lib/jquery/jquery.autocomplete.js b/src/Umbraco.Web.UI.Client/lib/jquery/jquery.autocomplete.js deleted file mode 100644 index 9207aaafc2..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/jquery/jquery.autocomplete.js +++ /dev/null @@ -1,809 +0,0 @@ -/* -* jQuery Autocomplete plugin 1.1 -* -* Copyright (c) 2009 Jörn Zaefferer -* -* Dual licensed under the MIT and GPL licenses: -* http://www.opensource.org/licenses/mit-license.php -* http://www.gnu.org/licenses/gpl.html -* -* Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $ -*/ - -; (function($) { - - $.fn.extend({ - autocomplete: function(urlOrData, options) { - var isUrl = typeof urlOrData == "string"; - options = $.extend({}, $.Autocompleter.defaults, { - url: isUrl ? urlOrData : null, - data: isUrl ? null : urlOrData, - delay: isUrl ? $.Autocompleter.defaults.delay : 10, - max: options && !options.scroll ? 10 : 150 - }, options); - - // if highlight is set to false, replace it with a do-nothing function - options.highlight = options.highlight || function(value) { return value; }; - - // if the formatMatch option is not specified, then use formatItem for backwards compatibility - options.formatMatch = options.formatMatch || options.formatItem; - - return this.each(function() { - new $.Autocompleter(this, options); - }); - }, - result: function(handler) { - return this.bind("result", handler); - }, - search: function(handler) { - return this.trigger("search", [handler]); - }, - flushCache: function() { - return this.trigger("flushCache"); - }, - setOptions: function(options) { - return this.trigger("setOptions", [options]); - }, - unautocomplete: function() { - return this.trigger("unautocomplete"); - } - }); - - $.Autocompleter = function(input, options) { - - var KEY = { - UP: 38, - DOWN: 40, - DEL: 46, - TAB: 9, - RETURN: 13, - ESC: 27, - COMMA: 188, - PAGEUP: 33, - PAGEDOWN: 34, - BACKSPACE: 8 - }; - - // Create $ object for input element - var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); - - var timeout; - var previousValue = ""; - var cache = $.Autocompleter.Cache(options); - var hasFocus = 0; - var lastKeyPressCode; - var config = { - mouseDownOnSelect: false - }; - var select = $.Autocompleter.Select(options, input, selectCurrent, config); - - var blockSubmit; - - // prevent form submit in opera when selecting with return key - $.browser.opera && $(input.form).bind("submit.autocomplete", function() { - if (blockSubmit) { - blockSubmit = false; - return false; - } - }); - - // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all - $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) { - // a keypress means the input has focus - // avoids issue where input had focus before the autocomplete was applied - hasFocus = 1; - // track last key pressed - lastKeyPressCode = event.keyCode; - switch (event.keyCode) { - - case KEY.UP: - event.preventDefault(); - if (select.visible()) { - select.prev(); - } else { - onChange(0, true); - } - break; - - case KEY.DOWN: - event.preventDefault(); - if (select.visible()) { - select.next(); - } else { - onChange(0, true); - } - break; - - case KEY.PAGEUP: - event.preventDefault(); - if (select.visible()) { - select.pageUp(); - } else { - onChange(0, true); - } - break; - - case KEY.PAGEDOWN: - event.preventDefault(); - if (select.visible()) { - select.pageDown(); - } else { - onChange(0, true); - } - break; - - // matches also semicolon - case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: - case KEY.TAB: - case KEY.RETURN: - if (selectCurrent()) { - // stop default to prevent a form submit, Opera needs special handling - event.preventDefault(); - blockSubmit = true; - return false; - } - break; - - case KEY.ESC: - select.hide(); - break; - - default: - clearTimeout(timeout); - timeout = setTimeout(onChange, options.delay); - break; - } - }).focus(function() { - // track whether the field has focus, we shouldn't process any - // results if the field no longer has focus - hasFocus++; - }).blur(function() { - hasFocus = 0; - if (!config.mouseDownOnSelect) { - hideResults(); - } - }).click(function() { - // show select when clicking in a focused field - if (hasFocus++ > 1 && !select.visible()) { - onChange(0, true); - } - }).bind("search", function() { - // TODO why not just specifying both arguments? - var fn = (arguments.length > 1) ? arguments[1] : null; - function findValueCallback(q, data) { - var result; - if (data && data.length) { - for (var i = 0; i < data.length; i++) { - if (data[i].result.toLowerCase() == q.toLowerCase()) { - result = data[i]; - break; - } - } - } - if (typeof fn == "function") fn(result); - else $input.trigger("result", result && [result.data, result.value]); - } - $.each(trimWords($input.val()), function(i, value) { - request(value, findValueCallback, findValueCallback); - }); - }).bind("flushCache", function() { - cache.flush(); - }).bind("setOptions", function() { - $.extend(options, arguments[1]); - // if we've updated the data, repopulate - if ("data" in arguments[1]) - cache.populate(); - }).bind("unautocomplete", function() { - select.unbind(); - $input.unbind(); - $(input.form).unbind(".autocomplete"); - }); - - - function selectCurrent() { - var selected = select.selected(); - if (!selected) - return false; - - var v = selected.result; - previousValue = v; - - if (options.multiple) { - var words = trimWords($input.val()); - if (words.length > 1) { - var seperator = options.multipleSeparator.length; - var cursorAt = $(input).selection().start; - var wordAt, progress = 0; - $.each(words, function(i, word) { - progress += word.length; - if (cursorAt <= progress) { - wordAt = i; - return false; - } - progress += seperator; - }); - words[wordAt] = v; - // TODO this should set the cursor to the right position, but it gets overriden somewhere - //$.Autocompleter.Selection(input, progress + seperator, progress + seperator); - v = words.join(options.multipleSeparator); - } - v += options.multipleSeparator; - } - - $input.val(v); - hideResultsNow(); - $input.trigger("result", [selected.data, selected.value]); - return true; - } - - function onChange(crap, skipPrevCheck) { - if (lastKeyPressCode == KEY.DEL) { - select.hide(); - return; - } - - var currentValue = $input.val(); - - if (!skipPrevCheck && currentValue == previousValue) - return; - - previousValue = currentValue; - - currentValue = lastWord(currentValue); - if (currentValue.length >= options.minChars) { - $input.addClass(options.loadingClass); - if (!options.matchCase) - currentValue = currentValue.toLowerCase(); - request(currentValue, receiveData, hideResultsNow); - } else { - stopLoading(); - select.hide(); - } - }; - - function trimWords(value) { - if (!value) - return [""]; - if (!options.multiple) - return [$.trim(value)]; - return $.map(value.split(options.multipleSeparator), function(word) { - return $.trim(value).length ? $.trim(word) : null; - }); - } - - function lastWord(value) { - if (!options.multiple) - return value; - var words = trimWords(value); - if (words.length == 1) - return words[0]; - var cursorAt = $(input).selection().start; - if (cursorAt == value.length) { - words = trimWords(value) - } else { - words = trimWords(value.replace(value.substring(cursorAt), "")); - } - return words[words.length - 1]; - } - - // fills in the input box w/the first match (assumed to be the best match) - // q: the term entered - // sValue: the first matching result - function autoFill(q, sValue) { - // autofill in the complete box w/the first match as long as the user hasn't entered in more data - // if the last user key pressed was backspace, don't autofill - if (options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE) { - // fill in the value (keep the case the user has typed) - $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); - // select the portion of the value not typed by the user (so the next character will erase) - $(input).selection(previousValue.length, previousValue.length + sValue.length); - } - }; - - function hideResults() { - clearTimeout(timeout); - timeout = setTimeout(hideResultsNow, 200); - }; - - function hideResultsNow() { - var wasVisible = select.visible(); - select.hide(); - clearTimeout(timeout); - stopLoading(); - if (options.mustMatch) { - // call search and run callback - $input.search( - function(result) { - // if no value found, clear the input box - if (!result) { - if (options.multiple) { - var words = trimWords($input.val()).slice(0, -1); - $input.val(words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "")); - } - else { - $input.val(""); - $input.trigger("result", null); - } - } - } - ); - } - }; - - function receiveData(q, data) { - if (data && data.length && hasFocus) { - stopLoading(); - select.display(data, q); - autoFill(q, data[0].value); - select.show(); - } else { - hideResultsNow(); - } - }; - - function request(term, success, failure) { - if (!options.matchCase) - term = term.toLowerCase(); - var data = cache.load(term); - // recieve the cached data - if (data && data.length) { - success(term, data); - // if an AJAX url has been supplied, try loading the data now - } else if ((typeof options.url == "string") && (options.url.length > 0)) { - - var extraParams = { - timestamp: +new Date() - }; - $.each(options.extraParams, function(key, param) { - extraParams[key] = typeof param == "function" ? param() : param; - }); - - $.ajax({ - // try to leverage ajaxQueue plugin to abort previous requests - mode: "abort", - // limit abortion to this input - port: "autocomplete" + input.name, - dataType: options.dataType, - url: options.url, - data: $.extend({ - q: lastWord(term), - limit: options.max - }, extraParams), - success: function(data) { - var parsed = options.parse && options.parse(data) || parse(data); - cache.add(term, parsed); - success(term, parsed); - } - }); - } else { - // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match - select.emptyList(); - failure(term); - } - }; - - function parse(data) { - var parsed = []; - var rows = data.split("\n"); - for (var i = 0; i < rows.length; i++) { - var row = $.trim(rows[i]); - if (row) { - row = row.split("|"); - parsed[parsed.length] = { - data: row, - value: row[0], - result: options.formatResult && options.formatResult(row, row[0]) || row[0] - }; - } - } - return parsed; - }; - - function stopLoading() { - $input.removeClass(options.loadingClass); - }; - - }; - - $.Autocompleter.defaults = { - inputClass: "ac_input", - resultsClass: "ac_results", - loadingClass: "ac_loading", - minChars: 1, - delay: 400, - matchCase: false, - matchSubset: true, - matchContains: false, - cacheLength: 10, - max: 100, - mustMatch: false, - extraParams: {}, - selectFirst: true, - formatItem: function(row) { return row[0]; }, - formatMatch: null, - autoFill: false, - width: 0, - multiple: false, - multipleSeparator: ", ", - highlight: function(value, term) { - return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); - }, - scroll: true, - scrollHeight: 180 - }; - - $.Autocompleter.Cache = function(options) { - - var data = {}; - var length = 0; - - function matchSubset(s, sub) { - s = s.toString(); // FR: This forces the value to be a string, otherwise the indexOf() method fails on anything other but a string - if (!options.matchCase) - s = s.toLowerCase(); - var i = s.indexOf(sub); - if (options.matchContains == "word") { - i = s.toLowerCase().search("\\b" + sub.toLowerCase()); - } - if (i == -1) return false; - return i == 0 || options.matchContains; - }; - - function add(q, value) { - if (length > options.cacheLength) { - flush(); - } - if (!data[q]) { - length++; - } - data[q] = value; - } - - function populate() { - if (!options.data) return false; - // track the matches - var stMatchSets = {}, - nullData = 0; - - // no url was specified, we need to adjust the cache length to make sure it fits the local data store - if (!options.url) options.cacheLength = 1; - - // track all options for minChars = 0 - stMatchSets[""] = []; - - // loop through the array and create a lookup structure - for (var i = 0, ol = options.data.length; i < ol; i++) { - var rawValue = options.data[i]; - // if rawValue is a string, make an array otherwise just reference the array - rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; - - var value = options.formatMatch(rawValue, i + 1, options.data.length); - if (value === false) - continue; - - var firstChar = value.charAt(0).toLowerCase(); - // if no lookup array for this character exists, look it up now - if (!stMatchSets[firstChar]) - stMatchSets[firstChar] = []; - - // if the match is a string - var row = { - value: value, - data: rawValue, - result: options.formatResult && options.formatResult(rawValue) || value - }; - - // push the current match into the set list - stMatchSets[firstChar].push(row); - - // keep track of minChars zero items - if (nullData++ < options.max) { - stMatchSets[""].push(row); - } - }; - - // add the data items to the cache - $.each(stMatchSets, function(i, value) { - // increase the cache size - options.cacheLength++; - // add to the cache - add(i, value); - }); - } - - // populate any existing data - setTimeout(populate, 25); - - function flush() { - data = {}; - length = 0; - } - - return { - flush: flush, - add: add, - populate: populate, - load: function(q) { - if (!options.cacheLength || !length) - return null; - /* - * if dealing w/local data and matchContains than we must make sure - * to loop through all the data collections looking for matches - */ - if (!options.url && options.matchContains) { - // track all matches - var csub = []; - // loop through all the data grids for matches - for (var k in data) { - // don't search through the stMatchSets[""] (minChars: 0) cache - // this prevents duplicates - if (k.length > 0) { - var c = data[k]; - $.each(c, function(i, x) { - // if we've got a match, add it to the array - if (matchSubset(x.value, q)) { - csub.push(x); - } - }); - } - } - return csub; - } else - // if the exact item exists, use it - if (data[q]) { - return data[q]; - } else - if (options.matchSubset) { - for (var i = q.length - 1; i >= options.minChars; i--) { - var c = data[q.substr(0, i)]; - if (c) { - var csub = []; - $.each(c, function(i, x) { - if (matchSubset(x.value, q)) { - csub[csub.length] = x; - } - }); - return csub; - } - } - } - return null; - } - }; - }; - - $.Autocompleter.Select = function(options, input, select, config) { - var CLASSES = { - ACTIVE: "ac_over" - }; - - var listItems, - active = -1, - data, - term = "", - needsInit = true, - element, - list; - - // Create results - function init() { - if (!needsInit) - return; - element = $("
") - .hide() - .addClass(options.resultsClass) - .css("position", "absolute") - .appendTo(document.body); - - list = $("
    ").appendTo(element).mouseover(function(event) { - if (target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') { - active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event)); - $(target(event)).addClass(CLASSES.ACTIVE); - } - }).click(function(event) { - $(target(event)).addClass(CLASSES.ACTIVE); - select(); - // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus - input.focus(); - return false; - }).mousedown(function() { - config.mouseDownOnSelect = true; - }).mouseup(function() { - config.mouseDownOnSelect = false; - }); - - if (options.width > 0) - element.css("width", options.width); - - needsInit = false; - } - - function target(event) { - var element = event.target; - while (element && element.tagName != "LI") - element = element.parentNode; - // more fun with IE, sometimes event.target is empty, just ignore it then - if (!element) - return []; - return element; - } - - function moveSelect(step) { - listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE); - movePosition(step); - var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE); - if (options.scroll) { - var offset = 0; - listItems.slice(0, active).each(function() { - offset += this.offsetHeight; - }); - if ((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) { - list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight()); - } else if (offset < list.scrollTop()) { - list.scrollTop(offset); - } - } - }; - - function movePosition(step) { - active += step; - if (active < 0) { - active = listItems.size() - 1; - } else if (active >= listItems.size()) { - active = 0; - } - } - - function limitNumberOfItems(available) { - return options.max && options.max < available - ? options.max - : available; - } - - function fillList() { - list.empty(); - var max = limitNumberOfItems(data.length); - for (var i = 0; i < max; i++) { - if (!data[i]) - continue; - var formatted = options.formatItem(data[i].data, i + 1, max, data[i].value, term); - if (formatted === false) - continue; - var li = $("
  • ").html(options.highlight(formatted, term)).addClass(i % 2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0]; - $.data(li, "ac_data", data[i]); - } - listItems = list.find("li"); - if (options.selectFirst) { - listItems.slice(0, 1).addClass(CLASSES.ACTIVE); - active = 0; - } - // apply bgiframe if available - if ($.fn.bgiframe) - list.bgiframe(); - } - - return { - display: function(d, q) { - init(); - data = d; - term = q; - fillList(); - }, - next: function() { - moveSelect(1); - }, - prev: function() { - moveSelect(-1); - }, - pageUp: function() { - if (active != 0 && active - 8 < 0) { - moveSelect(-active); - } else { - moveSelect(-8); - } - }, - pageDown: function() { - if (active != listItems.size() - 1 && active + 8 > listItems.size()) { - moveSelect(listItems.size() - 1 - active); - } else { - moveSelect(8); - } - }, - hide: function() { - element && element.hide(); - listItems && listItems.removeClass(CLASSES.ACTIVE); - active = -1; - }, - visible: function() { - return element && element.is(":visible"); - }, - current: function() { - return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]); - }, - show: function() { - var offset = $(input).offset(); - element.css({ - width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(), - top: offset.top + input.offsetHeight, - left: offset.left - }).show(); - if (options.scroll) { - list.scrollTop(0); - list.css({ - maxHeight: options.scrollHeight, - overflow: 'auto' - }); - - if ($.browser.msie && typeof document.body.style.maxHeight === "undefined") { - var listHeight = 0; - listItems.each(function() { - listHeight += this.offsetHeight; - }); - var scrollbarsVisible = listHeight > options.scrollHeight; - list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight); - if (!scrollbarsVisible) { - // IE doesn't recalculate width when scrollbar disappears - listItems.width(list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right"))); - } - } - - } - }, - selected: function() { - var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE); - return selected && selected.length && $.data(selected[0], "ac_data"); - }, - emptyList: function() { - list && list.empty(); - }, - unbind: function() { - element && element.remove(); - } - }; - }; - - $.fn.selection = function(start, end) { - if (start !== undefined) { - return this.each(function() { - if (this.createTextRange) { - var selRange = this.createTextRange(); - if (end === undefined || start == end) { - selRange.move("character", start); - selRange.select(); - } else { - selRange.collapse(true); - selRange.moveStart("character", start); - selRange.moveEnd("character", end); - selRange.select(); - } - } else if (this.setSelectionRange) { - this.setSelectionRange(start, end); - } else if (this.selectionStart) { - this.selectionStart = start; - this.selectionEnd = end; - } - }); - } - var field = this[0]; - if (field.createTextRange) { - var range = document.selection.createRange(), - orig = field.value, - teststring = "<->", - textLength = range.text.length; - range.text = teststring; - var caretAt = field.value.indexOf(teststring); - field.value = orig; - this.selection(caretAt, caretAt + textLength); - return { - start: caretAt, - end: caretAt + textLength - } - } else if (field.selectionStart !== undefined) { - return { - start: field.selectionStart, - end: field.selectionEnd - } - } - }; - -})(jQuery); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/jquery/jquery.ba-bbq.min.js b/src/Umbraco.Web.UI.Client/lib/jquery/jquery.ba-bbq.min.js deleted file mode 100644 index a9fc08814c..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/jquery/jquery.ba-bbq.min.js +++ /dev/null @@ -1,18 +0,0 @@ -/* -* jQuery BBQ: Back Button & Query Library - v1.3pre - 8/26/2010 -* http://benalman.com/projects/jquery-bbq-plugin/ -* -* Copyright (c) 2010 "Cowboy" Ben Alman -* Dual licensed under the MIT and GPL licenses. -* http://benalman.com/about/license/ -*/ -(function ($, r) { var h, n = Array.prototype.slice, t = decodeURIComponent, a = $.param, j, c, m, y, b = $.bbq = $.bbq || {}, s, x, k, e = $.event.special, d = "hashchange", B = "querystring", F = "fragment", z = "elemUrlAttr", l = "href", w = "src", p = /^.*\?|#.*$/g, u, H, g, i, C, E = {}; function G(I) { return typeof I === "string" } function D(J) { var I = n.call(arguments, 1); return function () { return J.apply(this, I.concat(n.call(arguments))) } } function o(I) { return I.replace(H, "$2") } function q(I) { return I.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/, "$1") } function f(K, P, I, L, J) { var R, O, N, Q, M; if (L !== h) { N = I.match(K ? H : /^([^#?]*)\??([^#]*)(#?.*)/); M = N[3] || ""; if (J === 2 && G(L)) { O = L.replace(K ? u : p, "") } else { Q = m(N[2]); L = G(L) ? m[K ? F : B](L) : L; O = J === 2 ? L : J === 1 ? $.extend({}, L, Q) : $.extend({}, Q, L); O = j(O); if (K) { O = O.replace(g, t) } } R = N[1] + (K ? C : O || !N[1] ? "?" : "") + O + M } else { R = P(I !== h ? I : location.href) } return R } a[B] = D(f, 0, q); a[F] = c = D(f, 1, o); a.sorted = j = function (J, K) { var I = [], L = {}; $.each(a(J, K).split("&"), function (P, M) { var O = M.replace(/(?:%5B|=).*$/, ""), N = L[O]; if (!N) { N = L[O] = []; I.push(O) } N.push(M) }); return $.map(I.sort(), function (M) { return L[M] }).join("&") }; c.noEscape = function (J) { J = J || ""; var I = $.map(J.split(""), encodeURIComponent); g = new RegExp(I.join("|"), "g") }; c.noEscape(",/"); c.ajaxCrawlable = function (I) { if (I !== h) { if (I) { u = /^.*(?:#!|#)/; H = /^([^#]*)(?:#!|#)?(.*)$/; C = "#!" } else { u = /^.*#/; H = /^([^#]*)#?(.*)$/; C = "#" } i = !!I } return i }; c.ajaxCrawlable(0); $.deparam = m = function (L, I) { var K = {}, J = { "true": !0, "false": !1, "null": null }; $.each(L.replace(/\+/g, " ").split("&"), function (O, T) { var N = T.split("="), S = t(N[0]), M, R = K, P = 0, U = S.split("]["), Q = U.length - 1; if (/\[/.test(U[0]) && /\]$/.test(U[Q])) { U[Q] = U[Q].replace(/\]$/, ""); U = U.shift().split("[").concat(U); Q = U.length - 1 } else { Q = 0 } if (N.length === 2) { M = t(N[1]); if (I) { M = M && !isNaN(M) ? +M : M === "undefined" ? h : J[M] !== h ? J[M] : M } if (Q) { for (; P <= Q; P++) { S = U[P] === "" ? R.length : U[P]; R = R[S] = P < Q ? R[S] || (U[P + 1] && isNaN(U[P + 1]) ? {} : []) : M } } else { if ($.isArray(K[S])) { K[S].push(M) } else { if (K[S] !== h) { K[S] = [K[S], M] } else { K[S] = M } } } } else { if (S) { K[S] = I ? h : "" } } }); return K }; function A(K, I, J) { if (I === h || typeof I === "boolean") { J = I; I = a[K ? F : B]() } else { I = G(I) ? I.replace(K ? u : p, "") : I } return m(I, J) } m[B] = D(A, 0); m[F] = y = D(A, 1); $[z] || ($[z] = function (I) { return $.extend(E, I) })({ a: l, base: l, iframe: w, img: w, input: w, form: "action", link: l, script: w }); k = $[z]; function v(L, J, K, I) { if (!G(K) && typeof K !== "object") { I = K; K = J; J = h } return this.each(function () { var O = $(this), M = J || k()[(this.nodeName || "").toLowerCase()] || "", N = M && O.attr(M) || ""; O.attr(M, a[L](N, K, I)) }) } $.fn[B] = D(v, B); $.fn[F] = D(v, F); b.pushState = s = function (L, I) { if (G(L) && /^#/.test(L) && I === h) { I = 2 } var K = L !== h, J = c(location.href, K ? L : {}, K ? I : 2); location.href = J }; b.getState = x = function (I, J) { return I === h || typeof I === "boolean" ? y(I) : y(J)[I] }; b.removeState = function (I) { var J = {}; if (I !== h) { J = x(); $.each($.isArray(I) ? I : arguments, function (L, K) { delete J[K] }) } s(J, 2) }; e[d] = $.extend(e[d], { add: function (I) { var K; function J(M) { var L = M[F] = c(); M.getState = function (N, O) { return N === h || typeof N === "boolean" ? m(L, N) : m(L, O)[N] }; K.apply(this, arguments) } if ($.isFunction(I)) { K = I; return J } else { K = I.handler; I.handler = J } } }) })(jQuery, this); -/* -* jQuery hashchange event - v1.3 - 7/21/2010 -* http://benalman.com/projects/jquery-hashchange-plugin/ -* -* Copyright (c) 2010 "Cowboy" Ben Alman -* Dual licensed under the MIT and GPL licenses. -* http://benalman.com/about/license/ -*/ -(function ($, e, b) { var c = "hashchange", h = document, f, g = $.event.special, i = h.documentMode, d = "on" + c in e && (i === b || i > 7); function a(j) { j = j || location.href; return "#" + j.replace(/^[^#]*#?(.*)$/, "$1") } $.fn[c] = function (j) { return j ? this.bind(c, j) : this.trigger(c) }; $.fn[c].delay = 50; g[c] = $.extend(g[c], { setup: function () { if (d) { return false } $(f.start) }, teardown: function () { if (d) { return false } $(f.stop) } }); f = (function () { var j = {}, p, m = a(), k = function (q) { return q }, l = k, o = k; j.start = function () { p || n() }; j.stop = function () { p && clearTimeout(p); p = b }; function n() { var r = a(), q = o(m); if (r !== m) { l(m = r, q); $(e).trigger(c) } else { if (q !== m) { location.href = location.href.replace(/#.*/, "") + q } } p = setTimeout(n, $.fn[c].delay) } $.browser.msie && !d && (function () { var q, r; j.start = function () { if (!q) { r = $.fn[c].src; r = r && r + a(); q = $('