diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 39a63b5093..d589b90d74 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -60,7 +60,7 @@ namespace Umbraco.Core.Models new XAttribute("writerID", content.Writer.Id), new XAttribute("creatorID", content.Creator.Id), new XAttribute("nodeType", content.ContentType.Id), - new XAttribute("template", content.Template),//Template name versus Id + new XAttribute("template", content.Template ?? string.Empty),//Template name versus Id - note that the template name/alias isn't saved in the db. new XAttribute("sortOrder", content.SortOrder), new XAttribute("createDate", content.CreateDate), new XAttribute("updateDate", content.UpdateDate), @@ -82,5 +82,17 @@ namespace Umbraco.Core.Models return xml; } + + /// + /// Creates the xml representation for the object + /// + /// to generate xml for + /// Boolean indicating whether the xml should be generated for preview + /// Xml representation of the passed in + public static XElement ToXml(this IContent content, bool isPreview) + { + //TODO Do a proper implementation of this + return content.ToXml(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWorkRepository.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWorkRepository.cs index e64bf79612..8281d63bae 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWorkRepository.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWorkRepository.cs @@ -2,6 +2,9 @@ namespace Umbraco.Core.Persistence.UnitOfWork { + /// + /// Defines the Unit Of Work-part of a repository + /// public interface IUnitOfWorkRepository { void PersistNewItem(IEntity entity); diff --git a/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs b/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs index 3bc9afd7f1..79a151f874 100644 --- a/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs +++ b/src/Umbraco.Core/Publishing/BasePublishingStrategy.cs @@ -46,6 +46,17 @@ namespace Umbraco.Core.Publishing Published(content, e); } + /// + /// Raises the event + /// + /// + /// The instance containing the event data. + protected virtual void OnPublished(IEnumerable content, PublishingEventArgs e) + { + if (Published != null) + Published(content, e); + } + /// /// Occurs before unpublish /// diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 17540689bc..729b7c1809 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -9,6 +9,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 9e68a575ee..0236863d61 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -175,17 +175,17 @@ namespace Umbraco.Tests.Persistence.Repositories var content = new Content(1048, contentType); content.Name = "Textpage 2 Child Node"; content.Creator = new Profile(0, "Administrator"); + content.Writer = new Profile(0, "Administrator"); // Act repository.AddOrUpdate(content); unitOfWork.Commit(); var id = content.Id; - var unitOfWork2 = provider.GetUnitOfWork(); - var contentTypeRepository2 = new ContentTypeRepository(unitOfWork2); - var repository2 = new ContentRepository(unitOfWork2, InMemoryCacheProvider.Current, contentTypeRepository2); + var contentTypeRepository2 = new ContentTypeRepository(unitOfWork); + var repository2 = new ContentRepository(unitOfWork, InMemoryCacheProvider.Current, contentTypeRepository2); repository2.Delete(content); - unitOfWork2.Commit(); + unitOfWork.Commit(); var content1 = repository2.Get(id); diff --git a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs new file mode 100644 index 0000000000..88ab06d263 --- /dev/null +++ b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Web; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Web.Strategies; +using umbraco.editorControls.tinyMCE3; +using umbraco.interfaces; + +namespace Umbraco.Tests.Publishing +{ + [TestFixture] + public class PublishingStrategyTests : BaseDatabaseFactoryTest + { + [SetUp] + public override void Initialize() + { + //this ensures its reset + PluginManager.Current = new PluginManager(); + + //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver + PluginManager.Current.AssembliesToScan = new[] + { + typeof(IDataType).Assembly, + typeof(tinyMCE3dataType).Assembly + }; + + DataTypesResolver.Current = new DataTypesResolver( + PluginManager.Current.ResolveDataTypes()); + + base.Initialize(); + + CreateTestData(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + [Test, Ignore] + public void Can_Publish_And_Update_Xml_Cache() + { + // Arrange + var httpContext = base.GetUmbracoContext("/test", 1234).HttpContext; + var updateContentCache = new UpdateContentCache(httpContext); + var contentService = ServiceContext.ContentService; + var content = contentService.GetById(1046); + + // Act + bool published = contentService.Publish(content, 0); + + // Assert + Assert.That(published, Is.True); + Assert.That(content.Published, Is.True); + } + + public void CreateTestData() + { + //NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. + + //Create and Save ContentType "umbTextpage" -> 1045 + ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage", "Textpage"); + ServiceContext.ContentTypeService.Save(contentType); + + //Create and Save Content "Homepage" based on "umbTextpage" -> 1046 + Content textpage = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(textpage, 0); + + //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1047 + Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); + ServiceContext.ContentService.Save(subpage, 0); + + //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1048 + Content subpage2 = MockedContent.CreateSimpleContent(contentType, "Text Page 2", textpage.Id); + ServiceContext.ContentService.Save(subpage2, 0); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index abbb7caa1c..1a17cbc411 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -121,6 +121,7 @@ + diff --git a/src/Umbraco.Web/Publishing/PublishingStrategy.cs b/src/Umbraco.Web/Publishing/PublishingStrategy.cs index 44bfbb6ab4..16ebd693c5 100644 --- a/src/Umbraco.Web/Publishing/PublishingStrategy.cs +++ b/src/Umbraco.Web/Publishing/PublishingStrategy.cs @@ -132,6 +132,8 @@ namespace Umbraco.Web.Publishing OnPublished(item, e); } + OnPublished(content, e); + //NOTE: Ideally the xml cache should be refreshed here - as part of the publishing return true; diff --git a/src/Umbraco.Web/Strategies/TrashedContent.cs b/src/Umbraco.Web/Strategies/TrashedContent.cs new file mode 100644 index 0000000000..bffbdf11cf --- /dev/null +++ b/src/Umbraco.Web/Strategies/TrashedContent.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Web.Strategies +{ + public class TrashedContent + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/UpdateContentCache.cs b/src/Umbraco.Web/Strategies/UpdateContentCache.cs new file mode 100644 index 0000000000..ae70dc1ae9 --- /dev/null +++ b/src/Umbraco.Web/Strategies/UpdateContentCache.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Web; +using System.Web.Caching; +using System.Xml; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Web.Publishing; +using umbraco.interfaces; +using umbraco.presentation.nodeFactory; +using Node = umbraco.NodeFactory.Node; + +namespace Umbraco.Web.Strategies +{ + public class UpdateContentCache : IApplicationStartupHandler + { + // Sync access to internal cache + private static readonly object XmlContentInternalSyncLock = new object(); + private const string XmlContextContentItemKey = "UmbracoXmlContextContent"; + private readonly HttpContextBase _httpContext; + + public UpdateContentCache() + { + _httpContext = new HttpContextWrapper(HttpContext.Current); + + PublishingStrategy.Published += PublishingStrategy_Published; + } + + public UpdateContentCache(HttpContextBase httpContext) + { + _httpContext = httpContext; + + PublishingStrategy.Published += PublishingStrategy_Published; + } + + void PublishingStrategy_Published(object sender, PublishingEventArgs e) + { + if((sender is IContent) == false) return; + + //var e = new DocumentCacheEventArgs(); + //FireBeforeUpdateDocumentCache(d, e); + + var content = sender as IContent; + + // lock the xml cache so no other thread can write to it at the same time + // note that some threads could read from it while we hold the lock, though + lock (XmlContentInternalSyncLock) + { + // modify a clone of the cache because even though we're into the write-lock + // we may have threads reading at the same time. why is this an option? + XmlDocument wip = UmbracoSettings.CloneXmlCacheOnPublish + ? CloneXmlDoc(XmlContent) + : XmlContent; + + ClearContextCache(); + + XmlContent = UpdateXmlAndSitemap(content, wip, true); + } + + // clear cached field values + if (_httpContext != null) + { + Cache httpCache = _httpContext.Cache; + string cachedFieldKeyStart = String.Format("contentItem{0}_", content.Id); + var foundKeys = new List(); + foreach (DictionaryEntry cacheItem in httpCache) + { + string key = cacheItem.Key.ToString(); + if (key.StartsWith(cachedFieldKeyStart)) + foundKeys.Add(key); + } + foreach (string foundKey in foundKeys) + { + httpCache.Remove(foundKey); + } + } + + //Action.RunActionHandlers(d, ActionPublish.Instance); + //FireAfterUpdateDocumentCache(d, e); + } + + private XmlDocument UpdateXmlAndSitemap(IContent content, XmlDocument xmlContentCopy, bool updateSitemapProvider) + { + // check if document *is* published, it could be unpublished by an event + if (content.Published) + { + int parentId = content.Level == 1 ? -1 : content.ParentId; + xmlContentCopy = AppendContentXml(content.Id, content.Level, parentId, content.ToXml(false).GetXmlNode(), xmlContentCopy); + + // update sitemapprovider + if (updateSitemapProvider && SiteMap.Provider is UmbracoSiteMapProvider) + { + try + { + var prov = (UmbracoSiteMapProvider)SiteMap.Provider; + global::umbraco.NodeFactory.Node n = new Node(content.Id, true); + if (!String.IsNullOrEmpty(n.Url) && n.Url != "/#") + { + prov.UpdateNode(n); + } + else + { + //Log.Add(LogTypes.Error, content.Id, "Can't update Sitemap Provider due to empty Url in node"); + } + } + catch (Exception ee) + { + //Log.Add(LogTypes.Error, content.Id, string.Format("Error adding node to Sitemap Provider in PublishNodeDo(): {0}", ee)); + } + } + } + + return xmlContentCopy; + } + + private XmlDocument AppendContentXml(int id, int level, int parentId, XmlNode docNode, XmlDocument xmlContentCopy) + { + // Find the document in the xml cache + XmlNode x = xmlContentCopy.GetElementById(id.ToString()); + + // if the document is not there already then it's a new document + // we must make sure that its document type exists in the schema + var xmlContentCopy2 = xmlContentCopy; + if (x == null && UmbracoSettings.UseLegacyXmlSchema == false) + { + //TODO Look into the validate schema method - seems a bit odd + //xmlContentCopy = ValidateSchema(docNode.Name, xmlContentCopy); + if (xmlContentCopy != xmlContentCopy2) + docNode = xmlContentCopy.ImportNode(docNode, true); + } + + // Find the parent (used for sortering and maybe creation of new node) + XmlNode parentNode; + if (level == 1) + parentNode = xmlContentCopy.DocumentElement; + else + parentNode = xmlContentCopy.GetElementById(parentId.ToString()); + + if (parentNode != null) + { + if (x == null) + { + x = docNode; + parentNode.AppendChild(x); + } + else + { + //TransferValuesFromDocumentXmlToPublishedXml(docNode, x); + } + + // TODO: Update with new schema! + string xpath = UmbracoSettings.UseLegacyXmlSchema ? "./node" : "./* [@id]"; + XmlNodeList childNodes = parentNode.SelectNodes(xpath); + + // Maybe sort the nodes if the added node has a lower sortorder than the last + if (childNodes.Count > 0) + { + int siblingSortOrder = + int.Parse(childNodes[childNodes.Count - 1].Attributes.GetNamedItem("sortOrder").Value); + int currentSortOrder = int.Parse(x.Attributes.GetNamedItem("sortOrder").Value); + if (childNodes.Count > 1 && siblingSortOrder > currentSortOrder) + { + //SortNodes(ref parentNode); + } + } + } + + return xmlContentCopy; + } + + private XmlDocument CloneXmlDoc(XmlDocument xmlDoc) + { + var xmlCopy = (XmlDocument)xmlDoc.CloneNode(true); + return xmlCopy; + } + + /// + /// Clear HTTPContext cache if any + /// + private void ClearContextCache() + { + // If running in a context very important to reset context cache or else new nodes are missing + if (_httpContext != null && _httpContext.Items.Contains(XmlContextContentItemKey)) + _httpContext.Items.Remove(XmlContextContentItemKey); + } + + private XmlDocument ValidateSchema(string docTypeAlias, XmlDocument xmlDoc) + { + // check if doctype is defined in schema else add it + // can't edit the doctype of an xml document, must create a new document + + var doctype = xmlDoc.DocumentType; + var subset = doctype.InternalSubset; + if (!subset.Contains(string.Format("", docTypeAlias))) + { + subset = string.Format("\r\n\r\n{1}", docTypeAlias, subset); + var xmlDoc2 = new XmlDocument(); + doctype = xmlDoc2.CreateDocumentType("root", null, null, subset); + xmlDoc2.AppendChild(doctype); + var root = xmlDoc2.ImportNode(xmlDoc.DocumentElement, true); + xmlDoc2.AppendChild(root); + + // apply + xmlDoc = xmlDoc2; + } + + return xmlDoc; + } + + private XmlDocument XmlContent + { + get + { + var content = _httpContext.Items[XmlContextContentItemKey] as XmlDocument; + if (content == null) + { + //content = global::umbraco.content.Instance.XmlContent; + + //TODO Move this to an extension method for the ContentType + var dtd = new StringBuilder(); + dtd.AppendLine(" "); + dtd.AppendLine("]>"); + + content = new XmlDocument(); + content.LoadXml( + String.Format("{0}{1}{0}", + Environment.NewLine, + dtd)); + + _httpContext.Items[XmlContextContentItemKey] = content; + } + return content; + } + set { _httpContext.Items[XmlContextContentItemKey] = value; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/UpdateMultipleContentCache.cs b/src/Umbraco.Web/Strategies/UpdateMultipleContentCache.cs new file mode 100644 index 0000000000..a964ab8e27 --- /dev/null +++ b/src/Umbraco.Web/Strategies/UpdateMultipleContentCache.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Web.Publishing; + +namespace Umbraco.Web.Strategies +{ + public class UpdateMultipleContentCache + { + public UpdateMultipleContentCache() + { + PublishingStrategy.Published += PublishingStrategy_Published; + } + + void PublishingStrategy_Published(object sender, Core.PublishingEventArgs e) + { + if ((sender is IEnumerable) == false) return; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c4fbacf117..bd32a1259c 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -323,6 +323,9 @@ + + + ASPXCodeBehind