diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec
index 1dfec6062c..0c7d51334e 100644
--- a/build/NuSpecs/UmbracoCms.Core.nuspec
+++ b/build/NuSpecs/UmbracoCms.Core.nuspec
@@ -33,8 +33,8 @@
-
-
+
+
diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs
index 2d87f634f4..8378081394 100644
--- a/src/Umbraco.Core/IO/IOHelper.cs
+++ b/src/Umbraco.Core/IO/IOHelper.cs
@@ -152,12 +152,7 @@ namespace Umbraco.Core.IO
/// A value indicating whether the filepath is valid.
internal static bool VerifyEditPath(string filePath, string validDir)
{
- if (filePath.StartsWith(MapPath(SystemDirectories.Root)) == false)
- filePath = MapPath(filePath);
- if (validDir.StartsWith(MapPath(SystemDirectories.Root)) == false)
- validDir = MapPath(validDir);
-
- return filePath.StartsWith(validDir);
+ return VerifyEditPath(filePath, new[] { validDir });
}
///
@@ -182,12 +177,17 @@ namespace Umbraco.Core.IO
/// A value indicating whether the filepath is valid.
internal static bool VerifyEditPath(string filePath, IEnumerable validDirs)
{
+ var mappedRoot = MapPath(SystemDirectories.Root);
+ if (filePath.StartsWith(mappedRoot) == false)
+ filePath = MapPath(filePath);
+
+ // don't trust what we get, it may contain relative segments
+ filePath = Path.GetFullPath(filePath);
+
foreach (var dir in validDirs)
{
var validDir = dir;
- if (filePath.StartsWith(MapPath(SystemDirectories.Root)) == false)
- filePath = MapPath(filePath);
- if (validDir.StartsWith(MapPath(SystemDirectories.Root)) == false)
+ if (validDir.StartsWith(mappedRoot) == false)
validDir = MapPath(validDir);
if (filePath.StartsWith(validDir))
@@ -219,11 +219,8 @@ namespace Umbraco.Core.IO
/// A value indicating whether the filepath is valid.
internal static bool VerifyFileExtension(string filePath, List validFileExtensions)
{
- if (filePath.StartsWith(MapPath(SystemDirectories.Root)) == false)
- filePath = MapPath(filePath);
- var f = new FileInfo(filePath);
-
- return validFileExtensions.Contains(f.Extension.Substring(1));
+ var ext = Path.GetExtension(filePath);
+ return ext != null && validFileExtensions.Contains(ext.TrimStart('.'));
}
///
diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs
index 13df315960..8fcda3d10e 100644
--- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs
+++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs
@@ -33,6 +33,16 @@ namespace Umbraco.Core.IO
if (rootPath.StartsWith("~/"))
throw new ArgumentException("The rootPath argument cannot be a virtual path and cannot start with '~/'");
+ // rootPath should be... rooted, as in, it's a root path!
+ // but the test suite App.config cannot really "root" anything so we'll have to do it here
+
+ //var localRoot = AppDomain.CurrentDomain.BaseDirectory;
+ var localRoot = IOHelper.GetRootDirectorySafe();
+ if (Path.IsPathRooted(rootPath) == false)
+ {
+ rootPath = Path.Combine(localRoot, rootPath);
+ }
+
RootPath = rootPath;
_rootUrl = rootUrl;
}
@@ -177,9 +187,23 @@ namespace Umbraco.Core.IO
path = GetRelativePath(path);
}
- return !path.StartsWith(RootPath)
- ? Path.Combine(RootPath, path)
- : path;
+ // if already a full path, return
+ if (path.StartsWith(RootPath))
+ return path;
+
+ // else combine and sanitize, ie GetFullPath will take care of any relative
+ // segments in path, eg '../../foo.tmp' - it may throw a SecurityException
+ // if the combined path reaches illegal parts of the filesystem
+ var fpath = Path.Combine(RootPath, path);
+ fpath = Path.GetFullPath(fpath);
+
+ // at that point, path is within legal parts of the filesystem, ie we have
+ // permissions to reach that path, but it may nevertheless be outside of
+ // our root path, due to relative segments, so better check
+ if (fpath.StartsWith(RootPath))
+ return fpath;
+
+ throw new FileSecurityException("File '" + path + "' is outside this filesystem's root.");
}
public string GetUrl(string path)
diff --git a/src/Umbraco.Core/Models/IPublishedContentWithKey.cs b/src/Umbraco.Core/Models/IPublishedContentWithKey.cs
new file mode 100644
index 0000000000..b0e71221b2
--- /dev/null
+++ b/src/Umbraco.Core/Models/IPublishedContentWithKey.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Represents a cached content with a GUID key.
+ ///
+ /// This is temporary, because we cannot add the Key property to IPublishedContent without
+ /// breaking backward compatibility. With v8, it will be merged into IPublishedContent.
+ public interface IPublishedContentWithKey : IPublishedContent
+ {
+ Guid Key { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs
index 8c0eeef86f..fef066e0b1 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs
@@ -62,9 +62,20 @@ namespace Umbraco.Core.Models.PublishedContent
// model and therefore returned the original content unchanged.
var model = content.CreateModel();
- var extended = model == content // == means the factory did not create a model
- ? new PublishedContentExtended(content) // so we have to extend
- : model; // else we can use what the factory returned
+ IPublishedContent extended;
+ if (model == content) // == means the factory did not create a model
+ {
+ // so we have to extend
+ var contentWithKey = content as IPublishedContentWithKey;
+ extended = contentWithKey == null
+ ? new PublishedContentExtended(content)
+ : new PublishedContentWithKeyExtended(contentWithKey);
+ }
+ else
+ {
+ // else we can use what the factory returned
+ extended = model;
+ }
// so extended should always implement IPublishedContentExtended, however if
// by mistake the factory returned a different object that does not implement
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyExtended.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyExtended.cs
new file mode 100644
index 0000000000..492fd79796
--- /dev/null
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyExtended.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Umbraco.Core.Models.PublishedContent
+{
+ public class PublishedContentWithKeyExtended : PublishedContentExtended, IPublishedContentWithKey
+ {
+ // protected for models, internal for PublishedContentExtended static Extend method
+ protected internal PublishedContentWithKeyExtended(IPublishedContentWithKey content)
+ : base(content)
+ { }
+
+ public Guid Key { get { return ((IPublishedContentWithKey) Content).Key; } }
+ }
+}
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyModel.cs
new file mode 100644
index 0000000000..4761a52617
--- /dev/null
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyModel.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Umbraco.Core.Models.PublishedContent
+{
+ public abstract class PublishedContentWithKeyModel : PublishedContentModel, IPublishedContentWithKey
+ {
+ protected PublishedContentWithKeyModel(IPublishedContentWithKey content)
+ : base (content)
+ { }
+
+ public Guid Key { get { return ((IPublishedContentWithKey) Content).Key; } }
+ }
+}
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyWrapped.cs
new file mode 100644
index 0000000000..35d7dd6f1f
--- /dev/null
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyWrapped.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Umbraco.Core.Models.PublishedContent
+{
+ ///
+ /// Provides an abstract base class for IPublishedContentWithKey implementations that
+ /// wrap and extend another IPublishedContentWithKey.
+ ///
+ public class PublishedContentWithKeyWrapped : PublishedContentWrapped, IPublishedContentWithKey
+ {
+ protected PublishedContentWithKeyWrapped(IPublishedContentWithKey content)
+ : base(content)
+ { }
+
+ public virtual Guid Key { get { return ((IPublishedContentWithKey) Content).Key; } }
+ }
+}
diff --git a/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs
index b48e096e3f..f8fc459aec 100644
--- a/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs
@@ -105,12 +105,22 @@ namespace Umbraco.Core.Persistence.Repositories
dirs += "," + SystemDirectories.MvcViews;*/
//Validate file
- var validFile = IOHelper.VerifyEditPath(script.VirtualPath, dirs.Split(','));
+ string fullPath;
+ try
+ {
+ // may throw for security reasons
+ fullPath = FileSystem.GetFullPath(script.Path);
+ }
+ catch
+ {
+ return false;
+ }
+ var isValidPath = IOHelper.VerifyEditPath(fullPath, dirs.Split(','));
//Validate extension
- var validExtension = IOHelper.VerifyFileExtension(script.VirtualPath, exts);
+ var isValidExtension = IOHelper.VerifyFileExtension(script.Path, exts);
- return validFile && validExtension;
+ return isValidPath && isValidExtension;
}
#endregion
diff --git a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs
index fc16009e6c..20d3c38042 100644
--- a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs
@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Rdbms;
-
using Umbraco.Core.Persistence.Factories;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.SqlSyntax;
@@ -15,52 +15,47 @@ namespace Umbraco.Core.Persistence.Repositories
{
internal class ServerRegistrationRepository : PetaPocoRepositoryBase, IServerRegistrationRepository
{
- public ServerRegistrationRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax)
- : base(work, cache, logger, sqlSyntax)
+ private readonly ICacheProvider _staticCache;
+
+ public ServerRegistrationRepository(IDatabaseUnitOfWork work, ICacheProvider staticCache, ILogger logger, ISqlSyntaxProvider sqlSyntax)
+ : base(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax)
{
+ _staticCache = staticCache;
+ }
+
+ protected override int PerformCount(IQuery query)
+ {
+ throw new NotSupportedException("This repository does not support this method");
+ }
+
+ protected override bool PerformExists(int id)
+ {
+ // use the underlying GetAll which force-caches all registrations
+ return GetAll().Any(x => x.Id == id);
}
protected override IServerRegistration PerformGet(int id)
{
- var sql = GetBaseQuery(false);
- sql.Where(GetBaseWhereClause(), new { Id = id });
-
- var serverDto = Database.First(sql);
- if (serverDto == null)
- return null;
-
- var factory = new ServerRegistrationFactory();
- var entity = factory.BuildEntity(serverDto);
-
- //on initial construction we don't want to have dirty properties tracked
- // http://issues.umbraco.org/issue/U4-1946
- entity.ResetDirtyProperties(false);
-
- return entity;
+ // use the underlying GetAll which force-caches all registrations
+ return GetAll().FirstOrDefault(x => x.Id == id);
}
protected override IEnumerable PerformGetAll(params int[] ids)
{
- var factory = new ServerRegistrationFactory();
+ // we do NOT want to populate the cache on-demand, because then it might happen
+ // during a ReadCommited transaction, and reading the registrations under ReadCommited
+ // is NOT safe because they could be updated in the middle of the read.
+ //
+ // the cache is populated by ReloadCache which should only be called from methods
+ // that ensure proper locking (at least, read-lock in ReadCommited) of the repo.
- if (ids.Any())
- {
- return Database.Fetch("WHERE id in (@ids)", new { ids = ids })
- .Select(x => factory.BuildEntity(x));
- }
-
- return Database.Fetch("WHERE id > 0")
- .Select(x => factory.BuildEntity(x));
+ var all = _staticCache.GetCacheItem>(CacheKey, Enumerable.Empty);
+ return ids.Length == 0 ? all : all.Where(x => ids.Contains(x.Id));
}
protected override IEnumerable PerformGetByQuery(IQuery query)
{
- var factory = new ServerRegistrationFactory();
- var sqlClause = GetBaseQuery(false);
- var translator = new SqlTranslator(sqlClause, query);
- var sql = translator.Translate();
-
- return Database.Fetch(sql).Select(x => factory.BuildEntity(x));
+ throw new NotSupportedException("This repository does not support this method");
}
protected override Sql GetBaseQuery(bool isCount)
@@ -101,6 +96,7 @@ namespace Umbraco.Core.Persistence.Repositories
entity.Id = id;
entity.ResetDirtyProperties();
+ ReloadCache();
}
protected override void PersistUpdatedItem(IServerRegistration entity)
@@ -113,13 +109,34 @@ namespace Umbraco.Core.Persistence.Repositories
Database.Update(dto);
entity.ResetDirtyProperties();
+ ReloadCache();
+ }
+
+ public override void PersistDeletedItem(IEntity entity)
+ {
+ base.PersistDeletedItem(entity);
+ ReloadCache();
+ }
+
+ private static readonly string CacheKey = GetCacheTypeKey() + "all";
+
+ public void ReloadCache()
+ {
+ var factory = new ServerRegistrationFactory();
+ var all = Database.Fetch("WHERE id > 0")
+ .Select(x => factory.BuildEntity(x))
+ .Cast()
+ .ToArray();
+ _staticCache.ClearCacheItem(CacheKey);
+ _staticCache.GetCacheItem(CacheKey, () => all);
}
public void DeactiveStaleServers(TimeSpan staleTimeout)
{
var timeoutDate = DateTime.Now.Subtract(staleTimeout);
- Database.Update("SET isActive=0, isMaster=0 WHERE lastNotifiedDate < @timeoutDate", new { timeoutDate = timeoutDate });
+ Database.Update("SET isActive=0, isMaster=0 WHERE lastNotifiedDate < @timeoutDate", new { /*timeoutDate =*/ timeoutDate });
+ ReloadCache();
}
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs
index f4d7dcda29..425f7c8253 100644
--- a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs
@@ -99,19 +99,29 @@ namespace Umbraco.Core.Persistence.Repositories
return FileSystem.GetFiles(rootPath ?? string.Empty, "*.css").Select(Get);
}
+ private static readonly List ValidExtensions = new List { "css" };
+
public bool ValidateStylesheet(Stylesheet stylesheet)
{
var dirs = SystemDirectories.Css;
//Validate file
- var validFile = IOHelper.VerifyEditPath(stylesheet.VirtualPath, dirs.Split(','));
+ string fullPath;
+ try
+ {
+ // may throw for security reasons
+ fullPath = FileSystem.GetFullPath(stylesheet.Path);
+ }
+ catch
+ {
+ return false;
+ }
+ var isValidPath = IOHelper.VerifyEditPath(fullPath, dirs.Split(','));
//Validate extension
- var validExtension = IOHelper.VerifyFileExtension(stylesheet.VirtualPath, new List { "css" });
+ var isValidExtension = IOHelper.VerifyFileExtension(stylesheet.Path, ValidExtensions);
- var fileValid = validFile && validExtension;
-
- return fileValid;
+ return isValidPath && isValidExtension;
}
#endregion
diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs
index bb213eb50f..f9367d8433 100644
--- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs
+++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs
@@ -230,7 +230,7 @@ namespace Umbraco.Core.Persistence
{
return new ServerRegistrationRepository(
uow,
- CacheHelper.CreateDisabledCacheHelper(), //never cache
+ _cacheHelper.StaticCache,
_logger, _sqlSyntax);
}
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index c9df0d685a..3cc59708e9 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -1416,7 +1416,7 @@ namespace Umbraco.Core.Services
///
/// Copies an object by creating a new Content object of the same type and copies all data from the current
- /// to the new copy which is returned.
+ /// to the new copy which is returned. Recursively copies all children.
///
/// The to copy
/// Id of the Content's new Parent
@@ -1424,6 +1424,21 @@ namespace Umbraco.Core.Services
/// Optional Id of the User copying the Content
/// The newly created object
public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0)
+ {
+ return Copy(content, parentId, relateToOriginal, true, userId);
+ }
+
+ ///
+ /// Copies an object by creating a new Content object of the same type and copies all data from the current
+ /// to the new copy which is returned.
+ ///
+ /// The to copy
+ /// Id of the Content's new Parent
+ /// Boolean indicating whether the copy should be related to the original
+ /// A value indicating whether to recursively copy children.
+ /// Optional Id of the User copying the Content
+ /// The newly created object
+ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0)
{
//TODO: This all needs to be managed correctly so that the logic is submitted in one
// transaction, the CRUD needs to be moved to the repo
@@ -1466,13 +1481,16 @@ namespace Umbraco.Core.Services
}
}
- //Look for children and copy those as well
- var children = GetChildren(content.Id);
- foreach (var child in children)
+ if (recursive)
{
- //TODO: This shouldn't recurse back to this method, it should be done in a private method
- // that doesn't have a nested lock and so we can perform the entire operation in one commit.
- Copy(child, copy.Id, relateToOriginal, userId);
+ //Look for children and copy those as well
+ var children = GetChildren(content.Id);
+ foreach (var child in children)
+ {
+ //TODO: This shouldn't recurse back to this method, it should be done in a private method
+ // that doesn't have a nested lock and so we can perform the entire operation in one commit.
+ Copy(child, copy.Id, relateToOriginal, true, userId);
+ }
}
Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this);
diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs
index 949b4e61c7..772009e89a 100644
--- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs
+++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs
@@ -100,7 +100,6 @@ namespace Umbraco.Core.Services
xml.Add(new XAttribute("loginName", member.Username));
xml.Add(new XAttribute("email", member.Email));
- xml.Add(new XAttribute("key", member.Key));
return xml;
}
@@ -440,6 +439,7 @@ namespace Umbraco.Core.Services
var xml = new XElement(nodeName,
new XAttribute("id", contentBase.Id),
+ new XAttribute("key", contentBase.Key),
new XAttribute("parentID", contentBase.Level > 1 ? contentBase.ParentId : -1),
new XAttribute("level", contentBase.Level),
new XAttribute("creatorID", contentBase.CreatorId),
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index 8b308e5f21..c686cf4891 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -481,7 +481,7 @@ namespace Umbraco.Core.Services
///
/// Copies an object by creating a new Content object of the same type and copies all data from the current
- /// to the new copy which is returned.
+ /// to the new copy, which is returned. Recursively copies all children.
///
/// The to copy
/// Id of the Content's new Parent
@@ -490,6 +490,18 @@ namespace Umbraco.Core.Services
/// The newly created object
IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0);
+ ///
+ /// Copies an object by creating a new Content object of the same type and copies all data from the current
+ /// to the new copy which is returned.
+ ///
+ /// The to copy
+ /// Id of the Content's new Parent
+ /// Boolean indicating whether the copy should be related to the original
+ /// A value indicating whether to recursively copy children.
+ /// Optional Id of the User copying the Content
+ /// The newly created object
+ IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0);
+
///
/// Checks if the passed in can be published based on the anscestors publish state.
///
diff --git a/src/Umbraco.Core/Services/ServerRegistrationService.cs b/src/Umbraco.Core/Services/ServerRegistrationService.cs
index 43329ebe5f..5a4a48b7aa 100644
--- a/src/Umbraco.Core/Services/ServerRegistrationService.cs
+++ b/src/Umbraco.Core/Services/ServerRegistrationService.cs
@@ -6,7 +6,6 @@ using Umbraco.Core.Events;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
-using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Sync;
@@ -38,7 +37,6 @@ namespace Umbraco.Core.Services
_lrepo = new LockingRepository(UowProvider,
x => RepositoryFactory.CreateServerRegistrationRepository(x),
LockingRepositoryIds, LockingRepositoryIds);
-
}
///
@@ -51,10 +49,11 @@ namespace Umbraco.Core.Services
{
_lrepo.WithWriteLocked(xr =>
{
- var regs = xr.Repository.GetAll().ToArray(); // faster to query only once
+ ((ServerRegistrationRepository) xr.Repository).ReloadCache(); // ensure we have up-to-date cache
+
+ var regs = xr.Repository.GetAll().ToArray();
var hasMaster = regs.Any(x => ((ServerRegistration)x).IsMaster);
var server = regs.FirstOrDefault(x => x.ServerIdentity.InvariantEquals(serverIdentity));
- var hasServer = server != null;
if (server == null)
{
@@ -71,18 +70,17 @@ namespace Umbraco.Core.Services
server.IsMaster = true;
xr.Repository.AddOrUpdate(server);
- xr.UnitOfWork.Commit();
- xr.Repository.DeactiveStaleServers(staleTimeout);
+ xr.UnitOfWork.Commit(); // triggers a cache reload
+ xr.Repository.DeactiveStaleServers(staleTimeout); // triggers a cache reload
- // default role is single server
- _currentServerRole = ServerRole.Single;
+ // reload - cheap, cached
+ regs = xr.Repository.GetAll().ToArray();
- // if registrations contain more than 0/1 server, role is master or slave
- // compare to 0 or 1 depending on whether regs already contains the server
- if (regs.Length > (hasServer ? 1 : 0))
- _currentServerRole = server.IsMaster
- ? ServerRole.Master
- : ServerRole.Slave;
+ // default role is single server, but if registrations contain more
+ // than one active server, then role is master or slave
+ _currentServerRole = regs.Count(x => x.IsActive) > 1
+ ? (server.IsMaster ? ServerRole.Master : ServerRole.Slave)
+ : ServerRole.Single;
});
}
@@ -92,15 +90,27 @@ namespace Umbraco.Core.Services
/// The server unique identity.
public void DeactiveServer(string serverIdentity)
{
+ //_lrepo.WithWriteLocked(xr =>
+ //{
+ // var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == serverIdentity.ToUpper());
+ // var server = xr.Repository.GetByQuery(query).FirstOrDefault();
+ // if (server == null) return;
+
+ // server.IsActive = false;
+ // server.IsMaster = false;
+ // xr.Repository.AddOrUpdate(server);
+ //});
+
+ // because the repository caches "all" and has queries disabled...
+
_lrepo.WithWriteLocked(xr =>
{
- var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == serverIdentity.ToUpper());
- var server = xr.Repository.GetByQuery(query).FirstOrDefault();
- if (server == null) return;
+ ((ServerRegistrationRepository)xr.Repository).ReloadCache(); // ensure we have up-to-date cache
- server.IsActive = false;
- server.IsMaster = false;
- xr.Repository.AddOrUpdate(server);
+ var server = xr.Repository.GetAll().FirstOrDefault(x => x.ServerIdentity.InvariantEquals(serverIdentity));
+ if (server == null) return;
+ server.IsActive = server.IsMaster = false;
+ xr.Repository.AddOrUpdate(server); // will trigger a cache reload
});
}
@@ -119,11 +129,32 @@ namespace Umbraco.Core.Services
///
public IEnumerable GetActiveServers()
{
- return _lrepo.WithReadLocked(xr =>
- {
- var query = Query.Builder.Where(x => x.IsActive);
- return xr.Repository.GetByQuery(query).ToArray();
- });
+ //return _lrepo.WithReadLocked(xr =>
+ //{
+ // var query = Query.Builder.Where(x => x.IsActive);
+ // return xr.Repository.GetByQuery(query).ToArray();
+ //});
+
+ // because the repository caches "all" we should use the following code
+ // in order to ensure we use the cache and not hit the database each time
+
+ //return _lrepo.WithReadLocked(xr => xr.Repository.GetAll().Where(x => x.IsActive).ToArray());
+
+ // however, WithReadLocked (as any other LockingRepository methods) will attempt
+ // to properly lock the repository using a database-level lock, which wants
+ // the transaction isolation level to be RepeatableRead, which it is not by default,
+ // and then, see U4-7046.
+ //
+ // in addition, LockingRepository methods need to hit the database in order to
+ // ensure proper locking, and so if we know that the repository might not need the
+ // database, we cannot use these methods - and then what?
+ //
+ // this raises a good number of questions, including whether caching anything in
+ // repositories works at all in a LB environment - TODO: figure it out
+
+ var uow = UowProvider.GetUnitOfWork();
+ var repo = RepositoryFactory.CreateServerRegistrationRepository(uow);
+ return repo.GetAll().Where(x => x.IsActive).ToArray(); // fast, cached
}
///
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index f3e6cac1f1..b838db5441 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -364,10 +364,14 @@
+
+
+
+
diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config
index 9599390dfb..aa8ed6b0a7 100644
--- a/src/Umbraco.Tests/App.config
+++ b/src/Umbraco.Tests/App.config
@@ -12,41 +12,41 @@
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs
index d93c4d9e3c..32bbd34a77 100644
--- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs
+++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs
@@ -10,6 +10,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Tests.PublishedContent;
using Umbraco.Tests.TestHelpers;
+using Umbraco.Web;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.PublishedCache.XmlPublishedCache;
@@ -106,6 +107,14 @@ namespace Umbraco.Tests.Cache.PublishedCache
DoAssert(dicDoc);
}
+ [Test]
+ public void DictionaryDocument_Key()
+ {
+ var key = Guid.NewGuid();
+ var dicDoc = GetDictionaryDocument(keyVal: key);
+ DoAssert(dicDoc, keyVal: key);
+ }
+
[Test]
public void DictionaryDocument_Get_Children()
{
@@ -122,10 +131,12 @@ namespace Umbraco.Tests.Cache.PublishedCache
Assert.AreEqual(444555, dicDoc.Children.ElementAt(1).Id);
}
- [Test]
- public void Convert_From_Search_Result()
+ [TestCase(true)]
+ [TestCase(false)]
+ public void Convert_From_Search_Result(bool withKey)
{
var ctx = GetUmbracoContext("/test", 1234);
+ var key = Guid.NewGuid();
var result = new SearchResult()
{
@@ -138,6 +149,7 @@ namespace Umbraco.Tests.Cache.PublishedCache
result.Fields.Add("__Path", "-1,1234");
result.Fields.Add("__nodeName", "Test");
result.Fields.Add("id", "1234");
+ if (withKey) result.Fields.Add("key", key.ToString());
result.Fields.Add("nodeName", "Test");
result.Fields.Add("nodeTypeAlias", Constants.Conventions.MediaTypes.Image);
result.Fields.Add("parentID", "-1");
@@ -148,21 +160,24 @@ namespace Umbraco.Tests.Cache.PublishedCache
var store = new PublishedMediaCache(ctx.Application);
var doc = store.CreateFromCacheValues(store.ConvertFromSearchResult(result));
- DoAssert(doc, 1234, 0, 0, "", "Image", 0, "Shannon", "", 0, 0, "-1,1234", default(DateTime), DateTime.Parse("2012-07-16T10:34:09"), 2);
+ DoAssert(doc, 1234, withKey ? key : default(Guid), 0, 0, "", "Image", 0, "Shannon", "", 0, 0, "-1,1234", default(DateTime), DateTime.Parse("2012-07-16T10:34:09"), 2);
Assert.AreEqual(null, doc.Parent);
}
- [Test]
- public void Convert_From_XPath_Navigator()
+ [TestCase(true)]
+ [TestCase(false)]
+ public void Convert_From_XPath_Navigator(bool withKey)
{
var ctx = GetUmbracoContext("/test", 1234);
+ var key = Guid.NewGuid();
var xmlDoc = GetMediaXml();
+ if (withKey) ((XmlElement)xmlDoc.DocumentElement.FirstChild).SetAttribute("key", key.ToString());
var navigator = xmlDoc.SelectSingleNode("/root/Image").CreateNavigator();
var cache = new PublishedMediaCache(ctx.Application);
var doc = cache.CreateFromCacheValues(cache.ConvertFromXPathNavigator(navigator, true));
- DoAssert(doc, 2000, 0, 2, "image1", "Image", 2044, "Shannon", "Shannon2", 22, 33, "-1,2000", DateTime.Parse("2012-06-12T14:13:17"), DateTime.Parse("2012-07-20T18:50:43"), 1);
+ DoAssert(doc, 2000, withKey ? key : default(Guid), 0, 2, "image1", "Image", 2044, "Shannon", "Shannon2", 22, 33, "-1,2000", DateTime.Parse("2012-06-12T14:13:17"), DateTime.Parse("2012-07-20T18:50:43"), 1);
Assert.AreEqual(null, doc.Parent);
Assert.AreEqual(2, doc.Children.Count());
Assert.AreEqual(2001, doc.Children.ElementAt(0).Id);
@@ -197,6 +212,7 @@ namespace Umbraco.Tests.Cache.PublishedCache
private Dictionary GetDictionary(
int id,
+ Guid key,
int parentId,
string idKey,
string templateKey,
@@ -207,6 +223,7 @@ namespace Umbraco.Tests.Cache.PublishedCache
return new Dictionary()
{
{idKey, id.ToString()},
+ {"key", key.ToString()},
{templateKey, "333"},
{"sortOrder", "44"},
{nodeNameKey, "Testing"},
@@ -232,6 +249,7 @@ namespace Umbraco.Tests.Cache.PublishedCache
string nodeTypeAliasKey = "nodeTypeAlias",
string pathKey = "path",
int idVal = 1234,
+ Guid keyVal = default(Guid),
int parentIdVal = 321,
IEnumerable children = null)
{
@@ -239,10 +257,10 @@ namespace Umbraco.Tests.Cache.PublishedCache
children = new List();
var dicDoc = new PublishedMediaCache.DictionaryPublishedContent(
//the dictionary
- GetDictionary(idVal, parentIdVal, idKey, templateKey, nodeNameKey, nodeTypeAliasKey, pathKey),
+ GetDictionary(idVal, keyVal, parentIdVal, idKey, templateKey, nodeNameKey, nodeTypeAliasKey, pathKey),
//callback to get the parent
d => new PublishedMediaCache.DictionaryPublishedContent(
- GetDictionary(parentIdVal, -1, idKey, templateKey, nodeNameKey, nodeTypeAliasKey, pathKey),
+ GetDictionary(parentIdVal, default(Guid), -1, idKey, templateKey, nodeNameKey, nodeTypeAliasKey, pathKey),
//there is no parent
a => null,
//we're not going to test this so ignore
@@ -261,6 +279,7 @@ namespace Umbraco.Tests.Cache.PublishedCache
private void DoAssert(
PublishedMediaCache.DictionaryPublishedContent dicDoc,
int idVal = 1234,
+ Guid keyVal = default(Guid),
int templateIdVal = 333,
int sortOrderVal = 44,
string urlNameVal = "testing",
@@ -281,7 +300,7 @@ namespace Umbraco.Tests.Cache.PublishedCache
if (!updateDateVal.HasValue)
updateDateVal = DateTime.Parse("2012-01-03");
- DoAssert((IPublishedContent)dicDoc, idVal, templateIdVal, sortOrderVal, urlNameVal, nodeTypeAliasVal, nodeTypeIdVal, writerNameVal,
+ DoAssert((IPublishedContent)dicDoc, idVal, keyVal, templateIdVal, sortOrderVal, urlNameVal, nodeTypeAliasVal, nodeTypeIdVal, writerNameVal,
creatorNameVal, writerIdVal, creatorIdVal, pathVal, createDateVal, updateDateVal, levelVal);
//now validate the parentId that has been parsed, this doesn't exist on the IPublishedContent
@@ -291,7 +310,8 @@ namespace Umbraco.Tests.Cache.PublishedCache
private void DoAssert(
IPublishedContent doc,
int idVal = 1234,
- int templateIdVal = 333,
+ Guid keyVal = default(Guid),
+ int templateIdVal = 333,
int sortOrderVal = 44,
string urlNameVal = "testing",
string nodeTypeAliasVal = "myType",
@@ -311,6 +331,7 @@ namespace Umbraco.Tests.Cache.PublishedCache
updateDateVal = DateTime.Parse("2012-01-03");
Assert.AreEqual(idVal, doc.Id);
+ Assert.AreEqual(keyVal, doc.GetKey());
Assert.AreEqual(templateIdVal, doc.TemplateId);
Assert.AreEqual(sortOrderVal, doc.SortOrder);
Assert.AreEqual(urlNameVal, doc.UrlName);
diff --git a/src/Umbraco.Tests/IO/PhysicalFileSystemTests.cs b/src/Umbraco.Tests/IO/PhysicalFileSystemTests.cs
index 418cb3dda2..4ee178a954 100644
--- a/src/Umbraco.Tests/IO/PhysicalFileSystemTests.cs
+++ b/src/Umbraco.Tests/IO/PhysicalFileSystemTests.cs
@@ -27,6 +27,8 @@ namespace Umbraco.Tests.IO
public void TearDown()
{
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests");
+ if (Directory.Exists(path) == false) return;
+
var files = Directory.GetFiles(path);
foreach (var f in files)
{
@@ -39,5 +41,31 @@ namespace Umbraco.Tests.IO
{
return "/Media/" + path;
}
+
+ [Test]
+ public void GetFullPathTest()
+ {
+ // outside of tests, one initializes the PhysicalFileSystem with eg ~/Dir
+ // and then, rootPath = /path/to/Dir and rootUrl = /Dir/
+ // here we initialize the PhysicalFileSystem with
+ // rootPath = /path/to/FileSysTests
+ // rootUrl = /Media/
+
+ var basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests");
+
+ // ensure that GetFullPath
+ // - does return the proper full path
+ // - does properly normalize separators
+ // - does throw on invalid paths
+
+ var path = _fileSystem.GetFullPath("foo.tmp");
+ Assert.AreEqual(Path.Combine(basePath, @"foo.tmp"), path);
+
+ path = _fileSystem.GetFullPath("foo/bar.tmp");
+ Assert.AreEqual(Path.Combine(basePath, @"foo\bar.tmp"), path);
+
+ // that path is invalid as it would be outside the root directory
+ Assert.Throws(() => _fileSystem.GetFullPath("../../foo.tmp"));
+ }
}
}
diff --git a/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs
index b94339970f..bcb49068fa 100644
--- a/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs
+++ b/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
+using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
@@ -18,17 +19,20 @@ namespace Umbraco.Tests.Persistence.Repositories
[TestFixture]
public class ServerRegistrationRepositoryTest : BaseDatabaseFactoryTest
{
+ private ICacheProvider _staticCache;
+
[SetUp]
public override void Initialize()
{
base.Initialize();
+ _staticCache = new StaticCacheProvider();
CreateTestData();
}
- private ServerRegistrationRepository CreateRepositor(IDatabaseUnitOfWork unitOfWork)
+ private ServerRegistrationRepository CreateRepository(IDatabaseUnitOfWork unitOfWork)
{
- return new ServerRegistrationRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax);
+ return new ServerRegistrationRepository(unitOfWork, _staticCache, Mock.Of(), SqlSyntax);
}
[Test]
@@ -39,7 +43,7 @@ namespace Umbraco.Tests.Persistence.Repositories
var unitOfWork = provider.GetUnitOfWork();
// Act
- using (var repository = CreateRepositor(unitOfWork))
+ using (var repository = CreateRepository(unitOfWork))
{
var server = new ServerRegistration("http://shazwazza.com", "COMPUTER1", DateTime.Now);
repository.AddOrUpdate(server);
@@ -57,7 +61,7 @@ namespace Umbraco.Tests.Persistence.Repositories
var unitOfWork = provider.GetUnitOfWork();
// Act
- using (var repository = CreateRepositor(unitOfWork))
+ using (var repository = CreateRepository(unitOfWork))
{
var server = repository.Get(1);
server.ServerIdentity = "COMPUTER2";
@@ -75,7 +79,7 @@ namespace Umbraco.Tests.Persistence.Repositories
var unitOfWork = provider.GetUnitOfWork();
// Act
- using (var repository = CreateRepositor(unitOfWork))
+ using (var repository = CreateRepository(unitOfWork))
{
// Assert
Assert.That(repository, Is.Not.Null);
@@ -88,7 +92,7 @@ namespace Umbraco.Tests.Persistence.Repositories
// Arrange
var provider = new PetaPocoUnitOfWorkProvider(Logger);
var unitOfWork = provider.GetUnitOfWork();
- using (var repository = CreateRepositor(unitOfWork))
+ using (var repository = CreateRepository(unitOfWork))
{
// Act
var server = repository.Get(1);
@@ -108,7 +112,7 @@ namespace Umbraco.Tests.Persistence.Repositories
// Arrange
var provider = new PetaPocoUnitOfWorkProvider(Logger);
var unitOfWork = provider.GetUnitOfWork();
- using (var repository = CreateRepositor(unitOfWork))
+ using (var repository = CreateRepository(unitOfWork))
{
// Act
var servers = repository.GetAll();
@@ -119,39 +123,41 @@ namespace Umbraco.Tests.Persistence.Repositories
}
- [Test]
- public void Can_Perform_GetByQuery_On_Repository()
- {
- // Arrange
- var provider = new PetaPocoUnitOfWorkProvider(Logger);
- var unitOfWork = provider.GetUnitOfWork();
- using (var repository = CreateRepositor(unitOfWork))
- {
- // Act
- var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == "COMPUTER3");
- var result = repository.GetByQuery(query);
+ // queries are not supported due to in-memory caching
- // Assert
- Assert.AreEqual(1, result.Count());
- }
- }
+ //[Test]
+ //public void Can_Perform_GetByQuery_On_Repository()
+ //{
+ // // Arrange
+ // var provider = new PetaPocoUnitOfWorkProvider(Logger);
+ // var unitOfWork = provider.GetUnitOfWork();
+ // using (var repository = CreateRepository(unitOfWork))
+ // {
+ // // Act
+ // var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == "COMPUTER3");
+ // var result = repository.GetByQuery(query);
- [Test]
- public void Can_Perform_Count_On_Repository()
- {
- // Arrange
- var provider = new PetaPocoUnitOfWorkProvider(Logger);
- var unitOfWork = provider.GetUnitOfWork();
- using (var repository = CreateRepositor(unitOfWork))
- {
- // Act
- var query = Query.Builder.Where(x => x.ServerAddress.StartsWith("http://"));
- int count = repository.Count(query);
+ // // Assert
+ // Assert.AreEqual(1, result.Count());
+ // }
+ //}
- // Assert
- Assert.That(count, Is.EqualTo(2));
- }
- }
+ //[Test]
+ //public void Can_Perform_Count_On_Repository()
+ //{
+ // // Arrange
+ // var provider = new PetaPocoUnitOfWorkProvider(Logger);
+ // var unitOfWork = provider.GetUnitOfWork();
+ // using (var repository = CreateRepository(unitOfWork))
+ // {
+ // // Act
+ // var query = Query.Builder.Where(x => x.ServerAddress.StartsWith("http://"));
+ // int count = repository.Count(query);
+
+ // // Assert
+ // Assert.That(count, Is.EqualTo(2));
+ // }
+ //}
[Test]
public void Can_Perform_Add_On_Repository()
@@ -159,7 +165,7 @@ namespace Umbraco.Tests.Persistence.Repositories
// Arrange
var provider = new PetaPocoUnitOfWorkProvider(Logger);
var unitOfWork = provider.GetUnitOfWork();
- using (var repository = CreateRepositor(unitOfWork))
+ using (var repository = CreateRepository(unitOfWork))
{
// Act
var server = new ServerRegistration("http://shazwazza.com", "COMPUTER4", DateTime.Now);
@@ -178,7 +184,7 @@ namespace Umbraco.Tests.Persistence.Repositories
// Arrange
var provider = new PetaPocoUnitOfWorkProvider(Logger);
var unitOfWork = provider.GetUnitOfWork();
- using (var repository = CreateRepositor(unitOfWork))
+ using (var repository = CreateRepository(unitOfWork))
{
// Act
var server = repository.Get(2);
@@ -203,7 +209,7 @@ namespace Umbraco.Tests.Persistence.Repositories
// Arrange
var provider = new PetaPocoUnitOfWorkProvider(Logger);
var unitOfWork = provider.GetUnitOfWork();
- using (var repository = CreateRepositor(unitOfWork))
+ using (var repository = CreateRepository(unitOfWork))
{
// Act
var server = repository.Get(3);
@@ -224,7 +230,7 @@ namespace Umbraco.Tests.Persistence.Repositories
// Arrange
var provider = new PetaPocoUnitOfWorkProvider(Logger);
var unitOfWork = provider.GetUnitOfWork();
- using (var repository = CreateRepositor(unitOfWork))
+ using (var repository = CreateRepository(unitOfWork))
{
// Act
var exists = repository.Exists(3);
@@ -246,7 +252,7 @@ namespace Umbraco.Tests.Persistence.Repositories
{
var provider = new PetaPocoUnitOfWorkProvider(Logger);
using (var unitOfWork = provider.GetUnitOfWork())
- using (var repository = new ServerRegistrationRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax))
+ using (var repository = CreateRepository(unitOfWork))
{
repository.AddOrUpdate(new ServerRegistration("http://localhost", "COMPUTER1", DateTime.Now) { IsActive = true });
repository.AddOrUpdate(new ServerRegistration("http://www.mydomain.com", "COMPUTER2", DateTime.Now));
diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs
index 75846fb404..2877e3bb90 100644
--- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs
+++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs
@@ -21,6 +21,16 @@ namespace Umbraco.Tests.PropertyEditors
Assert.AreEqual(mediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString);
}
+ ///
+ /// Test to ensure useCropDimensions is observed
+ ///
+ [Test]
+ public void GetCropUrl_CropAliasIgnoreWidthHeightTest()
+ {
+ var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "Thumb", useCropDimensions: true, width: 50, height: 50);
+ Assert.AreEqual(mediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString);
+ }
+
[Test]
public void GetCropUrl_WidthHeightTest()
{
@@ -124,5 +134,77 @@ namespace Umbraco.Tests.PropertyEditors
var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, width: 300, height: 150, preferFocalPoint:true);
Assert.AreEqual(mediaPath + "?anchor=center&mode=crop&width=300&height=150", urlString);
}
+
+ ///
+ /// Test to check if height ratio is returned for a predefined crop without coordinates and focal point in centre when a width parameter is passed
+ ///
+ [Test]
+ public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidth()
+ {
+ var cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}";
+
+ var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", width: 200);
+ Assert.AreEqual(mediaPath + "?anchor=center&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString);
+ }
+
+ ///
+ /// Test to check if height ratio is returned for a predefined crop without coordinates and focal point is custom when a width parameter is passed
+ ///
+ [Test]
+ public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPoint()
+ {
+ var cropperJson = "{\"focalPoint\": {\"left\": 0.4275,\"top\": 0.41},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}";
+
+ var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", width: 200);
+ Assert.AreEqual(mediaPath + "?center=0.41,0.4275&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString);
+ }
+
+ ///
+ /// Test to check if crop ratio is ignored if useCropDimensions is true
+ ///
+ [Test]
+ public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore()
+ {
+ var cropperJson = "{\"focalPoint\": {\"left\": 0.4275,\"top\": 0.41},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}";
+
+ var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", width: 200, useCropDimensions: true);
+ Assert.AreEqual(mediaPath + "?center=0.41,0.4275&mode=crop&width=270&height=161", urlString);
+ }
+
+ ///
+ /// Test to check if width ratio is returned for a predefined crop without coordinates and focal point in centre when a height parameter is passed
+ ///
+ [Test]
+ public void GetCropUrl_PreDefinedCropNoCoordinatesWithHeight()
+ {
+ var cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}";
+
+ var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", height: 200);
+ Assert.AreEqual(mediaPath + "?anchor=center&mode=crop&widthratio=1.6770186335403726708074534161&height=200", urlString);
+ }
+
+ ///
+ /// Test to check result when only a width parameter is passed, effectivly a resize only
+ ///
+ [Test]
+ public void GetCropUrl_WidthOnlyParameter()
+ {
+ var cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}";
+
+ var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, width: 200);
+ Assert.AreEqual(mediaPath + "?anchor=center&mode=crop&width=200", urlString);
+ }
+
+ ///
+ /// Test to check result when only a height parameter is passed, effectivly a resize only
+ ///
+ [Test]
+ public void GetCropUrl_HeightOnlyParameter()
+ {
+ var cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}";
+
+ var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, height: 200);
+ Assert.AreEqual(mediaPath + "?anchor=center&mode=crop&height=200", urlString);
+ }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Tests/Services/BaseServiceTest.cs b/src/Umbraco.Tests/Services/BaseServiceTest.cs
index b711f7c3ac..ebd0be3b55 100644
--- a/src/Umbraco.Tests/Services/BaseServiceTest.cs
+++ b/src/Umbraco.Tests/Services/BaseServiceTest.cs
@@ -51,7 +51,6 @@ namespace Umbraco.Tests.Services
Content trashed = MockedContent.CreateSimpleContent(contentType, "Text Page Deleted", -20);
trashed.Trashed = true;
ServiceContext.ContentService.Save(trashed, 0);
-
- }
+ }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs
index 176db015f8..841789a980 100644
--- a/src/Umbraco.Tests/Services/ContentServiceTests.cs
+++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs
@@ -1220,6 +1220,52 @@ namespace Umbraco.Tests.Services
//Assert.AreNotEqual(content.Name, copy.Name);
}
+ [Test]
+ public void Can_Copy_Recursive()
+ {
+ // Arrange
+ var contentService = ServiceContext.ContentService;
+ var temp = contentService.GetById(NodeDto.NodeIdSeed + 1);
+ Assert.AreEqual("Home", temp.Name);
+ Assert.AreEqual(2, temp.Children().Count());
+
+ // Act
+ var copy = contentService.Copy(temp, temp.ParentId, false, true, 0);
+ var content = contentService.GetById(NodeDto.NodeIdSeed + 1);
+
+ // Assert
+ Assert.That(copy, Is.Not.Null);
+ Assert.That(copy.Id, Is.Not.EqualTo(content.Id));
+ Assert.AreNotSame(content, copy);
+ Assert.AreEqual(2, copy.Children().Count());
+
+ var child = contentService.GetById(NodeDto.NodeIdSeed + 2);
+ var childCopy = copy.Children().First();
+ Assert.AreEqual(childCopy.Name, child.Name);
+ Assert.AreNotEqual(childCopy.Id, child.Id);
+ Assert.AreNotEqual(childCopy.Key, child.Key);
+ }
+
+ [Test]
+ public void Can_Copy_NonRecursive()
+ {
+ // Arrange
+ var contentService = ServiceContext.ContentService;
+ var temp = contentService.GetById(NodeDto.NodeIdSeed + 1);
+ Assert.AreEqual("Home", temp.Name);
+ Assert.AreEqual(2, temp.Children().Count());
+
+ // Act
+ var copy = contentService.Copy(temp, temp.ParentId, false, false, 0);
+ var content = contentService.GetById(NodeDto.NodeIdSeed + 1);
+
+ // Assert
+ Assert.That(copy, Is.Not.Null);
+ Assert.That(copy.Id, Is.Not.EqualTo(content.Id));
+ Assert.AreNotSame(content, copy);
+ Assert.AreEqual(0, copy.Children().Count());
+ }
+
[Test]
public void Can_Copy_Content_With_Tags()
{
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js
index 8e58535c74..fcc8e8c2ed 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js
@@ -9,7 +9,7 @@
*/
function LegacyController($scope, $routeParams, $element) {
- var url = decodeURIComponent($routeParams.url.toLowerCase().replace(/javascript\:/g, ""));
+ var url = decodeURIComponent($routeParams.url.replace(/javascript\:/gi, ""));
//split into path and query
var urlParts = url.split("?");
var extIndex = urlParts[0].lastIndexOf(".");
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js
index e439d0e37f..4a19ea9926 100644
--- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js
@@ -43,7 +43,7 @@ function examineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeo
umbRequestHelper.resourcePromise(
$http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetSearchResults", {
searcherName: searcher.name,
- query: searcher.searchText,
+ query: encodeURIComponent(searcher.searchText),
queryType: searcher.searchType
})),
'Failed to search')
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js
index edb7f768d1..628112903d 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js
@@ -21,7 +21,7 @@ angular.module("umbraco")
};
$scope.percentage = function(spans){
- return ((spans / $scope.columns) * 100).toFixed(1);
+ return ((spans / $scope.columns) * 100).toFixed(8);
};
$scope.toggleCollection = function(collection, toggle){
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js
index 70f9951de5..7f7b817b8a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js
@@ -19,7 +19,7 @@ function RowConfigController($scope) {
};
$scope.percentage = function(spans) {
- return ((spans / $scope.columns) * 100).toFixed(1);
+ return ((spans / $scope.columns) * 100).toFixed(8);
};
$scope.toggleCollection = function(collection, toggle) {
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js
index 0e794c29d8..69a85957d9 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js
@@ -14,7 +14,8 @@ angular.module("umbraco")
$scope.control.value = {
focalPoint: data.focalPoint,
id: data.id,
- image: data.image
+ image: data.image,
+ altText: data.altText
};
$scope.setUrl();
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js
index 9428c64704..24aa722cc3 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js
@@ -424,7 +424,7 @@ angular.module("umbraco")
};
$scope.percentage = function (spans) {
- return ((spans / $scope.model.config.items.columns) * 100).toFixed(1);
+ return ((spans / $scope.model.config.items.columns) * 100).toFixed(8);
};
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js
index 229a7992f5..bfe5d1ba2e 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js
@@ -161,7 +161,7 @@ angular.module("umbraco")
};
$scope.percentage = function(spans){
- return ((spans / $scope.model.value.columns) * 100).toFixed(1);
+ return ((spans / $scope.model.value.columns) * 100).toFixed(8);
};
$scope.zeroWidthFilter = function (cell) {
diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
index e8d7ea6d99..39258f6b1a 100644
--- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
+++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
@@ -135,13 +135,13 @@
False
..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll
-
- False
- ..\packages\ImageProcessor.1.9.5.0\lib\ImageProcessor.dll
+
+ ..\packages\ImageProcessor.2.2.8.0\lib\net45\ImageProcessor.dll
+ True
-
- False
- ..\packages\ImageProcessor.Web.3.3.1.0\lib\net45\ImageProcessor.Web.dll
+
+ ..\packages\ImageProcessor.Web.4.3.6.0\lib\net45\ImageProcessor.Web.dll
+ True
False
diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml
index 09d04219f2..f5dfc6459c 100644
--- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml
+++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml
@@ -14,7 +14,7 @@
}
}
-
+
if (Model.value.caption != null)
{
diff --git a/src/Umbraco.Web.UI/config/log4net.config b/src/Umbraco.Web.UI/config/log4net.config
index aa96f5a26a..497fd4471f 100644
--- a/src/Umbraco.Web.UI/config/log4net.config
+++ b/src/Umbraco.Web.UI/config/log4net.config
@@ -2,7 +2,7 @@
-
+
diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config
index 84ee708179..efab0db7da 100644
--- a/src/Umbraco.Web.UI/packages.config
+++ b/src/Umbraco.Web.UI/packages.config
@@ -5,8 +5,8 @@
-
-
+
+
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
index 1ebcf1b90d..54818e4477 100644
--- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
@@ -841,6 +841,10 @@ To manage your website, simply open the Umbraco back office and start adding con
Partial view saved without any errors!
Partial view not saved
An error occurred saving the file.
+ Script view saved
+ Script view saved without any errors!
+ Script view not saved
+ An error occurred saving the file.
Uses CSS syntax ex: h1, .redHeader, .blueTex
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
index f8a362d4f1..3548526c83 100644
--- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
@@ -842,6 +842,10 @@ To manage your website, simply open the Umbraco back office and start adding con
Partial view saved without any errors!
Partial view not saved
An error occurred saving the file.
+ Script view saved
+ Script view saved without any errors!
+ Script view not saved
+ An error occurred saving the file.
Uses CSS syntax ex: h1, .redHeader, .blueTex
diff --git a/src/Umbraco.Web.UI/umbraco/settings/scripts/editScript.aspx b/src/Umbraco.Web.UI/umbraco/settings/scripts/editScript.aspx
index e68922bd98..be0d68d3ba 100644
--- a/src/Umbraco.Web.UI/umbraco/settings/scripts/editScript.aspx
+++ b/src/Umbraco.Web.UI/umbraco/settings/scripts/editScript.aspx
@@ -1,6 +1,7 @@
<%@ Page Language="C#" MasterPageFile="../../masterpages/umbracoPage.Master" AutoEventWireup="true"
CodeBehind="editScript.aspx.cs" Inherits="umbraco.cms.presentation.settings.scripts.editScript"
ValidateRequest="False" %>
+<%@ Import Namespace="Umbraco.Core" %>
<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %>
<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %>
@@ -21,13 +22,10 @@
nameTxtBox: $('#<%= NameTxt.ClientID %>'),
originalFileName: '<%= NameTxt.Text %>',
saveButton: $("#<%= ((Control)SaveButton).ClientID %>"),
+ restServiceLocation: "<%= Url.GetSaveFileServicePath() %>",
editorSourceElement: $('#<%= editorSource.ClientID %>'),
- text: {
- fileErrorHeader: '<%= HttpUtility.JavaScriptStringEncode(umbraco.ui.Text("speechBubbles", "fileErrorHeader")) %>',
- fileSavedHeader: '<%= HttpUtility.JavaScriptStringEncode(umbraco.ui.Text("speechBubbles", "fileSavedHeader")) %>',
- fileSavedText: '',
- fileErrorText: '<%= HttpUtility.JavaScriptStringEncode(umbraco.ui.Text("speechBubbles", "fileErrorText")) %>',
- }
+ treeSyncPath: "<%= ScriptTreeSyncPath %>",
+ lttPathElement: $('#<%= lttPath.ClientID %>')
});
editor.init();
diff --git a/src/Umbraco.Web.UI/umbraco_client/Editors/EditScript.js b/src/Umbraco.Web.UI/umbraco_client/Editors/EditScript.js
index 7c66dc9a1d..a286eed582 100644
--- a/src/Umbraco.Web.UI/umbraco_client/Editors/EditScript.js
+++ b/src/Umbraco.Web.UI/umbraco_client/Editors/EditScript.js
@@ -30,63 +30,69 @@
doSubmit: function () {
var self = this;
- var fileName = this._opts.nameTxtBox.val();
- var codeVal = this._opts.editorSourceElement.val();
+ var filename = this._opts.nameTxtBox.val();
+ var codeval = this._opts.editorSourceElement.val();
//if CodeMirror is not defined, then the code editor is disabled.
if (typeof (CodeMirror) != "undefined") {
- codeVal = UmbEditor.GetCode();
+ codeval = UmbEditor.GetCode();
}
- umbraco.presentation.webservices.codeEditorSave.SaveScript(
- fileName, self._opts.originalFileName, codeVal,
- function (t) { self.submitSucces(t); },
- function (t) { self.submitFailure(t); });
+ this.save(
+ filename,
+ self._opts.originalFileName,
+ codeval);
},
- submitSucces: function(t) {
+ save: function (filename, oldName, contents) {
+ var self = this;
- if (t != 'true') {
- top.UmbSpeechBubble.ShowMessage('error', unescape(this._opts.text.fileErrorHeader), unescape(this._opts.text.fileErrorText));
+ $.post(self._opts.restServiceLocation + "SaveScript",
+ JSON.stringify({
+ filename: filename,
+ oldName: oldName,
+ contents: contents
+ }),
+ function (e) {
+ if (e.success) {
+ self.submitSuccess(e);
+ } else {
+ self.submitFailure(e.message, e.header);
+ }
+ });
+ },
+
+ submitSuccess: function(args) {
+ var msg = args.message;
+ var header = args.header;
+ var path = this._opts.treeSyncPath;
+ var pathChanged = false;
+ if (args.path) {
+ if (path != args.path) {
+ pathChanged = true;
+ }
+ path = args.path;
+ }
+ if (args.contents) {
+ UmbEditor.SetCode(args.contents);
}
- var newFilePath = this._opts.nameTxtBox.val();
-
- //if the filename changes, we need to redirect since the file name is used in the url
- if (this._opts.originalFileName != newFilePath) {
- var newLocation = window.location.pathname + "?" + "&file=" + newFilePath;
-
- UmbClientMgr.contentFrame(newLocation);
-
- //we need to do this after we navigate otherwise the navigation will wait unti lthe message timeout is done!
- top.UmbSpeechBubble.ShowMessage('save', unescape(this._opts.text.fileSavedHeader), unescape(this._opts.text.fileSavedText));
+ top.UmbSpeechBubble.ShowMessage("save", header, msg);
+ UmbClientMgr.mainTree().setActiveTreeType("scripts");
+ if (pathChanged) {
+ UmbClientMgr.mainTree().moveNode(this._opts.originalFileName, path);
+ this._opts.treeSyncPath = args.path;
+ this._opts.lttPathElement.prop("href", args.url).html(args.url);
}
else {
-
- top.UmbSpeechBubble.ShowMessage('save', unescape(this._opts.text.fileSavedHeader), unescape(this._opts.text.fileSavedText));
- UmbClientMgr.mainTree().setActiveTreeType('scripts');
-
- //we need to create a list of ids for each folder/file. Each folder/file's id is it's full path so we need to build each one.
- var paths = [];
- var parts = this._opts.originalFileName.split('/');
- for (var i = 0;i < parts.length;i++) {
- if (paths.length > 0) {
- paths.push(paths[i - 1] + "/" + parts[i]);
- }
- else {
- paths.push(parts[i]);
- }
- }
-
- //we need to pass in the newId parameter so it knows which node to resync after retreival from the server
- UmbClientMgr.mainTree().syncTree("-1,init," + paths.join(','), true, null, newFilePath);
- //set the original file path to the new one
- this._opts.originalFileName = newFilePath;
+ UmbClientMgr.mainTree().syncTree(path, true);
}
-
+
+ this._opts.lttPathElement.prop("href", args.url).html(args.url);
+ this._opts.originalFileName = args.name;
},
- submitFailure: function(t) {
- top.UmbSpeechBubble.ShowMessage('error', unescape(this._opts.text.fileErrorHeader), unescape(this._opts.text.fileErrorText));
+ submitFailure: function(err, header) {
+ top.UmbSpeechBubble.ShowMessage('error', header, err);
}
});
})(jQuery);
\ No newline at end of file
diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs
index c20f330e13..714b79514d 100644
--- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs
+++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs
@@ -45,22 +45,16 @@ namespace Umbraco.Web
private void UmbracoModule_RouteAttempt(object sender, RoutableAttemptEventArgs e)
{
+ // as long as umbraco is ready & configured, sync
switch (e.Outcome)
{
case EnsureRoutableOutcome.IsRoutable:
- Sync();
- break;
case EnsureRoutableOutcome.NotDocumentRequest:
- //so it's not a document request, we'll check if it's a back office request
- if (e.HttpContext.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath))
- {
- //it's a back office request, we should sync!
- Sync();
- }
+ case EnsureRoutableOutcome.NoContent:
+ Sync();
break;
//case EnsureRoutableOutcome.NotReady:
//case EnsureRoutableOutcome.NotConfigured:
- //case EnsureRoutableOutcome.NoContent:
//default:
// break;
}
diff --git a/src/Umbraco.Web/ImageCropperBaseExtensions.cs b/src/Umbraco.Web/ImageCropperBaseExtensions.cs
index cceac8ab31..b870335c91 100644
--- a/src/Umbraco.Web/ImageCropperBaseExtensions.cs
+++ b/src/Umbraco.Web/ImageCropperBaseExtensions.cs
@@ -81,6 +81,7 @@
{
return null;
}
+
if ((preferFocalPoint && cropDataSet.HasFocalPoint()) || (crop != null && crop.Coordinates == null && cropDataSet.HasFocalPoint()) || (string.IsNullOrEmpty(cropAlias) && cropDataSet.HasFocalPoint()))
{
cropUrl.Append("?center=" + cropDataSet.FocalPoint.Top.ToString(CultureInfo.InvariantCulture) + "," + cropDataSet.FocalPoint.Left.ToString(CultureInfo.InvariantCulture));
@@ -100,6 +101,7 @@
cropUrl.Append("?anchor=center");
cropUrl.Append("&mode=crop");
}
+
return cropUrl.ToString();
}
}
diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs
index de0f8a225e..a76b39f187 100644
--- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs
+++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs
@@ -110,9 +110,8 @@
bool useCropDimensions = false,
bool cacheBuster = true,
string furtherOptions = null,
- ImageCropRatioMode? ratioMode = null,
- bool upScale = true
- )
+ ImageCropRatioMode? ratioMode = null,
+ bool upScale = true)
{
string imageCropperValue = null;
@@ -153,6 +152,12 @@
///
/// The height of the output image.
///
+ ///
+ /// The Json data from the Umbraco Core Image Cropper property editor
+ ///
+ ///
+ /// The crop alias.
+ ///
///
/// Quality percentage of the output image.
///
@@ -162,17 +167,11 @@
///
/// The image crop anchor.
///
- ///
- /// The Json data from the Umbraco Core Image Cropper property editor
- ///
- ///
- /// The crop alias.
- ///
///
/// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one
///
///
- /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters>.
+ /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters
///
///
/// Add a serialised date of the last edit of the item to ensure client cache refresh when updated
@@ -182,10 +181,10 @@
///
///
/// Use a dimension as a ratio
- ///
+ ///
///
/// If the image should be upscaled to requested dimensions
- ///
+ ///
///
/// The .
///
@@ -203,12 +202,11 @@
string cacheBusterValue = null,
string furtherOptions = null,
ImageCropRatioMode? ratioMode = null,
- bool upScale = true
- )
+ bool upScale = true)
{
if (string.IsNullOrEmpty(imageUrl) == false)
{
- var imageResizerUrl = new StringBuilder();
+ var imageProcessorUrl = new StringBuilder();
if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null))
{
@@ -217,95 +215,111 @@
{
var crop = cropDataSet.GetCrop(cropAlias);
- imageResizerUrl.Append(cropDataSet.Src);
+ imageProcessorUrl.Append(cropDataSet.Src);
var cropBaseUrl = cropDataSet.GetCropBaseUrl(cropAlias, preferFocalPoint);
if (cropBaseUrl != null)
{
- imageResizerUrl.Append(cropBaseUrl);
+ imageProcessorUrl.Append(cropBaseUrl);
}
else
{
return null;
}
- if (crop!= null & useCropDimensions)
+ if (crop != null & useCropDimensions)
{
width = crop.Width;
height = crop.Height;
}
+
+ // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height
+ if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null)
+ {
+ var heightRatio = (decimal)crop.Height / (decimal)crop.Width;
+ imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture));
+ }
+
+ // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width
+ if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null)
+ {
+ var widthRatio = (decimal)crop.Width / (decimal)crop.Height;
+ imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture));
+ }
}
}
else
{
- imageResizerUrl.Append(imageUrl);
+ imageProcessorUrl.Append(imageUrl);
if (imageCropMode == null)
{
imageCropMode = ImageCropMode.Pad;
}
- imageResizerUrl.Append("?mode=" + imageCropMode.ToString().ToLower());
+ imageProcessorUrl.Append("?mode=" + imageCropMode.ToString().ToLower());
if (imageCropAnchor != null)
{
- imageResizerUrl.Append("&anchor=" + imageCropAnchor.ToString().ToLower());
+ imageProcessorUrl.Append("&anchor=" + imageCropAnchor.ToString().ToLower());
}
}
if (quality != null)
{
- imageResizerUrl.Append("&quality=" + quality);
+ imageProcessorUrl.Append("&quality=" + quality);
}
if (width != null && ratioMode != ImageCropRatioMode.Width)
{
- imageResizerUrl.Append("&width=" + width);
+ imageProcessorUrl.Append("&width=" + width);
}
if (height != null && ratioMode != ImageCropRatioMode.Height)
{
- imageResizerUrl.Append("&height=" + height);
+ imageProcessorUrl.Append("&height=" + height);
}
if (ratioMode == ImageCropRatioMode.Width && height != null)
{
- //if only height specified then assume a sqaure
+ // if only height specified then assume a sqaure
if (width == null)
{
width = height;
}
- var widthRatio = (decimal)width/(decimal)height;
- imageResizerUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture));
+
+ var widthRatio = (decimal)width / (decimal)height;
+ imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture));
}
if (ratioMode == ImageCropRatioMode.Height && width != null)
{
- //if only width specified then assume a sqaure
+ // if only width specified then assume a sqaure
if (height == null)
{
height = width;
}
- var heightRatio = (decimal)height/(decimal)width;
- imageResizerUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture));
+
+ var heightRatio = (decimal)height / (decimal)width;
+ imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture));
}
if (upScale == false)
{
- imageResizerUrl.Append("&upscale=false");
+ imageProcessorUrl.Append("&upscale=false");
}
if (furtherOptions != null)
{
- imageResizerUrl.Append(furtherOptions);
+ imageProcessorUrl.Append(furtherOptions);
}
if (cacheBusterValue != null)
{
- imageResizerUrl.Append("&rnd=").Append(cacheBusterValue);
+ imageProcessorUrl.Append("&rnd=").Append(cacheBusterValue);
}
- return imageResizerUrl.ToString();
+ return imageProcessorUrl.ToString();
}
return string.Empty;
diff --git a/src/Umbraco.Web/Models/PublishedContentWithKeyBase.cs b/src/Umbraco.Web/Models/PublishedContentWithKeyBase.cs
new file mode 100644
index 0000000000..52bd8b2f59
--- /dev/null
+++ b/src/Umbraco.Web/Models/PublishedContentWithKeyBase.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Diagnostics;
+using Umbraco.Core.Models;
+
+namespace Umbraco.Web.Models
+{
+ ///
+ /// Provide an abstract base class for IPublishedContent implementations.
+ ///
+ [DebuggerDisplay("Content Id: {Id}, Name: {Name}")]
+ public abstract class PublishedContentWithKeyBase : PublishedContentBase, IPublishedContentWithKey
+ {
+ public abstract Guid Key { get; }
+ }
+}
diff --git a/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs b/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs
index 5c0050c2a1..35b3d6ee62 100644
--- a/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs
+++ b/src/Umbraco.Web/PublishedCache/MemberPublishedContent.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Web.PublishedCache
///
/// Exposes a member object as IPublishedContent
///
- public sealed class MemberPublishedContent : PublishedContentBase
+ public sealed class MemberPublishedContent : PublishedContentWithKeyBase
{
private readonly IMember _member;
@@ -150,6 +150,11 @@ namespace Umbraco.Web.PublishedCache
get { return _member.Id; }
}
+ public override Guid Key
+ {
+ get { return _member.Key; }
+ }
+
public override int TemplateId
{
get { throw new NotSupportedException(); }
diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs
index 2e437ccc97..61dd50e610 100644
--- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs
+++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs
@@ -273,6 +273,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
values.Add("level", values["__Path"].Split(',').Length.ToString());
}
+ // because, migration
+ if (values.ContainsKey("key") == false)
+ values["key"] = Guid.Empty.ToString();
+
return new CacheValues
{
Values = values,
@@ -321,6 +325,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
result.Current.MoveToParent();
}
}
+ // because, migration
+ if (values.ContainsKey("key") == false)
+ values["key"] = Guid.Empty.ToString();
//add the user props
while (result.MoveNext())
{
@@ -526,7 +533,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
/// This is a helper class and definitely not intended for public use, it expects that all of the values required
/// to create an IPublishedContent exist in the dictionary by specific aliases.
///
- internal class DictionaryPublishedContent : PublishedContentBase
+ internal class DictionaryPublishedContent : PublishedContentWithKeyBase
{
// note: I'm not sure this class fully complies with IPublishedContent rules especially
// I'm not sure that _properties contains all properties including those without a value,
@@ -534,7 +541,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
// List of properties that will appear in the XML and do not match
// anything in the ContentType, so they must be ignored.
- private static readonly string[] IgnoredKeys = { "version", "isDoc", "key" };
+ private static readonly string[] IgnoredKeys = { "version", "isDoc" };
public DictionaryPublishedContent(
IDictionary valueDictionary,
@@ -555,6 +562,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
LoadedFromExamine = fromExamine;
ValidateAndSetProperty(valueDictionary, val => _id = int.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int!
+ ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key");
// wtf are we dealing with templates for medias?!
ValidateAndSetProperty(valueDictionary, val => _templateId = int.Parse(val), "template", "templateId");
ValidateAndSetProperty(valueDictionary, val => _sortOrder = int.Parse(val), "sortOrder");
@@ -664,6 +672,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
get { return _id; }
}
+ public override Guid Key { get { return _key; } }
+
public override int TemplateId
{
get
@@ -803,6 +813,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
private readonly List _keysAdded = new List();
private int _id;
+ private Guid _key;
private int _templateId;
private int _sortOrder;
private string _name;
diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs
index 5450d4063f..4bc2f2388a 100644
--- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs
+++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
///
[Serializable]
[XmlType(Namespace = "http://umbraco.org/webservices/")]
- internal class XmlPublishedContent : PublishedContentBase
+ internal class XmlPublishedContent : PublishedContentWithKeyBase
{
///
/// Initializes a new instance of the XmlPublishedContent class with an Xml node.
@@ -64,6 +64,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
private IPublishedContent _parent;
private int _id;
+ private Guid _key;
private int _template;
private string _name;
private string _docTypeAlias;
@@ -150,6 +151,16 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
}
}
+ public override Guid Key
+ {
+ get
+ {
+ if (_initialized == false)
+ Initialize();
+ return _key;
+ }
+ }
+
public override int TemplateId
{
get
@@ -348,6 +359,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
if (_xmlNode.Attributes != null)
{
_id = int.Parse(_xmlNode.Attributes.GetNamedItem("id").Value);
+ if (_xmlNode.Attributes.GetNamedItem("key") != null) // because, migration
+ _key = Guid.Parse(_xmlNode.Attributes.GetNamedItem("key").Value);
if (_xmlNode.Attributes.GetNamedItem("template") != null)
_template = int.Parse(_xmlNode.Attributes.GetNamedItem("template").Value);
if (_xmlNode.Attributes.GetNamedItem("sortOrder") != null)
diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs
index 8f7dbc31df..d6cf3b3141 100644
--- a/src/Umbraco.Web/PublishedContentExtensions.cs
+++ b/src/Umbraco.Web/PublishedContentExtensions.cs
@@ -19,7 +19,17 @@ namespace Umbraco.Web
/// Provides extension methods for IPublishedContent.
///
public static class PublishedContentExtensions
- {
+ {
+ #region Key
+
+ public static Guid GetKey(this IPublishedContent content)
+ {
+ var contentWithKey = content as IPublishedContentWithKey;
+ return contentWithKey == null ? Guid.Empty : contentWithKey.Key;
+ }
+
+ #endregion
+
#region Urls
///
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index b8ed24d5bb..1ad99b4315 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -303,6 +303,7 @@
+
diff --git a/src/Umbraco.Web/WebServices/SaveFileController.cs b/src/Umbraco.Web/WebServices/SaveFileController.cs
index 651b372988..3abfd31f52 100644
--- a/src/Umbraco.Web/WebServices/SaveFileController.cs
+++ b/src/Umbraco.Web/WebServices/SaveFileController.cs
@@ -12,8 +12,9 @@ using Umbraco.Web.Mvc;
using umbraco;
using umbraco.cms.businesslogic.macro;
using System.Collections.Generic;
+using umbraco.cms.helpers;
using Umbraco.Core;
-
+using Umbraco.Core.Configuration;
using Template = umbraco.cms.businesslogic.template.Template;
namespace Umbraco.Web.WebServices
@@ -216,6 +217,42 @@ namespace Umbraco.Web.WebServices
}
}
+ [HttpPost]
+ public JsonResult SaveScript(string filename, string oldName, string contents)
+ {
+ filename = filename.TrimStart(System.IO.Path.DirectorySeparatorChar);
+
+ var svce = (FileService) Services.FileService;
+ var script = svce.GetScriptByName(oldName);
+ if (script == null)
+ script = new Script(filename);
+ else
+ script.Path = filename;
+ script.Content = contents;
+
+ try
+ {
+ if (svce.ValidateScript(script) == false)
+ return Failed(ui.Text("speechBubbles", "scriptErrorText"), ui.Text("speechBubbles", "scriptErrorHeader"),
+ new FileSecurityException("File '" + filename + "' is not a valid script file."));
+
+ svce.SaveScript(script);
+ }
+ catch (Exception e)
+ {
+ return Failed(ui.Text("speechBubbles", "scriptErrorText"), ui.Text("speechBubbles", "scriptErrorHeader"), e);
+ }
+
+ return Success(ui.Text("speechBubbles", "scriptSavedText"), ui.Text("speechBubbles", "scriptSavedHeader"),
+ new
+ {
+ path = DeepLink.GetTreePathFromFilePath(script.Path),
+ name = script.Path,
+ url = script.VirtualPath,
+ contents = script.Content
+ });
+ }
+
///
/// Returns a successful message
///
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/scripts/editScript.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/scripts/editScript.aspx.cs
index ee54297f06..dc26d20bb6 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/settings/scripts/editScript.aspx.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/settings/scripts/editScript.aspx.cs
@@ -41,58 +41,42 @@ namespace umbraco.cms.presentation.settings.scripts
protected MenuButton SaveButton;
- private string file;
+ private string filename;
+
+ protected string ScriptTreeSyncPath { get; private set; }
+ protected int ScriptId { get; private set; }
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
- NameTxt.Text = file;
+ NameTxt.Text = filename;
- string path = "";
- if (file.StartsWith("~/"))
- path = IOHelper.ResolveUrl(file);
- else
- path = IOHelper.ResolveUrl(SystemDirectories.Scripts + "/" + file);
+ // get the script, ensure it exists (not null) and validate (because
+ // the file service ensures that it loads scripts from the proper location
+ // but does not seem to validate extensions?) - in case of an error,
+ // throw - that's what we did anyways.
+ // also scrapping the code that added .cshtml and .vbhtml extensions, and
+ // ~/Views directory - we're not using editScript.aspx for views anymore.
- lttPath.Text = "" + path + "";
+ var svce = ApplicationContext.Current.Services.FileService;
+ var script = svce.GetScriptByName(filename);
+ if (script == null) // not found
+ throw new FileNotFoundException("Could not find file '" + filename + "'.");
- var exts = UmbracoConfig.For.UmbracoSettings().Content.ScriptFileTypes.ToList();
- if (UmbracoConfig.For.UmbracoSettings().Templates.DefaultRenderingEngine == RenderingEngine.Mvc)
- {
- exts.Add("cshtml");
- exts.Add("vbhtml");
- }
-
- var dirs = SystemDirectories.Scripts;
- if (UmbracoConfig.For.UmbracoSettings().Templates.DefaultRenderingEngine == RenderingEngine.Mvc)
- dirs += "," + SystemDirectories.MvcViews;
-
- // validate file
- IOHelper.ValidateEditPath(IOHelper.MapPath(path), dirs.Split(','));
-
- // validate extension
- IOHelper.ValidateFileExtension(IOHelper.MapPath(path), exts);
-
-
- StreamReader SR;
- string S;
- SR = File.OpenText(IOHelper.MapPath(path));
- S = SR.ReadToEnd();
- SR.Close();
-
- editorSource.Text = S;
+ lttPath.Text = "" + script.VirtualPath + "";
+ editorSource.Text = script.Content;
+ ScriptTreeSyncPath = DeepLink.GetTreePathFromFilePath(filename);
Panel1.Text = ui.Text("editscript", base.getUser());
pp_name.Text = ui.Text("name", base.getUser());
pp_path.Text = ui.Text("path", base.getUser());
- if (!IsPostBack)
+ if (IsPostBack == false)
{
- string sPath = DeepLink.GetTreePathFromFilePath(file);
ClientTools
.SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias)
- .SyncTree(sPath, false);
+ .SyncTree(ScriptTreeSyncPath, false);
}
}
@@ -100,12 +84,12 @@ namespace umbraco.cms.presentation.settings.scripts
{
base.OnInit(e);
- file = Request.QueryString["file"].TrimStart('/');
+ filename = Request.QueryString["file"].TrimStart('/');
//need to change the editor type if it is XML
- if (file.EndsWith("xml"))
+ if (filename.EndsWith("xml"))
editorSource.CodeBase = uicontrols.CodeArea.EditorType.XML;
- else if (file.EndsWith("master"))
+ else if (filename.EndsWith("master"))
editorSource.CodeBase = uicontrols.CodeArea.EditorType.HTML;
@@ -153,7 +137,6 @@ namespace umbraco.cms.presentation.settings.scripts
}
}
-
protected override void OnPreRender(EventArgs e)
{
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs
index 65fbf2537b..23d30181a9 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/codeEditorSave.asmx.cs
@@ -355,6 +355,7 @@ namespace umbraco.presentation.webservices
// return "false";
//}
+ [Obsolete("This method has been superceded by the REST service /Umbraco/RestServices/SaveFile/SaveScript which is powered by the SaveFileController.")]
[WebMethod]
public string SaveScript(string filename, string oldName, string contents)
{
diff --git a/src/UmbracoExamine/UmbracoContentIndexer.cs b/src/UmbracoExamine/UmbracoContentIndexer.cs
index de76ab8e79..613304c4ff 100644
--- a/src/UmbracoExamine/UmbracoContentIndexer.cs
+++ b/src/UmbracoExamine/UmbracoContentIndexer.cs
@@ -145,6 +145,7 @@ namespace UmbracoExamine
= new List
{
new StaticField("id", FieldIndexTypes.NOT_ANALYZED, false, string.Empty),
+ new StaticField("key", FieldIndexTypes.NOT_ANALYZED, false, string.Empty),
new StaticField( "version", FieldIndexTypes.NOT_ANALYZED, false, string.Empty),
new StaticField( "parentID", FieldIndexTypes.NOT_ANALYZED, false, string.Empty),
new StaticField( "level", FieldIndexTypes.NOT_ANALYZED, true, "NUMBER"),
diff --git a/src/umbraco.cms/businesslogic/CMSNode.cs b/src/umbraco.cms/businesslogic/CMSNode.cs
index 90091cc80e..661e620056 100644
--- a/src/umbraco.cms/businesslogic/CMSNode.cs
+++ b/src/umbraco.cms/businesslogic/CMSNode.cs
@@ -1181,6 +1181,7 @@ order by level,sortOrder";
{
// attributes
x.Attributes.Append(xmlHelper.addAttribute(xd, "id", this.Id.ToString()));
+ x.Attributes.Append(xmlHelper.addAttribute(xd, "key", this.UniqueId.ToString()));
if (this.Level > 1)
x.Attributes.Append(xmlHelper.addAttribute(xd, "parentID", this.Parent.Id.ToString()));
else
diff --git a/src/umbraco.cms/businesslogic/Content.cs b/src/umbraco.cms/businesslogic/Content.cs
index f81ecce499..36eafb91a2 100644
--- a/src/umbraco.cms/businesslogic/Content.cs
+++ b/src/umbraco.cms/businesslogic/Content.cs
@@ -408,6 +408,7 @@ namespace umbraco.cms.businesslogic
// attributes
x.Attributes.Append(XmlHelper.AddAttribute(xd, "id", this.Id.ToString()));
+ x.Attributes.Append(XmlHelper.AddAttribute(xd, "key", this.UniqueId.ToString()));
x.Attributes.Append(XmlHelper.AddAttribute(xd, "version", this.Version.ToString()));
if (this.Level > 1)
x.Attributes.Append(XmlHelper.AddAttribute(xd, "parentID", this.Parent.Id.ToString()));
diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs
index 1df6dd7c40..0c5f2c3025 100644
--- a/src/umbraco.cms/businesslogic/web/Document.cs
+++ b/src/umbraco.cms/businesslogic/web/Document.cs
@@ -1288,6 +1288,7 @@ namespace umbraco.cms.businesslogic.web
// attributes
x.Attributes.Append(addAttribute(xd, "id", Id.ToString()));
+ x.Attributes.Append(addAttribute(xd, "key", UniqueId.ToString()));
// x.Attributes.Append(addAttribute(xd, "version", Version.ToString()));
if (Level > 1)
x.Attributes.Append(addAttribute(xd, "parentID", Parent.Id.ToString()));