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 @@ } } - @Model.value.caption + @Model.value.altText 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()));