Compare commits

...

70 Commits

Author SHA1 Message Date
per ploug 838699d884 Variants button+ menu changes 2014-06-10 15:05:33 +02:00
Shannon ac1ce4346e re-adds old providers 2014-06-10 13:22:57 +02:00
Shannon 93d3a92d25 updates request/member segments 2014-06-10 00:36:03 +02:00
Shannon 00e1ecb80e removes test providers 2014-06-09 19:59:34 +02:00
Shannon ab640455b1 Fixing unpublishing which was unpublishing the master when a child was unpublished. 2014-06-09 19:22:17 +02:00
Shannon 4c1e5072dd Fixes some errors with url gen, fixes variant creation events since it's a copy 2014-06-09 18:52:40 +02:00
Shannon baf703646d Creating variants can now copy the data 2014-06-09 18:07:07 +02:00
Shannon ebb3f580aa fixes content name thingy with new variant drop down 2014-06-09 17:06:56 +02:00
Shannon 24b855a207 removes test provider 2014-06-09 16:32:10 +02:00
Shannon 1b77c39507 Merge branch '7.1.5' into 7.1.3-variations 2014-06-09 15:34:04 +02:00
Shannon 3e76623887 Merge branch '7.1.5' into 7.1.3-variations
Conflicts:
	src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js
	src/Umbraco.Web/Umbraco.Web.csproj
2014-06-09 14:36:07 +02:00
Shannon e667cdbc66 Removes UI for adding a segment value for a configured segment since it doesn't make sense, a configurable segment is always just a boolean match. 2014-06-04 20:32:51 +10:00
Shannon c364610f1d Change to make segment cookie http only 2014-06-04 20:24:02 +10:00
Shannon c4d07dbcc5 Added BrowserWidthProvider for testing against for responsive content - should employ a wurfl sln instead of js though 2014-06-04 20:16:41 +10:00
Shannon 77644c7bee Gets custom variants working/matching properly. 2014-06-04 17:50:07 +10:00
Shannon 6ddaa7cd4d Allows configurable segments to be advertised as variants 2014-06-04 16:57:30 +10:00
Shannon 14d4bbd854 removes compare UI for now. 2014-06-02 15:38:16 +10:00
Shannon 30df91bab9 attempts adding a very quick comparison to see what would happen. 2014-06-02 15:37:10 +10:00
Shannon 2490355934 Got distributed cache working properly with variants and ensures variant's names are synced to the master always. 2014-05-30 11:57:59 +10:00
Shannon fdc5976035 Ensures the xml cache is distributed properly including variants as well as Examine updating correctly. 2014-05-29 18:58:51 +10:00
Shannon e1702cec60 Gets variant names updated when a master doc changes it's name, this includes handling the invalidation of cache for the master + all variants and now my brain hurts but it is working. 2014-05-29 18:15:18 +10:00
Shannon 9d220c549b Gets Children working with variants 2014-05-29 16:17:44 +10:00
Shannon 51250d40a8 Gets 'Children' call in the cache to return the master's children 2014-05-29 14:42:33 +10:00
Shannon 9677b01f54 makes name not-editable for variants. 2014-05-29 14:10:16 +10:00
Shannon 847d6f3823 Doesn't map variant info when the item is new 2014-05-29 13:22:54 +10:00
Shannon c4faa084a1 Merge remote-tracking branch 'origin/7.1.5' into 7.1.3-variations 2014-05-29 13:22:14 +10:00
Shannon ec573f0b46 fixes null check on new items 2014-05-29 13:17:39 +10:00
Shannon 333f337ffc Ensures unique naming is ignoring variants, adds small UI bit to show that the current item is a variant. 2014-05-29 12:49:45 +10:00
Shannon 4472623595 Changes VariantDefinition -> VariantInfo and removes the child variants from the object so they are not cached as this causes problems. 2014-05-28 18:32:53 +10:00
Shannon df34723a1e make sure to clear the content runtime cache when templates are changed. 2014-05-28 17:59:17 +10:00
Shannon c284c0656e Removes xml namespaces for variants, changes isDoc='variant' for variants 2014-05-28 17:58:57 +10:00
Shannon 107958cccb Merge branch '7.1.4' into 7.1.3-variations 2014-05-28 16:36:08 +10:00
Shannon 3a4d05fe79 minor change 2014-05-28 10:31:38 +10:00
Shannon b7e126c577 started on some more ui bits - looking at comparing/editing side-by-side 2014-05-27 22:46:56 +10:00
Shannon 5a91e20b82 Updates variants to be stored in xml with namespace (and now keeps the @isDoc) - but not too sure about this still. 2014-05-27 17:17:33 +10:00
Shannon 8cfa707dd4 Adds CreateContentVariationWithIdentity on content service so the whole thing is done in one trans. Gets content finding working with language variants - but now need to determine how we really want that to work.... 2014-05-27 12:37:42 +10:00
Shannon 643a2850fa made the variant def cloneable, added some new xpath definitions to lookup variants as well as master docs 2014-05-26 19:43:53 +10:00
Shannon 600820e14e only shows the domain specific URL for language variants. 2014-05-26 15:51:47 +10:00
Shannon c866ecbe7b Moves the variant mapping to the content model mapper, updates IContent to expose the variants (lazily), adds new attributes to be stored in the xml cache 2014-05-26 15:19:31 +10:00
Shannon 3fb0e65d90 Hides the URL for any non-language variants. 2014-05-22 19:06:01 +10:00
Shannon 9e09443380 Updates content controller to pay attention to enabled/disabled providers and their variants. 2014-05-22 18:19:58 +10:00
Shannon 1624919f4a Got UI working to be able to enabled/diable custom variants that are exposed by a provider. 2014-05-22 18:04:31 +10:00
Shannon 2b502e89f7 Gets installable custom variants showing up 2014-05-22 16:02:04 +10:00
Shannon 45aea76a11 Fixes validation problems with dashboards - we don't want a global form for dashboards like we do with editors, this causes problems. 2014-05-22 14:12:40 +10:00
Shannon 13c6b6ed7c Merge branch '7.1.4' into 7.1.3-variations 2014-05-22 13:18:06 +10:00
Shannon 8a0edee12e Merge remote-tracking branch 'origin/7.1.4' into 7.1.3-variations 2014-05-22 10:52:53 +10:00
Shannon 772cffbe11 Variants creating/deleting/recycling, updated content service with methods to get variant definitions. 2014-05-21 19:54:19 +10:00
Shannon eef4dfc909 fixes some issues when deleting a variant, adds button to return to master doc from a variant. 2014-05-21 17:51:10 +10:00
Shannon e04d1dd5d1 fix isloading on rte 2014-05-21 17:50:38 +10:00
Shannon 0e01b04fff Merge branch '7.1.4' into 7.1.3-variations 2014-05-21 15:39:40 +10:00
Shannon 41cdfecd12 Updated lots of the relations service/repo, have menu showing allowing to create content variants which is working, updated the tree to ensure variants are not shown. 2014-05-20 19:04:21 +10:00
Shannon b61aec3156 Merge remote-tracking branch 'origin/7.1.4' into 7.1.3-variations 2014-05-20 13:30:06 +10:00
Shannon 15b5dcfd56 adjusts some namespaces 2014-05-20 13:15:11 +10:00
Shannon 26b95afdad Segments are working and persisting and configurable 2014-05-20 12:56:39 +10:00
Shannon fa002ec3b2 add/remove buttons working now. 2014-05-19 20:26:25 +10:00
Shannon 2e777121d6 Have the UI working (more or less) for configuring segment providers. 2014-05-19 20:13:31 +10:00
Shannon 5d40f85116 Got configurable segment providers in, got a dashboard in there to be able to enable / disable providers, now to setup configuration for each and ensure we check which ones are enabled in the pipeline process. 2014-05-19 18:17:18 +10:00
Shannon 311d0ae85d adds the configurable provider to the resolver collection, now to wire up in UI. 2014-05-19 15:17:39 +10:00
Shannon 2559be86e5 Creates configurable referer provider. 2014-05-19 15:11:55 +10:00
Shannon c2958471e7 Gets start of configurable segment providers 2014-05-19 15:03:01 +10:00
Shannon bb6bde247b MemberSegments object created and working, now can use the view helpers to check the current request including request params, cookie params and persisted member params. 2014-05-19 14:31:28 +10:00
Shannon f0c1d3dfe2 segments will be persisted to the member profile for any advertised segment. 2014-05-19 12:59:47 +10:00
Shannon f9bb36c95c some mods to try to get persisted segments persisted to members 2014-05-16 08:37:03 +10:00
Shannon 7da271ec40 Merge branch '7.1.3' into 7.1.3-variations 2014-05-15 16:16:03 +10:00
Shannon 6fcc59684b Gets the segments persisted to cookie for advertised segments. 2014-05-15 15:43:05 +10:00
Shannon 99b7f66c41 Merge branch '7.1.3' into 7.1.3-variations 2014-05-15 14:43:30 +10:00
Shannon 1e01d8272b Gets segmentation providers in, creates a default one (still needs work), exposes some APIs to get at the information which all happens lazily when required. 2014-05-13 19:26:37 +10:00
Shannon c3fe82a99d Merge branch '7.1.3' into 7.1.3-variations 2014-05-13 15:48:26 +10:00
Shannon c2b96c7211 started on a segments provider 2014-05-12 19:30:49 +10:00
Shannon 0a24581836 adds folder 2014-05-12 17:10:56 +10:00
100 changed files with 3611 additions and 512 deletions
+18
View File
@@ -100,6 +100,16 @@ namespace Umbraco.Core
public static class Member
{
/// <summary>
/// The label for the field to store the segment data for the member
/// </summary>
public const string SegmentsLabel = "Segments";
/// <summary>
/// The property type alias for the field to store the segment data for the member
/// </summary>
public const string Segments = "umbracoMemberSegments";
/// <summary>
/// if a role starts with __umbracoRole we won't show it as it's an internal role used for public access
/// </summary>
public static readonly string InternalRolePrefix = "__umbracoRole";
@@ -251,6 +261,14 @@ namespace Umbraco.Core
Alias = PasswordQuestion,
Name = PasswordQuestionLabel
}
},
{
Segments,
new PropertyType(PropertyEditors.NoEditAlias, DataTypeDatabaseType.Ntext, true)
{
Alias = Segments,
Name = SegmentsLabel
}
}
};
}
+5
View File
@@ -17,6 +17,11 @@
/// </summary>
public const string AuthCookieName = "UMB_UCONTEXT";
/// <summary>
/// The segment cookie name
/// </summary>
public const string SegmentCookieName = "UMB_SEGMENTS";
}
}
}
+34 -11
View File
@@ -2,6 +2,7 @@
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Umbraco.Core.Models.ContentVariations;
namespace Umbraco.Core.Models
{
@@ -21,6 +22,27 @@ namespace Umbraco.Core.Models
private int _writer;
private string _nodeName;//NOTE Once localization is introduced this will be the non-localized Node Name.
private bool _permissionsChanged;
public Content(string name, int parentId, IContentType contentType, PropertyCollection properties, VariantInfo variantInfo)
: base(name, parentId, contentType, properties)
{
Mandate.ParameterNotNull(variantInfo, "variantInfo");
Mandate.ParameterNotNull(contentType, "contentType");
_contentType = contentType;
VariantInfo = variantInfo;
}
public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties, VariantInfo variantInfo)
: base(name, parent, contentType, properties)
{
Mandate.ParameterNotNull(contentType, "contentType");
Mandate.ParameterNotNull(variantInfo, "variantInfo");
_contentType = contentType;
VariantInfo = variantInfo;
}
/// <summary>
/// Constructor for creating a Content object
/// </summary>
@@ -40,11 +62,8 @@ namespace Umbraco.Core.Models
/// <param name="contentType">ContentType for the current Content object</param>
/// <param name="properties">Collection of properties</param>
public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties)
: base(name, parent, contentType, properties)
{
Mandate.ParameterNotNull(contentType, "contentType");
_contentType = contentType;
: this(name, parent, contentType, properties, new VariantInfo())
{
}
/// <summary>
@@ -65,12 +84,9 @@ namespace Umbraco.Core.Models
/// <param name="parentId">Id of the Parent content</param>
/// <param name="contentType">ContentType for the current Content object</param>
/// <param name="properties">Collection of properties</param>
public Content(string name, int parentId, IContentType contentType, PropertyCollection properties)
: base(name, parentId, contentType, properties)
{
Mandate.ParameterNotNull(contentType, "contentType");
_contentType = contentType;
public Content(string name, int parentId, IContentType contentType, PropertyCollection properties)
: this(name, parentId, contentType, properties, new VariantInfo())
{
}
private static readonly PropertyInfo TemplateSelector = ExpressionHelper.GetPropertyInfo<Content, ITemplate>(x => x.Template);
@@ -82,6 +98,11 @@ namespace Umbraco.Core.Models
private static readonly PropertyInfo NodeNameSelector = ExpressionHelper.GetPropertyInfo<Content, string>(x => x.NodeName);
private static readonly PropertyInfo PermissionsChangedSelector = ExpressionHelper.GetPropertyInfo<Content, bool>(x => x.PermissionsChanged);
/// <summary>
/// Gets the variant definition for this content item
/// </summary>
public VariantInfo VariantInfo { get; set; }
/// <summary>
/// Gets or sets the template used by the Content.
/// This is used to override the default one from the ContentType.
@@ -454,6 +475,8 @@ namespace Umbraco.Core.Models
clone._contentType = (IContentType)ContentType.DeepClone();
clone.ResetDirtyProperties(false);
clone.VariantInfo = (VariantInfo)VariantInfo.DeepClone();
return clone;
}
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Umbraco.Core.Models.ContentVariations
{
/// <summary>
/// A class representing the variant info, whether it is a variant itself or a master doc
/// </summary>
public class VariantInfo
{
public VariantInfo()
{
IsVariant = false;
VariantIds = new int[] {};
}
public VariantInfo(params int[] variantIds)
{
VariantIds = variantIds.Any()
? variantIds
: new int[] {};
IsVariant = false;
}
public VariantInfo(int masterDocId, string key)
{
IsVariant = true;
MasterDocId = masterDocId;
Key = key;
VariantIds = new int[] { };
}
public int[] VariantIds { get; set; }
/// <summary>
/// Whether or not this is a variant (i.e. not a master doc)
/// </summary>
public bool IsVariant { get; private set; }
/// <summary>
/// The master doc id if this is a variant
/// </summary>
public int MasterDocId { get; private set; }
/// <summary>
/// They key stored with this variant when it is a variant.
/// </summary>
public string Key { get; private set; }
///// <summary>
///// The child variants for this node - if it is a master doc
///// </summary>
//public IEnumerable<ChildVariant> ChildVariants { get; private set; }
public object DeepClone()
{
var clone = (VariantInfo)MemberwiseClone();
return clone;
}
}
}
+6
View File
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Core.Persistence.Mappers;
namespace Umbraco.Core.Models
@@ -9,6 +10,11 @@ namespace Umbraco.Core.Models
/// </summary>
public interface IContent : IContentBase
{
/// <summary>
/// Gets the variant definition for this node
/// </summary>
VariantInfo VariantInfo { get; set; }
/// <summary>
/// Gets or sets the template used by the Content.
/// This is used to override the default one from the ContentType.
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Core.Models.Rdbms;
namespace Umbraco.Core.Persistence.Factories
@@ -10,13 +11,15 @@ namespace Umbraco.Core.Persistence.Factories
private readonly IContentType _contentType;
private readonly Guid _nodeObjectTypeId;
private readonly int _id;
private readonly VariantInfo _variantInfo;
private int _primaryKey;
public ContentFactory(IContentType contentType, Guid nodeObjectTypeId, int id)
public ContentFactory(IContentType contentType, Guid nodeObjectTypeId, int id, VariantInfo variantInfo)
{
_contentType = contentType;
_nodeObjectTypeId = nodeObjectTypeId;
_id = id;
_variantInfo = variantInfo;
}
public ContentFactory(Guid nodeObjectTypeId, int id)
@@ -29,7 +32,7 @@ namespace Umbraco.Core.Persistence.Factories
public IContent BuildEntity(DocumentDto dto)
{
var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, _contentType)
var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, _contentType, new PropertyCollection(), _variantInfo)
{
Id = _id,
Key =
@@ -3,10 +3,12 @@ using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Models.Rdbms;
@@ -26,7 +28,7 @@ namespace Umbraco.Core.Persistence.Repositories
{
private readonly IContentTypeRepository _contentTypeRepository;
private readonly ITemplateRepository _templateRepository;
private readonly ITagsRepository _tagRepository;
private readonly ITagsRepository _tagRepository;
private readonly CacheHelper _cacheHelper;
private readonly ContentPreviewRepository<IContent> _contentPreviewRepository;
private readonly ContentXmlRepository<IContent> _contentXmlRepository;
@@ -40,6 +42,7 @@ namespace Umbraco.Core.Persistence.Repositories
_contentTypeRepository = contentTypeRepository;
_templateRepository = templateRepository;
_tagRepository = tagRepository;
_cacheHelper = cacheHelper;
_contentPreviewRepository = new ContentPreviewRepository<IContent>(work, NullCacheProvider.Current);
_contentXmlRepository = new ContentXmlRepository<IContent>(work, NullCacheProvider.Current);
@@ -56,6 +59,7 @@ namespace Umbraco.Core.Persistence.Repositories
_contentTypeRepository = contentTypeRepository;
_templateRepository = templateRepository;
_tagRepository = tagRepository;
_cacheHelper = cacheHelper;
_contentPreviewRepository = new ContentPreviewRepository<IContent>(work, NullCacheProvider.Current);
_contentXmlRepository = new ContentXmlRepository<IContent>(work, NullCacheProvider.Current);
@@ -173,7 +177,7 @@ namespace Umbraco.Core.Persistence.Repositories
{
get { return new Guid(Constants.ObjectTypes.Document); }
}
#endregion
#region Overrides of VersionableRepositoryBase<IContent>
@@ -253,8 +257,12 @@ namespace Umbraco.Core.Persistence.Repositories
{
((Content)entity).AddingEntity();
//Ensure unique name on the same level
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name);
//Ensure unique name on the same level when it is NOT a variant
if (entity.VariantInfo.IsVariant == false)
{
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name);
}
var factory = new ContentFactory(NodeObjectTypeId, entity.Id);
var dto = factory.BuildDto(entity);
@@ -338,7 +346,41 @@ namespace Umbraco.Core.Persistence.Repositories
property.Id = keyDictionary[property.PropertyTypeId];
}
//lastly, check if we are a creating a published version , then update the tags table
//Check if this is a content variant, if it is then we need to create a relation accordingly
if (entity.VariantInfo.IsVariant)
{
var relType = Database.FirstOrDefault<RelationTypeDto>(
new Sql().Select("*").From<RelationTypeDto>().Where<RelationTypeDto>(typeDto => typeDto.Alias == "umbContentVariants"));
object relId;
if (relType == null)
{
relId = Database.Insert(new RelationTypeDto
{
Alias = "umbContentVariants",
ChildObjectType = new Guid(Constants.ObjectTypes.Document),
ParentObjectType = new Guid(Constants.ObjectTypes.Document),
Name = "umbContentVariants"
});
}
else
{
relId = relType.Id;
}
var asInt = relId.TryConvertTo<int>();
Database.Insert(new RelationDto
{
ChildId = entity.Id,
ParentId = entity.VariantInfo.MasterDocId,
Datetime = DateTime.Now,
RelationType = asInt.Result,
//the comment is the variant key
Comment = entity.VariantInfo.Key
});
}
//check if we are a creating a published version , then update the tags table
if (entity.Published)
{
UpdatePropertyTags(entity, _tagRepository);
@@ -363,8 +405,19 @@ namespace Umbraco.Core.Persistence.Repositories
entity.UpdateDate = DateTime.Now;
}
//Ensure unique name on the same level
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id);
var factory = new ContentFactory(NodeObjectTypeId, entity.Id);
if (entity.VariantInfo.IsVariant)
{
//If it is a variant, ensure it always has the same name as it's master
var master = Get(entity.VariantInfo.MasterDocId);
entity.Name = master.Name;
}
else
{
//Ensure unique name on the same level when it is NOT a variant
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id);
}
//Look up parent to get and set the correct Path and update SortOrder if ParentId has changed
if (((ICanBeDirty)entity).IsPropertyDirty("ParentId"))
@@ -383,7 +436,7 @@ namespace Umbraco.Core.Persistence.Repositories
// Gonna just leave it as is for now, and not re-propogate permissions.
}
var factory = new ContentFactory(NodeObjectTypeId, entity.Id);
//Look up Content entry to get Primary for updating the DTO
var contentDto = Database.SingleOrDefault<ContentDto>("WHERE nodeId = @Id", new { Id = entity.Id });
factory.SetPrimaryKey(contentDto.PrimaryKey);
@@ -479,8 +532,8 @@ namespace Umbraco.Core.Persistence.Repositories
property.Id = keyDictionary[property.PropertyTypeId];
}
}
//lastly, check if we are a newly published version and then update the tags table
//check if we are a newly published version and then update the tags table
if (isNewPublishedVersion)
{
UpdatePropertyTags(entity, _tagRepository);
@@ -558,6 +611,40 @@ namespace Umbraco.Core.Persistence.Repositories
}
}
internal VariantInfo GetVariantInfo(int contentId)
{
var sql = new Sql()
.Select("umbracoRelation.*")
.From<RelationDto>()
.InnerJoin<RelationTypeDto>()
.On<RelationDto, RelationTypeDto>(dto => dto.RelationType, dto => dto.Id)
.Where("umbracoRelationType.alias = @alias AND (umbracoRelation.parentId = @parentId OR umbracoRelation.childId = @childId)",
new {parentId = contentId, childId = contentId, alias = "umbContentVariants"});
var result = Database.Fetch<dynamic>(sql);
//first check if the result has the current content id as a childid in the collection,
// if it does, then it means that this content item is a variant, otherwise if it's id is
// contained in any parent ids then it's a master
var variant = result.Where(x => x.childId == contentId).ToArray();
if (variant.Any())
{
var row = variant.Single();
//this content item is a variant itself
return new VariantInfo(
//get it's master doc id
row.parentId,
row.comment);
}
var childVariants = result.Where(x => x.parentId == contentId)
.Select(x => (int)x.childId)
.ToArray();
//it's a master doc
return new VariantInfo(childVariants);
}
public IContent GetByLanguage(int id, string language)
{
var sql = GetBaseQuery(false);
@@ -638,8 +725,8 @@ namespace Umbraco.Core.Persistence.Repositories
{
var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId);
var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId);
var content = factory.BuildEntity(dto);
var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId, GetVariantInfo(dto.NodeId));
var content = (Content)factory.BuildEntity(dto);
//Check if template id is set on DocumentDto, and get ITemplate if it is.
if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0)
@@ -648,7 +735,7 @@ namespace Umbraco.Core.Persistence.Repositories
}
content.Properties = GetPropertyCollection(dto.NodeId, versionId, contentType, content.CreateDate, content.UpdateDate);
//on initial construction we don't want to have dirty properties tracked
// http://issues.umbraco.org/issue/U4-1946
((Entity)content).ResetDirtyProperties(false);
@@ -660,19 +747,34 @@ namespace Umbraco.Core.Persistence.Repositories
if (EnsureUniqueNaming == false)
return nodeName;
var sql = new Sql();
sql.Select("*")
var docSql = new Sql()
.Select("*")
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName));
//get all variants ids for this level so we can ignore them in the comparison
var childVarSql = new Sql()
.Select("umbracoNode.id")
.From<NodeDto>()
.InnerJoin<RelationDto>()
.On<RelationDto, NodeDto>(dto => dto.ChildId, dto => dto.NodeId)
.InnerJoin<RelationTypeDto>()
.On<RelationTypeDto, RelationDto>(dto => dto.Id, dto => dto.RelationType)
.Where<RelationTypeDto>(x => x.Alias == "umbContentVariants")
.Where<NodeDto>(x => x.ParentId == parentId);
int uniqueNumber = 1;
var currentName = nodeName;
var variantIds = Database.Fetch<int>(childVarSql);
var dtos = Database.Fetch<NodeDto>(docSql)
.Where(x => variantIds.Contains(x.NodeId) == false)
.OrderBy(x => x.Text, new SimilarNodeNameComparer());
var dtos = Database.Fetch<NodeDto>(sql);
if (dtos.Any())
{
var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer());
foreach (var dto in results)
foreach (var dto in dtos)
{
if(id != 0 && id == dto.NodeId) continue;
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Xml.Linq;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.Querying;
@@ -9,6 +10,7 @@ namespace Umbraco.Core.Persistence.Repositories
{
public interface IContentRepository : IRepositoryVersionable<int, IContent>
{
/// <summary>
/// Gets a specific language version of an <see cref="IContent"/>
/// </summary>
@@ -1,9 +1,13 @@
using Umbraco.Core.Models;
using System.Collections.Generic;
using Umbraco.Core.Models;
namespace Umbraco.Core.Persistence.Repositories
{
public interface IRelationRepository : IRepositoryQueryable<int, IRelation>
{
IEnumerable<IRelation> GetByParentId(int id, string relationTypeAlias);
IEnumerable<IRelation> GetByParentIds(int[] ids, string relationTypeAlias);
IEnumerable<IRelation> GetByChildIds(int[] ids, string relationTypeAlias);
IEnumerable<IRelation> GetByParentOrChildId(int id, string relationTypeAlias);
}
}
@@ -4,6 +4,6 @@ namespace Umbraco.Core.Persistence.Repositories
{
public interface IRelationTypeRepository : IRepositoryQueryable<int, IRelationType>
{
IRelationType GetByAlias(string relationTypeAlias);
}
}
@@ -47,8 +47,7 @@ namespace Umbraco.Core.Persistence.Repositories
var factory = new RelationFactory(relationType);
var entity = factory.BuildEntity(dto);
//on initial construction we don't want to have dirty properties tracked
// on initial construction we don't want to have dirty properties tracked
// http://issues.umbraco.org/issue/U4-1946
((TracksChangesEntityBase)entity).ResetDirtyProperties(false);
@@ -57,6 +56,9 @@ namespace Umbraco.Core.Persistence.Repositories
protected override IEnumerable<IRelation> PerformGetAll(params int[] ids)
{
//TODO: Performance here is no good, we can easily do a query with an SQL 'IN' operator
// to acheive this!!!
if (ids.Any())
{
foreach (var id in ids)
@@ -82,6 +84,7 @@ namespace Umbraco.Core.Persistence.Repositories
var dtos = Database.Fetch<RelationDto>(sql);
//TODO: This is gonna be pretty horrible for performance, should really just convert the already fetched list.
foreach (var dto in dtos)
{
yield return Get(dto.Id);
@@ -148,5 +151,97 @@ namespace Umbraco.Core.Persistence.Repositories
}
#endregion
public IEnumerable<IRelation> GetByParentId(int id, string relationTypeAlias)
{
//TODO: Caching?
var relationType = _relationTypeRepository.GetByAlias(relationTypeAlias);
if (relationType == null)
{
return Enumerable.Empty<IRelation>();
}
var sql = new Sql();
sql.Select("umbracoRelation.*")
.From<RelationDto>()
.InnerJoin<RelationTypeDto>()
.On<RelationDto, RelationTypeDto>(dto => dto.RelationType, dto => dto.Id)
.Where<RelationDto>(dto => dto.ParentId == id)
.Where<RelationTypeDto>(dto => dto.Alias == relationTypeAlias);
var factory = new RelationFactory(relationType);
return Database.Fetch<RelationDto>(sql).Select(factory.BuildEntity).ToArray();
}
public IEnumerable<IRelation> GetByParentIds(int[] ids, string relationTypeAlias)
{
//TODO: Caching?
var relationType = _relationTypeRepository.GetByAlias(relationTypeAlias);
if (relationType == null)
{
return Enumerable.Empty<IRelation>();
}
var sql = new Sql();
sql.Select("umbracoRelation.*")
.From<RelationDto>()
.InnerJoin<RelationTypeDto>()
.On<RelationDto, RelationTypeDto>(dto => dto.RelationType, dto => dto.Id)
.Where("umbracoRelation.parentId IN (@ids)", new {ids = ids})
.Where<RelationTypeDto>(dto => dto.Alias == relationTypeAlias);
var factory = new RelationFactory(relationType);
return Database.Fetch<RelationDto>(sql).Select(factory.BuildEntity).ToArray();
}
public IEnumerable<IRelation> GetByChildIds(int[] ids, string relationTypeAlias)
{
//TODO: Caching?
var relationType = _relationTypeRepository.GetByAlias(relationTypeAlias);
if (relationType == null)
{
return Enumerable.Empty<IRelation>();
}
var sql = new Sql();
sql.Select("umbracoRelation.*")
.From<RelationDto>()
.InnerJoin<RelationTypeDto>()
.On<RelationDto, RelationTypeDto>(dto => dto.RelationType, dto => dto.Id)
.Where("umbracoRelation.childId IN (@ids)", new { ids = ids })
.Where<RelationTypeDto>(dto => dto.Alias == relationTypeAlias);
var factory = new RelationFactory(relationType);
return Database.Fetch<RelationDto>(sql).Select(factory.BuildEntity).ToArray();
}
public IEnumerable<IRelation> GetByParentOrChildId(int id, string relationTypeAlias)
{
//TODO: Caching?
var relationType = _relationTypeRepository.GetByAlias(relationTypeAlias);
if (relationType == null)
{
return Enumerable.Empty<IRelation>();
}
var sql = new Sql();
sql.Select("umbracoRelation.*")
.From<RelationDto>()
.InnerJoin<RelationTypeDto>()
.On<RelationDto, RelationTypeDto>(dto => dto.RelationType, dto => dto.Id)
.Where<RelationDto>(dto => dto.ParentId == id || dto.ChildId == id)
.Where<RelationTypeDto>(dto => dto.Alias == relationTypeAlias);
var factory = new RelationFactory(relationType);
return Database.Fetch<RelationDto>(sql).Select(factory.BuildEntity).ToArray();
}
}
}
@@ -141,5 +141,22 @@ namespace Umbraco.Core.Persistence.Repositories
}
#endregion
public IRelationType GetByAlias(string relationTypeAlias)
{
var sql = new Sql();
sql.Select("*")
.From<RelationTypeDto>()
.Where<RelationTypeDto>(d => d.Alias == relationTypeAlias);
var factory = new RelationTypeFactory();
var dto = Database.FirstOrDefault<RelationTypeDto>(sql);
//TODO: Caching?
return dto == null
? null
: factory.BuildEntity(dto);
}
}
}
@@ -226,6 +226,12 @@ namespace Umbraco.Core.Persistence.Repositories
try
{
PersistNewItem((TEntity)entity);
//TODO: Currently the repositories are handling a caching layer BUT if this transaction fails
// the repositories are still clearing their cache whereas their cache should only be refreshed
// after the transaction is completed - need to re-visit how caching is working in repo's
// This executes BEFORE the transaction is complete!
_cache.Save(typeof(TEntity), entity);
}
catch (Exception)
@@ -247,6 +253,12 @@ namespace Umbraco.Core.Persistence.Repositories
try
{
PersistUpdatedItem((TEntity)entity);
//TODO: Currently the repositories are handling a caching layer BUT if this transaction fails
// the repositories are still clearing their cache whereas their cache should only be refreshed
// after the transaction is completed - need to re-visit how caching is working in repo's
// This executes BEFORE the transaction is complete!
_cache.Save(typeof(TEntity), entity);
}
catch (Exception)
@@ -266,6 +278,12 @@ namespace Umbraco.Core.Persistence.Repositories
public virtual void PersistDeletedItem(IEntity entity)
{
PersistDeletedItem((TEntity)entity);
//TODO: Currently the repositories are handling a caching layer BUT if this transaction fails
// the repositories are still clearing their cache whereas their cache should only be refreshed
// after the transaction is completed - need to re-visit how caching is working in repo's
// This executes BEFORE the transaction is complete!
_cache.Delete(typeof(TEntity), entity);
}
@@ -105,6 +105,17 @@ namespace Umbraco.Core.Persistence.UnitOfWork
/// </param>
internal void Commit(Action<UmbracoDatabase> transactionCompleting)
{
//TODO: Currently the repositories are handling a caching layer BUT if this transaction fails
// the repositories are still clearing their cache whereas their cache should only be refreshed
// after the transaction is completed - need to re-visit how caching is working in repo's
//One way to acheive this could be to return some instructions/results from the "Persist..." methods
// then pass these results back to the repository after the transaction is complete and they can
// take care of cache refreshing there. The benefit of this is that these operations could return multiple
// items to cache refresh since any given operation could affect multiple items, NOT just one!
//In the meantime we have to do the regular cache refresher thing but that means having to re-lookup the variants for each
// content item and then clear their cache. It might just be possible to simplify cache refreshers by having the
// repositories return a result of all entities that have been modified and just use that to refresh the cache.
using (var transaction = Database.GetTransaction())
{
foreach (var operation in _operations.OrderBy(o => o.ProcessDate))
+116 -17
View File
@@ -9,6 +9,8 @@ using Umbraco.Core.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Models.Rdbms;
using Umbraco.Core.Persistence;
@@ -73,6 +75,68 @@ namespace Umbraco.Core.Services
_repositoryFactory = repositoryFactory;
_dataTypeService = dataTypeService;
}
public IContent CreateContentVariant(IContent masterContent, string variantKey, bool copyPropertyData = true, int userId = 0)
{
Mandate.ParameterNotNull(masterContent, "masterContent");
Mandate.ParameterNotNullOrEmpty(variantKey, "variantKey");
var contentType = masterContent.ContentType;
Content content;
if (copyPropertyData)
{
content = (Content)masterContent.DeepCloneWithResetIdentities();
// A copy should never be set to published automatically even if the original was.
content.ChangePublishedState(PublishedState.Unpublished);
content.VariantInfo = new VariantInfo(masterContent.Id, variantKey);
if (Copying.IsRaisedEventCancelled(new CopyEventArgs<IContent>(masterContent, content, masterContent.ParentId), this))
{
return null;
}
}
else
{
content = new Content(masterContent.Name, masterContent.ParentId, contentType, new PropertyCollection(), new VariantInfo(masterContent.Id, variantKey))
{
CreatorId = userId,
WriterId = userId
};
if (Saving.IsRaisedEventCancelled(new SaveEventArgs<IContent>(content), this))
{
content.WasCancelled = true;
return content;
}
}
var uow = _uowProvider.GetUnitOfWork();
using (var repository = _repositoryFactory.CreateContentRepository(uow))
{
content.CreatorId = userId;
content.WriterId = userId;
repository.AddOrUpdate(content);
//Generate a new preview
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c));
uow.Commit();
}
if (copyPropertyData)
{
Copied.RaiseEvent(new CopyEventArgs<IContent>(masterContent, content, false, masterContent.ParentId), this);
}
else
{
Created.RaiseEvent(new NewEventArgs<IContent>(content, false, contentType.Alias, masterContent.ParentId), this);
Saved.RaiseEvent(new SaveEventArgs<IContent>(content, false), this);
}
Audit.Add(AuditTypes.New, string.Format("Content variant '{0}' was created", content.Name), content.CreatorId, content.Id);
return content;
}
/// <summary>
/// Assigns a single permission to the current content item for the specified user ids
@@ -216,10 +280,10 @@ namespace Umbraco.Core.Services
uow.Commit();
}
Saved.RaiseEvent(new SaveEventArgs<IContent>(content, false), this);
Created.RaiseEvent(new NewEventArgs<IContent>(content, false, contentTypeAlias, parentId), this);
Saved.RaiseEvent(new SaveEventArgs<IContent>(content, false), this);
Audit.Add(AuditTypes.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id);
return content;
@@ -297,6 +361,9 @@ namespace Umbraco.Core.Services
/// <returns><see cref="IContent"/></returns>
public IEnumerable<IContent> GetByIds(IEnumerable<int> ids)
{
//if we don't do this, it will return ALL of the content
if (ids.Any() == false) return Enumerable.Empty<IContent>();
using (var repository = _repositoryFactory.CreateContentRepository(_uowProvider.GetUnitOfWork()))
{
return repository.GetAll(ids.ToArray());
@@ -1599,11 +1666,10 @@ namespace Umbraco.Core.Services
x => x.Success || x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished))
{
item.Result.ContentItem.WriterId = userId;
repository.AddOrUpdate(item.Result.ContentItem);
//add or update a preview
repository.AddOrUpdatePreviewXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, c));
//add or update the published xml
repository.AddOrUpdateContentXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, c));
//add or update the item, xml structures and variants
AddOrUpdate(repository, item.Result.ContentItem, true);
updated.Add(item.Result.ContentItem);
}
@@ -1710,17 +1776,9 @@ namespace Umbraco.Core.Services
}
content.WriterId = userId;
repository.AddOrUpdate(content);
//Generate a new preview
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c));
//add or update content, xml structures and variants
AddOrUpdate(repository, content, published);
if (published)
{
//Content Xml
repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c));
}
uow.Commit();
}
@@ -1878,6 +1936,47 @@ namespace Umbraco.Core.Services
}
}
private void AddOrUpdate(IContentRepository repository, IContent content, bool isPublished)
{
//add or update the item
repository.AddOrUpdate(content);
//add or update a preview
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c));
if (isPublished)
{
//add or update the published xml
repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, c));
}
//If the master name has changed, then change all variants too.
// NOTE: We do this to keep the variant document name's in-sync with the master
// when we do an AddOrUpdate on a variant in the repository it will always ensure to
// set it's name to it's master
if (content.VariantInfo.IsVariant == false)
{
if (((ICanBeDirty)content).IsPropertyDirty("Name"))
{
if (content.VariantInfo.VariantIds.Any())
{
var variants = repository.GetAll(content.VariantInfo.VariantIds);
foreach (var variant in variants)
{
//add or update the item
repository.AddOrUpdate(variant);
//add or update a preview
repository.AddOrUpdatePreviewXml(variant, c => _entitySerializer.Serialize(this, _dataTypeService, c));
if (isPublished)
{
//add or update the published xml
repository.AddOrUpdateContentXml(variant, c => _entitySerializer.Serialize(this, _dataTypeService, c));
}
}
}
}
}
}
#endregion
#region Proxy Event Handlers
@@ -626,6 +626,7 @@ namespace Umbraco.Core.Services
{
strictSchemaBuilder.AppendLine(String.Format("<!ELEMENT {0} ANY>", safeAlias));
strictSchemaBuilder.AppendLine(String.Format("<!ATTLIST {0} id ID #REQUIRED>", safeAlias));
}
}
@@ -39,6 +39,15 @@ namespace Umbraco.Core.Services
xml.Add(new XAttribute("template", content.Template == null ? "0" : content.Template.Id.ToString(CultureInfo.InvariantCulture)));
xml.Add(new XAttribute("nodeTypeAlias", content.ContentType.Alias));
if (content.VariantInfo.IsVariant)
{
//normally this is empty, now we can use the value
xml.Attribute("isDoc").Value = "variant";
xml.Add(new XAttribute("masterDocId", content.VariantInfo.MasterDocId));
xml.Add(new XAttribute("variantKey", content.VariantInfo.Key));
}
if (deep)
{
var descendants = contentService.GetDescendants(content).ToArray();
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Publishing;
@@ -11,6 +12,9 @@ namespace Umbraco.Core.Services
/// </summary>
public interface IContentService : IService
{
IContent CreateContentVariant(IContent masterContent, string variantKey, bool copyPropertyData = true, int userId = 0);
/// <summary>
/// Assigns a single permission to the current content item for the specified user ids
/// </summary>
+34 -1
View File
@@ -34,7 +34,7 @@ namespace Umbraco.Core.Services
/// <param name="ids">Optional array of integer ids to return relations for</param>
/// <returns>An enumerable list of <see cref="Relation"/> objects</returns>
IEnumerable<IRelation> GetAllRelations(params int[] ids);
/// <summary>
/// Gets all <see cref="Relation"/> objects by their <see cref="RelationType"/>
/// </summary>
@@ -63,6 +63,30 @@ namespace Umbraco.Core.Services
/// <returns>An enumerable list of <see cref="Relation"/> objects</returns>
IEnumerable<IRelation> GetByParentId(int id);
/// <summary>
/// Gets a list of <see cref="Relation"/> objects by their parent Id for a certain relation type
/// </summary>
/// <param name="id">Id of the parent to retrieve relations for</param>
/// <param name="relationTypeAlias"></param>
/// <returns>An enumerable list of <see cref="Relation"/> objects</returns>
IEnumerable<IRelation> GetByParentId(int id, string relationTypeAlias);
/// <summary>
/// Gets a list of <see cref="Relation"/> objects by any of parent Ids for a certain relation type
/// </summary>
/// <param name="ids">Ids of any parent to retrieve relations for</param>
/// <param name="relationTypeAlias"></param>
/// <returns>An enumerable list of <see cref="Relation"/> objects</returns>
IEnumerable<IRelation> GetByParentIds(int[] ids, string relationTypeAlias);
/// <summary>
/// Gets a list of <see cref="Relation"/> objects by any of child Ids for a certain relation type
/// </summary>
/// <param name="ids">Ids of any child to retrieve relations for</param>
/// <param name="relationTypeAlias"></param>
/// <returns>An enumerable list of <see cref="Relation"/> objects</returns>
IEnumerable<IRelation> GetByChildIds(int[] ids, string relationTypeAlias);
/// <summary>
/// Gets a list of <see cref="Relation"/> objects by their parent entity
/// </summary>
@@ -108,6 +132,15 @@ namespace Umbraco.Core.Services
/// <returns>An enumerable list of <see cref="Relation"/> objects</returns>
IEnumerable<IRelation> GetByParentOrChildId(int id);
/// <summary>
/// Gets a list of <see cref="Relation"/> objects by their child or parent Id.
/// Using this method will get you all relations regards of it being a child or parent relation.
/// </summary>
/// <param name="id">Id of the child or parent to retrieve relations for</param>
/// <param name="relationTypeAlias"></param>
/// <returns>An enumerable list of <see cref="Relation"/> objects</returns>
IEnumerable<IRelation> GetByParentOrChildId(int id, string relationTypeAlias);
/// <summary>
/// Gets a list of <see cref="Relation"/> objects by the Name of the <see cref="RelationType"/>
/// </summary>
@@ -128,6 +128,30 @@ namespace Umbraco.Core.Services
}
}
public IEnumerable<IRelation> GetByParentId(int id, string relationTypeAlias)
{
using (var repository = _repositoryFactory.CreateRelationRepository(_uowProvider.GetUnitOfWork()))
{
return repository.GetByParentId(id, relationTypeAlias);
}
}
public IEnumerable<IRelation> GetByParentIds(int[] ids, string relationTypeAlias)
{
using (var repository = _repositoryFactory.CreateRelationRepository(_uowProvider.GetUnitOfWork()))
{
return repository.GetByParentIds(ids, relationTypeAlias);
}
}
public IEnumerable<IRelation> GetByChildIds(int[] ids, string relationTypeAlias)
{
using (var repository = _repositoryFactory.CreateRelationRepository(_uowProvider.GetUnitOfWork()))
{
return repository.GetByChildIds(ids, relationTypeAlias);
}
}
/// <summary>
/// Gets a list of <see cref="Relation"/> objects by their parent entity
/// </summary>
@@ -146,6 +170,7 @@ namespace Umbraco.Core.Services
/// <returns>An enumerable list of <see cref="Relation"/> objects</returns>
public IEnumerable<IRelation> GetByParent(IUmbracoEntity parent, string relationTypeAlias)
{
//TODO: Fix this, it should be filtering via SQL
return GetByParent(parent).Where(relation => relation.RelationType.Alias == relationTypeAlias);
}
@@ -199,6 +224,14 @@ namespace Umbraco.Core.Services
}
}
public IEnumerable<IRelation> GetByParentOrChildId(int id, string relationTypeAlias)
{
using (var repository = _repositoryFactory.CreateRelationRepository(_uowProvider.GetUnitOfWork()))
{
return repository.GetByParentOrChildId(id, relationTypeAlias);
}
}
/// <summary>
/// Gets a list of <see cref="Relation"/> objects by the Name of the <see cref="RelationType"/>
/// </summary>
+4 -1
View File
@@ -338,6 +338,7 @@
<Compile Include="Models\ContentTypeCompositionBase.cs" />
<Compile Include="Models\ContentTypeExtensions.cs" />
<Compile Include="Models\ContentTypeSort.cs" />
<Compile Include="Models\ContentVariations\VariantInfo.cs" />
<Compile Include="Models\ContentXmlEntity.cs" />
<Compile Include="Models\DeepCloneHelper.cs" />
<Compile Include="Models\IDeepCloneable.cs" />
@@ -1174,7 +1175,9 @@
<ItemGroup>
<Content Include="Strings\Notes.txt" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Folder Include="Persistence\Migrations\Upgrades\TargetVersionEight\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+14
View File
@@ -289,5 +289,19 @@ namespace Umbraco.Core
}
}
/// <summary>
/// Converts an XContainer to an XmlNode for the given xml document
/// </summary>
/// <param name="element"></param>
/// <param name="xmlDoc"></param>
/// <returns></returns>
public static XmlNode ToXmlNode(this XContainer element, XmlDocument xmlDoc)
{
using (XmlReader xmlReader = element.CreateReader())
{
return xmlDoc.ReadNode(xmlReader);
}
}
}
}
+16
View File
@@ -400,5 +400,21 @@ namespace Umbraco.Core
var d = m.Cast<Match>().ToDictionary(attributeSet => attributeSet.Groups["attributeName"].Value.ToString().ToLower(), attributeSet => attributeSet.Groups["attributeValue"].Value.ToString());
return d;
}
public static XmlElement CloneElement(XmlElement current, XmlDocument doc, string prefix, string ns)
{
var clone = doc.CreateElement(prefix, current.LocalName, ns);
foreach (XmlAttribute att in current.Attributes)
clone.SetAttribute(att.Name, att.Value);
foreach (XmlElement el in current.ChildNodes)
{
var newDatael = doc.ImportNode(el, true);
clone.AppendChild(newDatael);
}
return clone;
}
}
}
@@ -7,6 +7,7 @@ using System.Web.Mvc;
using System.Web.Routing;
using System.Xml;
using NUnit.Framework;
using Umbraco.Web.Routing.Segments;
using Umbraco.Web.Security;
using umbraco.BusinessLogic;
using Umbraco.Core;
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using NUnit.Framework;
using Umbraco.Web.Models.Segments;
using Umbraco.Web.Routing.Segments;
namespace Umbraco.Tests.Routing
{
//[TestFixture]
//public class ContentSegmentProviderTests
//{
// [Test]
// public void Get_Advertised_Segments()
// {
// var provider = new MyTestProvider();
// Assert.AreEqual(3, provider.SegmentKeysToPersist.Count());
// }
// private class MyTestProvider : ContentSegmentProvider
// {
// public override SegmentCollection GetSegmentsForRequest(Uri originalRequestUrl, Uri cleanedRequestUrl, HttpRequestBase httpRequest)
// {
// return new SegmentCollection(Enumerable.Empty<Segment>());
// }
// }
//}
}
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Moq;
using NUnit.Framework;
using Umbraco.Web.Models.Segments;
using Umbraco.Web.Routing;
using Umbraco.Web.Routing.Segments;
namespace Umbraco.Tests.Routing
{
[TestFixture]
public class ContentSegmentsTests
{
[Test]
public void Get_All_Segments_For_Request()
{
var result = RequestSegments.GetAllSegmentsForRequest(new ContentSegmentProvider[]
{
new MyTestProvider1(),
new MyTestProvider2()
}, new Uri("http://localhost/test?blah=1"), new Uri("http://localhost/test/"),
Mock.Of<HttpRequestBase>(req => req.Cookies == new HttpCookieCollection()),
new Dictionary<string, bool>
{
{typeof(MyTestProvider1).FullName, true},
{typeof(MyTestProvider2).FullName, true}
});
Assert.AreEqual(4, result.Count());
Assert.AreEqual(true, result["key1"].Value);
Assert.AreEqual("blah", result["key2"].Value);
Assert.AreEqual(9876, result["key3"].Value);
Assert.AreEqual(123, result["key4"].Value);
}
[Test]
public void Get_All_Segments_For_Request_With_Approval()
{
var result = RequestSegments.GetAllSegmentsForRequest(new ContentSegmentProvider[]
{
new MyTestProvider1(),
new MyTestProvider2()
}, new Uri("http://localhost/test?blah=1"), new Uri("http://localhost/test/"),
Mock.Of<HttpRequestBase>(req => req.Cookies == new HttpCookieCollection()),
new Dictionary<string, bool>
{
{typeof(MyTestProvider1).FullName, true},
{typeof(MyTestProvider2).FullName, false}
});
Assert.AreEqual(3, result.Count());
}
private class MyTestProvider1 : ContentSegmentProvider
{
public override SegmentCollection GetSegmentsForRequest(Uri originalRequestUrl, Uri cleanedRequestUrl, HttpRequestBase httpRequest)
{
return new SegmentCollection(new List<Segment>
{
new Segment("key1", true),
new Segment("key2", "blahdd"),
new Segment("key3", 9876)
});
}
}
private class MyTestProvider2 : ContentSegmentProvider
{
public override SegmentCollection GetSegmentsForRequest(Uri originalRequestUrl, Uri cleanedRequestUrl, HttpRequestBase httpRequest)
{
return new SegmentCollection(new List<Segment>
{
new Segment("key1", true),
new Segment("key2", "blah"),
new Segment("key4", 123)
});
}
}
}
}
@@ -1,4 +1,5 @@
using Moq;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Tests.TestHelpers;
@@ -6,6 +7,7 @@ using Umbraco.Tests.TestHelpers.Stubs;
using Umbraco.Web.Routing;
using umbraco.BusinessLogic;
using umbraco.cms.businesslogic.template;
using Umbraco.Web.Routing.Segments;
namespace Umbraco.Tests.Routing
{
@@ -8,6 +8,7 @@ using Umbraco.Tests.TestHelpers.Stubs;
using Umbraco.Web;
using Umbraco.Web.PublishedCache.XmlPublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Routing.Segments;
namespace Umbraco.Tests.TestHelpers
{
+2
View File
@@ -169,6 +169,7 @@
<Compile Include="AngularIntegration\JsInitializationTests.cs" />
<Compile Include="AngularIntegration\ServerVariablesParserTests.cs" />
<Compile Include="AttemptTests.cs" />
<Compile Include="Routing\ContentSegmentProviderTests.cs" />
<Compile Include="Membership\DynamicMemberContentTests.cs" />
<Compile Include="Membership\MembershipProviderBaseTests.cs" />
<Compile Include="Membership\UmbracoServiceMembershipProviderTests.cs" />
@@ -317,6 +318,7 @@
<Compile Include="PublishedContent\StronglyTypedModels\Textpage.cs" />
<Compile Include="PublishedContent\StronglyTypedModels\TypedModelBase.cs" />
<Compile Include="PublishedContent\StronglyTypedModels\UmbracoTemplatePage`T.cs" />
<Compile Include="Routing\ContentSegmentsTests.cs" />
<Compile Include="Services\LocalizationServiceTests.cs" />
<Compile Include="Resolvers\XsltExtensionsResolverTests.cs" />
<Compile Include="Services\MemberServiceTests.cs" />
+1
View File
@@ -4,6 +4,7 @@ module.exports = function (grunt) {
// Default task.
grunt.registerTask('default', ['jshint:dev','build','karma:unit']);
grunt.registerTask('dev', ['jshint:dev', 'build', 'webserver', 'open:dev', 'watch']);
grunt.registerTask('vs', ['jshint:dev', 'build', 'watch']);
//run by the watch task
grunt.registerTask('watch-js', ['jshint:dev','concat','copy:app','copy:mocks','copy:packages','copy:vs','karma:unit']);
@@ -15,11 +15,13 @@ angular.module("umbraco.directives")
templateUrl: 'views/directives/umb-content-name.html',
scope: {
placeholder: '@placeholder',
model: '=ngModel'
model: '=ngModel',
readonly: '='
},
link: function(scope, element, attrs, ngModel) {
var inputElement = element.find("input");
var inputElement = null;
if(scope.placeholder && scope.placeholder[0] === "@"){
localizationService.localize(scope.placeholder.substring(1))
.then(function(value){
@@ -39,6 +41,12 @@ angular.module("umbraco.directives")
}
var mouseMoveDebounce = _.throttle(function (e) {
//don't process if it's null
if (!inputElement || inputElement.length === 0) {
return;
}
mX = e.pageX;
mY = e.pageY;
// not focused and not over element
@@ -62,18 +70,25 @@ angular.module("umbraco.directives")
})();
$timeout(function(){
$timeout(function () {
inputElement = element.find("input");
if(!scope.model){
scope.goEdit();
}
}, 100, false);
scope.goEdit = function(){
scope.editMode = true;
$timeout(function () {
inputElement.focus();
}, 100, false);
if (inputElement && inputElement.length > 0) {
$timeout(function () {
inputElement.focus();
}, 100, false);
}
};
scope.exitEdit = function(){
@@ -3,8 +3,8 @@
* @name umbraco.directives.directive:preventDefault
**/
angular.module("umbraco.directives")
.directive('preventDefault', function() {
return function(scope, element, attrs) {
.directive('preventDefault', function () {
return function (scope, element, attrs) {
var enabled = true;
//check if there's a value for the attribute, if there is and it's false then we conditionally don't
@@ -0,0 +1,29 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:stopPropagation
**/
angular.module("umbraco.directives")
.directive('stopPropagation', function () {
return function (scope, element, attrs) {
var enabled = true;
//check if there's a value for the attribute, if there is and it's false then we conditionally don't
//stop propogation
if (attrs.preventDefault) {
attrs.$observe("stopPropagation", function (newVal) {
enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true;
});
}
$(element).click(function (event) {
if (event.metaKey || event.ctrlKey) {
return;
}
else {
if (enabled === true) {
event.stopPropagation();
}
}
});
};
});
@@ -8,29 +8,33 @@
**/
function valTab() {
return {
require: "^form",
//this means the form is optional and will not throw an exception if not found
// if the form is not found, then this directive will not do anything
require: "^?form",
restrict: "A",
link: function (scope, element, attr, formCtrl) {
var tabId = "tab" + scope.tab.id;
scope.tabHasError = false;
if (formCtrl) {
var tabId = "tab" + scope.tab.id;
//listen for form validation changes
scope.$on("valStatusChanged", function(evt, args) {
if (!args.form.$valid) {
var tabContent = element.closest(".umb-panel").find("#" + tabId);
//check if the validation messages are contained inside of this tabs
if (tabContent.find(".ng-invalid").length > 0) {
scope.tabHasError = true;
} else {
scope.tabHasError = false;
//listen for form validation changes
scope.$on("valStatusChanged", function (evt, args) {
if (!args.form.$valid) {
var tabContent = element.closest(".umb-panel").find("#" + tabId);
//check if the validation messages are contained inside of this tabs
if (tabContent.find(".ng-invalid").length > 0) {
scope.tabHasError = true;
} else {
scope.tabHasError = false;
}
}
else {
scope.tabHasError = false;
}
}
else {
scope.tabHasError = false;
}
});
});
}
}
};
@@ -630,6 +630,23 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
[{ id: id }])),
'Failed to publish content with id ' + id);
},
createVariant: function(id, key) {
if (!id) {
throw "id cannot be null";
}
if (!key) {
throw "key cannot be null";
}
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"PostCreateVariant",
[{ id: id }, {key: key}])),
'Failed to create a content variant for master doc id ' + id + " and key " + key);
}
@@ -276,11 +276,10 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS
* @param {object} treeNode the node to remove
*/
removeNode: function(treeNode) {
if (treeNode.parent() == null) {
throw "Cannot remove a node that doesn't have a parent";
if (angular.isFunction(treeNode.parent) && treeNode.parent() != null) {
//remove the current item from it's siblings
treeNode.parent().children.splice(treeNode.parent().children.indexOf(treeNode), 1);
}
//remove the current item from it's siblings
treeNode.parent().children.splice(treeNode.parent().children.indexOf(treeNode), 1);
},
/**
@@ -83,4 +83,5 @@
@import "property-editors.less";
@import "typeahead.less";
@import "dashboards.less";
@import "hacks.less";
@@ -0,0 +1,22 @@
table.segment-dashboard td:first-child{
width:85px;
}
table.segment-dashboard td:nth-child(2) {
width:500px;
}
table.segment-dashboard td small span {
color:@grayLight;
}
table.segment-dashboard td small {
color:@gray;
}
.segment-config .umb-pane {
border-bottom:solid @gray 1px;
position:relative;
}
.segment-config .umb-pane button {
top: 4px;
left: 550px;
position: absolute;
}
@@ -84,4 +84,9 @@ iframe, .content-column-body {
}
.icon-chevron-down:before {
content: "\e0c9";
}
.caret.caret-reversed {
border-top-width: 0;
border-bottom: 4px solid #000000;
}
+58 -3
View File
@@ -16,6 +16,10 @@
left: 0px;
}
.umb-panel-header .span7 {
position:relative;
}
.umb-panel-body {
top: 101px;
left: 0px;
@@ -44,15 +48,19 @@
margin: 15px 0 0 20px;
padding: 3px 5px;
height: auto;
width: 100%;
border: 1px solid @grayLighter;
}
.umb-panel-header .umb-headline:focus,.umb-panel-header .umb-headline:active {
.umb-panel-header input.umb-headline:focus,.umb-panel-header input.umb-headline:active {
border: 1px solid @grayLight;
background-color: @white;
}
.umb-panel-header div.umb-headline:hover,.umb-panel-header div.umb-headline:active {
border-bottom: none;
color:@gray;
}
.umb-headline-editor-wrapper{
position: relative;
}
@@ -252,4 +260,51 @@
.umb-panel .text-success { color: @formSuccessText; }
.umb-panel a.text-success:hover,
.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); }
.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); }
//Don't know where else to put this, putting it here for now, more or less this file seems
// to be the stying for 'editors'
.umb-segment-selector {
position:absolute;
left:20px;
z-index:2345;
top:15px;
width: 100%;
}
.umb-segment-selector .dropdown-toggle:hover *,
.umb-segment-selector .dropdown-toggle{
text-decoration: none !important;
outline: none;
}
/*.umb-panel-header .umb-content-headline:hover .umb-segment-selector {
top:15px;
transition: opacity .5s ease-in;
opacity: 1;
}*/
.umb-segment-selector ul {
padding: 0;
border-radius: 0;
box-shadow: none;
width:100%;
}
.umb-segment-selector ul i {
color:@blue;
}
.umb-segment-selector li.trashed a, .umb-segment-selector li.trashed i, .umb-segment-selector li.trashed span{
color:@grayLight;
}
.umb-content-headline > small {
position: absolute;
top: 40px;
left: 28px;
}
.umb-content-headline > small span{
color:@blue;
}
@@ -488,3 +488,31 @@ body.touch .umb-actions a{
body.touch a.umb-options i {margin-top: 20px;}
/* no idea where to put this, for drop down menus */
.dropdown-menu.umb-actions li{
position:relative;
}
.dropdown-menu.umb-actions li a{
padding-left: 50px;
}
.dropdown-menu.umb-actions li button{
margin:10px;
}
.dropdown-menu.umb-actions li small {
display:block;
color:@grayLight;
}
.dropdown-menu.umb-actions li div.checkbox-container {
position: absolute;
top: 0;
left: 0;
width: 45px;
height: 45px;
}
.dropdown-menu.umb-actions li div.checkbox-container input{
display: block;
margin: 18px;
float: none;
}
@@ -1,4 +1,4 @@
<form ng-controller="Umbraco.DashboardController" class="umb-dashboard" val-form-manager>
<div ng-controller="Umbraco.DashboardController" class="umb-dashboard">
<umb-panel>
<umb-header tabs="dashboard.tabs">
<div class="umb-headline-editor-wrapper span12">
@@ -28,4 +28,4 @@
</umb-tab>
</umb-tab-view>
</umb-panel>
</form>
</div>
@@ -0,0 +1,6 @@
<div ng-controller="Umbraco.Editors.Content.CompareController">
<div ng-repeat="id in ids" ng-include="'views/content/edit.html'">
</div>
</div>
@@ -0,0 +1,16 @@
/**
* @ngdoc controller
* @name Umbraco.Editors.Content.CompareController
* @function
*
* @description
* The controller for the content variant comparing
*/
function ContentCompareController($scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http, $location) {
//setup scope vars
$scope.ids = $routeParams.id.split(",");
}
angular.module("umbraco").controller("Umbraco.Editors.Content.CompareController", ContentCompareController);
@@ -6,7 +6,7 @@
* @description
* The controller for deleting content
*/
function ContentDeleteController($scope, contentResource, treeService, navigationService) {
function ContentDeleteController($scope, contentResource, treeService, navigationService, $location) {
$scope.performDelete = function() {
@@ -24,7 +24,17 @@ function ContentDeleteController($scope, contentResource, treeService, navigatio
//ensure the recycle bin has child nodes now
var recycleBin = treeService.getDescendantNode(rootNode, -20);
recycleBin.hasChildren = true;
//this could be null if the current node is a variant (i.e. it doesn't actually exist in the tree)
if (recycleBin) {
recycleBin.hasChildren = true;
}
if ($scope.currentNode.metaData && $scope.currentNode.metaData.masterDocId) {
//this could be a variant, if it is then we should navigate to it's master
$location.path("content/content/edit/" + $scope.currentNode.metaData.masterDocId);
}
navigationService.hideMenu();
});
@@ -6,7 +6,7 @@
* @description
* The controller for the content editor
*/
function ContentEditController($scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) {
function ContentEditController($scope, $element, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http, $location) {
//setup scope vars
$scope.defaultButton = null;
@@ -15,7 +15,6 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS
$scope.currentSection = appState.getSectionState("currentSection");
$scope.currentNode = null; //the editors affiliated node
//This sets up the action buttons based on what permissions the user has.
//The allowedActions parameter contains a list of chars, each represents a button by permission so
//here we'll build the buttons according to the chars of the user.
@@ -121,8 +120,8 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS
/** Syncs the content item to it's tree node - this occurs on first load and after saving */
function syncTreeNode(content, path, initialLoad) {
//If this is a child of a list view then we can't actually sync the real tree
if (!$scope.content.isChildOfListView) {
//If this is a child of a list view or a variant then we can't actually sync the real tree
if (!$scope.content.isChildOfListView && !$scope.content.masterDocId) {
navigationService.syncTree({ tree: "content", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) {
$scope.currentNode = syncArgs.node;
});
@@ -201,8 +200,15 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS
});
}
else {
//we are editing so get the content item from the server
contentResource.getById($routeParams.id)
//When we are comparing content variants, we will render this template in an iteration so we need to check
// where to get the id from, either the routeParams or from the current scope
var id = (angular.isDefined($scope.$index) && angular.isDefined($scope.id)) ? $scope.id : $routeParams.id;
contentResource.getById(id)
.then(function (data) {
$scope.loaded = true;
$scope.content = data;
@@ -219,9 +225,27 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS
syncTreeNode($scope.content, data.path, true);
});
_.each($scope.content.variants, function(i) {
i.compare = true;
});
$scope.showCompare = true;
});
}
$scope.compareVariants = function () {
var ids = _.map($scope.content.variants, function(i) {
return i.id;
});
$location.path("content/content/compare/" + ids.join(","));
}
$scope.toggleCompare = function(variant) {
$scope.showCompare = _.find($scope.content.variants, function (i) {
return i.compare === true;
});
}
$scope.unPublish = function () {
@@ -284,6 +308,29 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS
}
};
$scope.selectVariant = function (v)
{
if (v.exists) {
$location.path("content/content/edit/" + v.id);
}
else {
//we need to create it, for now we'll use a normal prompt
if (confirm("Are you sure you wish to create this content variant? (" + v.key + ")")) {
contentResource.createVariant($scope.content.id, v.key)
.then(function (newId) {
$location.path("content/content/edit/" + newId);
});
}
}
}
$scope.toMasterDoc = function() {
$location.path("content/content/edit/" + $scope.content.masterDocId);
}
}
angular.module("umbraco").controller("Umbraco.Editors.Content.EditController", ContentEditController);
@@ -1,39 +1,122 @@
<form novalidate name="contentForm"
ng-controller="Umbraco.Editors.Content.EditController"
ng-show="loaded"
ng-submit="save()"
val-form-manager>
ng-controller="Umbraco.Editors.Content.EditController"
ng-show="loaded"
ng-submit="save()"
val-form-manager>
<umb-panel ng-class="'editor-breadcrumb'">
<umb-header tabs="content.tabs">
<div class="span7">
<umb-content-name
placeholder="@placeholders_entername"
ng-model="content.name"/>
<div class="span7 umb-content-headline">
<!--
<small ng-if="content.masterDocId != null">
Variant: <span>{{content.variantKey}}</span>
</small>
<div class="umb-segment-selector" ng-if="content.masterDocId != null">
<a class="btn" href="" ng-click="toMasterDoc()">
<small>Back to master doc</small>
<span class="caret caret-reversed"></span>
</a>
</div>
-->
<div class="umb-segment-selector"
ng-if="content.variants.length > 0 || content.masterDocId != null">
<a href style="float: right" class="btn btn-link dropdown-toggle" data-toggle="dropdown">
<i class="icon-globe"></i>
<span class="caret"></span>
</a>
<!--
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<small>Variants</small>
<span class="caret"></span>
</a>
-->
<ul class="dropdown-menu umb-actions" role="menu">
<li class="action seb" ng-if="content.masterDocId != null">
<a style="padding-left: 20px" href="" ng-click="toMasterDoc()">
<i class="icon icon-reply-arrow"></i>
<div class="menu-label ng-binding">
Back to master document
<small>
</small>
</div>
</a>
</li>
<li
class="action"
ng-repeat="item in content.variants"
ng-class="{'trashed' : item.isTrashed}">
<ng-form name="itemCompare">
<a href=""
style="padding-left: 20px"
ng-attr-title="{{item.exists ? 'Edit variant' : 'Create variant' }}"
ng-click="selectVariant(item)">
<i class="icon"
ng-class="item.isTrashed ? 'icon-trash' : item.exists ? 'icon-edit' : 'icon-add'"></i>
<div class="menu-label ng-binding">
{{content.name}} - <span style="opacity: 0.8">{{item.name}}</span>
<small
ng-if="item.lastUpdated">Updated: {{item.lastUpdated | date : 'medium'}}
</small>
<small
ng-if="!item.lastUpdated">Create new variant
</small>
</div>
</a>
</ng-form>
</li>
</ul>
</div>
<umb-content-name readonly="content.masterDocId"
placeholder="@placeholders_entername"
ng-model="content.name" />
</div>
<div class="span5">
<div class="span5">
<div class="btn-toolbar pull-right umb-btn-toolbar">
<div class="btn-group" ng-animate="'fade'" ng-show="formStatus">
<p class="btn btn-link umb-status-label">{{formStatus}}</p>
</div>
<umb-options-menu ng-show="currentNode"
current-node="currentNode"
current-section="{{currentSection}}">
current-node="currentNode"
current-section="{{currentSection}}">
</umb-options-menu>
</div>
</div>
</div>
</div>
</umb-header>
<umb-tab-view>
<umb-tab id="tab{{tab.id}}" rel="{{tab.id}}" ng-repeat="tab in content.tabs">
<div class="umb-pane">
<umb-property
property="property"
ng-repeat="property in tab.properties">
<umb-property property="property"
ng-repeat="property in tab.properties">
<umb-editor model="property"></umb-editor>
</umb-property>
@@ -45,19 +128,19 @@
</a>
</div>
<div class="btn-group dropup" ng-if="defaultButton" >
<div class="btn-group dropup" ng-if="defaultButton">
<!-- primary button -->
<a class="btn btn-success" href="#" ng-click="performAction(defaultButton)" prevent-default>
<localize key="{{defaultButton.labelKey}}">{{defaultButton.labelKey}}</localize>
<localize key="{{defaultButton.labelKey}}">{{defaultButton.labelKey}}</localize>
</a>
<a class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-if="subButtons.length > 0">
<span class="caret"></span>
</a>
<!-- sub buttons -->
<ul class="dropdown-menu bottom-up" role="menu" aria-labelledby="dLabel" ng-if="subButtons.length > 0">
<ul class="dropdown-menu bottom-up" role="menu" aria-labelledby="dLabel" ng-if="subButtons.length > 0">
<li ng-repeat="btn in subButtons">
<a href="#" ng-click="performAction(btn)" prevent-default>
<localize key="{{btn.labelKey}}">{{btn.labelKey}}</localize>
@@ -65,7 +148,7 @@
</li>
</ul>
</div>
</div>
</div>
</div>
</umb-tab>
@@ -0,0 +1,121 @@
function segmentDashboardController($scope, umbRequestHelper, $log, $http, formHelper) {
function refreshData() {
return umbRequestHelper.resourcePromise(
$http.get(umbRequestHelper.getApiUrl("segmentDashboardApiBaseUrl", "GetProviders")),
"Failed to retrieve provider data")
.then(function(data) {
$scope.providers = data;
});
}
$scope.providers = [];
$scope.configuringSegments = false;
$scope.providerConfig = {
providerName: "",
providerTypeName: "",
}
$scope.segmentConfig = [];
$scope.variantConfig = [];
$scope.toggle = function(provider) {
umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl("segmentDashboardApiBaseUrl", "PostToggleProvider",
[{ typeName: provider.typeName }])),
'Failed to toggle segment provider')
.then(function() {
//toggle local var
provider.enabled = !provider.enabled;
});
}
$scope.showSegmentConfig = function (item) {
$scope.configuringSegments = true;
$scope.providerConfig.providerName = item.displayName;
$scope.providerConfig.providerTypeName = item.typeName;
$scope.segmentConfig = item.segmentConfig;
}
$scope.showVariantConfig = function (item) {
$scope.configuringVariants = true;
$scope.providerConfig.providerName = item.displayName;
$scope.providerConfig.providerTypeName = item.typeName;
$scope.variantConfig = item.variantConfig;
}
$scope.cancel = function () {
formHelper.resetForm({ scope: $scope });
$scope.configuringVariants = false;
$scope.configuringSegments = false;
$scope.providerConfig = {
providerName: "",
providerTypeName: "",
}
$scope.segmentConfig = [];
$scope.variantConfig = [];
}
$scope.addItem = function() {
$scope.segmentConfig.push({ matchExpression: "", key: "", value: "" });
}
$scope.removeItem = function(item) {
$scope.segmentConfig = _.reject($scope.segmentConfig, function (i) { return i === item; });
}
$scope.saveSegmentConfig = function (formCtrl) {
if (formHelper.submitForm({ scope: $scope, formCtrl: formCtrl })) {
umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl("segmentDashboardApiBaseUrl", "PostSaveProviderSegmentConfig",
[{ typeName: $scope.providerConfig.providerTypeName }]),
$scope.segmentConfig),
'Failed to save segment configuration')
.then(function () {
//now that its successful, save the persisted values back to the provider's values
var provider = _.find($scope.providers, function (i) { return i.typeName === $scope.providerConfig.providerTypeName; });
provider.segmentConfig = $scope.segmentConfig;
refreshData().then(function() {
//exit editing config
$scope.cancel();
});
});
}
}
$scope.saveVariantConfig = function (formCtrl) {
umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl("segmentDashboardApiBaseUrl", "PostSaveProviderVariantConfig",
[{ typeName: $scope.providerConfig.providerTypeName }]),
$scope.variantConfig),
'Failed to save variant configuration')
.then(function () {
//now that its successful, save the persisted values back to the provider's values
var provider = _.find($scope.providers, function (i) { return i.typeName === $scope.providerConfig.providerTypeName; });
provider.variantConfig = $scope.variantConfig;
refreshData().then(function () {
//exit editing config
$scope.cancel();
});
});
}
//initial data load
refreshData();
}
angular.module("umbraco").controller("Umbraco.Dashboard.SegmentDashboard", segmentDashboardController);
@@ -0,0 +1,104 @@
<div ng-controller="Umbraco.Dashboard.SegmentDashboard">
<table class="table segment-dashboard" ng-hide="configuringSegments || configuringVariants">
<tr ng-repeat="item in providers">
<td>
<button class="btn btn-primary" ng-if="!item.enabled" ng-click="toggle(item)">Enable</button>
<button class="btn btn-danger" ng-if="item.enabled" ng-click="toggle(item)">Disable</button>
</td>
<td>
<strong title="{{item.typeName}}">{{item.displayName}}</strong>
<p>
<small>{{item.description}}</small>
</p>
</td>
<td>
<button class="btn" ng-if="item.variantConfig.length > 0" ng-click="showVariantConfig(item)">Configure variants</button>
<button class="btn" ng-if="item.configurable" ng-click="showSegmentConfig(item)">Configure segments</button>
</td>
</tr>
</table>
<div ng-show="configuringVariants" >
<h3>'{{providerConfig.providerName}}' Variant Configuration</h3>
<p>
<small>
Here you can enable/disable content variants exposed by this provider
</small>
</p>
<form name="segmentVariantForm" novalidate val-form-manager>
<div ng-repeat="item in variantConfig">
<umb-pane>
<umb-control-group label="{{item.name}}" alias="{{item.key}}" description="Variant key: {{item.key}}">
<input type="checkbox" name="{{item.key}}" ng-model="$parent.item.enabled" />
</umb-control-group>
</umb-pane>
</div>
<button class="btn" ng-click="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary" ng-click="saveVariantConfig(segmentVariantForm)">Save</button>
</form>
</div>
<div ng-show="configuringSegments" class="segment-config">
<h3>'{{providerConfig.providerName}}' Segment Configuration</h3>
<p>
<small>
Here you can configure segment keys to assign to a request for this provider
</small>
</p>
<form name="segmentConfigForm" novalidate val-form-manager>
<button class="btn" ng-click="addItem()">Add</button>
<div ng-repeat="item in segmentConfig">
<umb-pane>
<ng-form name="segmentItemConfigForm">
<umb-control-group label="Match Expression" alias="matchExpression" description="The expression used to match against the value provided by the provider">
<input type="text" name="matchExpression" ng-model="$parent.item.matchExpression" required />
<span class="help-inline" val-msg-for="matchExpression" val-toggle-msg="required">Required</span>
</umb-control-group>
<umb-control-group label="Segment Key" alias="key" description="The key to store for this segment match">
<input type="text" name="key" ng-model="$parent.item.key" required />
<span class="help-inline" val-msg-for="key" val-toggle-msg="required">Required</span>
</umb-control-group>
<umb-control-group label="Persist segment" alias="value" description="This might be useful if you want to track an assigned segment">
<input type="checkbox" name="persist" ng-model="$parent.item.persist" title=" For example, if it's a referral provider a segment might be set when coming from 'mysite.com' and you want to know about that later on when the segment no longer exists in the request." />
</umb-control-group>
<umb-control-group label="Use as a Content Variant" alias="value" description="If checked, this segment key can be used to create custom Content Variants">
<input type="checkbox" name="persist" ng-model="$parent.item.asVariant" />
</umb-control-group>
</ng-form>
<button class="btn btn-danger btn-mini" ng-click="removeItem(item)">Remove</button>
</umb-pane>
</div>
<button class="btn" ng-click="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary" ng-click="saveSegmentConfig(segmentConfigForm)">Save</button>
</form>
</div>
</div>
@@ -1,19 +1,25 @@
<ng-form name="contentNameForm">
<div class="umb-headline-editor-wrapper" ng-class="{'ng-invalid': contentNameForm.name.$invalid}">
<input type="text"
name="name"
localize="placeholder"
class="umb-headline"
localize="placeholder"
select-on-focus
placeholder="{{placeholder}}"
ng-model="model"
required
val-server-field="Name"/>
<div class="umb-headline" ng-if="readonly">
{{model}}
</div>
<div ng-if ="!readonly">
<input type="text"
name="name"
localize="placeholder"
class="umb-headline input-block-level"
localize="placeholder"
select-on-focus
placeholder="{{placeholder}}"
ng-model="$parent.model"
required
val-server-field="Name" />
<span class="help-inline" val-msg-for="name" val-toggle-msg="required">Required</span>
<span class="help-inline" val-msg-for="name" val-toggle-msg="valServerField">{{contentNameForm.name.errorMsg}}</span>
<span class="help-inline" val-msg-for="name" val-toggle-msg="required">Required</span>
<span class="help-inline" val-msg-for="name" val-toggle-msg="valServerField">{{contentNameForm.name.errorMsg}}</span>
</div>
</div>
</ng-form>
@@ -198,7 +198,10 @@ angular.module("umbraco")
tinymce.DOM.events.domLoaded = true;
tinymce.init(baseLineConfigObj);
$scope.isLoading = false;
angularHelper.safeApply($scope, function() {
$scope.isLoading = false;
});
}, 200, false);
}
@@ -10,7 +10,7 @@ NOTES:
* Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config
* A new version will invalidate both client and server cache and create new persisted files
-->
<clientDependency version="1469612626" fileDependencyExtensions=".js,.css" loggerType="Umbraco.Web.UI.CdfLogger, umbraco">
<clientDependency version="1963089493" fileDependencyExtensions=".js,.css" loggerType="Umbraco.Web.UI.CdfLogger, umbraco">
<!--
This section is used for Web Forms only, the enableCompositeFiles="true" is optional and by default is set to true.
@@ -67,4 +67,15 @@
</control>
</tab>
</section>
<section alias="SegmentDashboard">
<areas>
<area>settings</area>
</areas>
<tab caption="Content Segmentation">
<control showOnce="false" addPanel="true" panelCaption="">
views/dashboard/settings/segmentdashboard.html
</control>
</tab>
</section>
</dashBoard>
@@ -162,7 +162,7 @@ namespace Umbraco.Web.Cache
{
if (e.RecycleBinEmptiedSuccessfully && e.IsContentRecycleBin)
{
DistributedCache.Instance.RemoveUnpublishedCachePermanently(e.Ids.ToArray());
DistributedCache.Instance.RemoveUnpublishedPageCache(e.Ids.ToArray());
}
}
@@ -177,8 +177,12 @@ namespace Umbraco.Web.Cache
/// </remarks>
static void ContentServiceTrashed(IContentService sender, MoveEventArgs<IContent> e)
{
DistributedCache.Instance.RefreshUnpublishedPageCache(
e.MoveInfoCollection.Select(x => x.Entity).ToArray());
var ids = e.MoveInfoCollection.Select(x => x.Entity.Id).ToList();
//ensure the variants are refreshed too
ids.AddRange(e.MoveInfoCollection.SelectMany(x => x.Entity.VariantInfo.VariantIds));
ids.AddRange(e.MoveInfoCollection.Select(x => x.Entity.VariantInfo.MasterDocId));
DistributedCache.Instance.RefreshUnpublishedPageCache(ids.ToArray());
}
/// <summary>
@@ -200,7 +204,13 @@ namespace Umbraco.Web.Cache
}
//run the un-published cache refresher since copied content is not published
DistributedCache.Instance.RefreshUnpublishedPageCache(e.Copy);
var ids = new List<int> {e.Copy.Id};
//ensure the variants are refreshed too
ids.AddRange(e.Copy.VariantInfo.VariantIds);
ids.Add(e.Copy.VariantInfo.MasterDocId);
DistributedCache.Instance.RefreshUnpublishedPageCache(ids.ToArray());
}
/// <summary>
@@ -210,7 +220,12 @@ namespace Umbraco.Web.Cache
/// <param name="e"></param>
static void ContentServiceDeleted(IContentService sender, DeleteEventArgs<IContent> e)
{
DistributedCache.Instance.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray());
var ids = e.DeletedEntities.Select(x => x.Id).ToList();
//ensure the variants are refreshed too
ids.AddRange(e.DeletedEntities.SelectMany(x => x.VariantInfo.VariantIds));
ids.AddRange(e.DeletedEntities.Select(x => x.VariantInfo.MasterDocId));
DistributedCache.Instance.RemoveUnpublishedPageCache(ids.ToArray());
}
/// <summary>
@@ -249,9 +264,14 @@ namespace Umbraco.Web.Cache
//filter out the entities that have only been saved (not newly published) since
// newly published ones will be synced with the published page cache refresher
var unpublished = e.SavedEntities.Where(x => x.JustPublished() == false);
var unpublished = e.SavedEntities.Where(x => x.JustPublished() == false).ToArray();
var ids = unpublished.Select(x => x.Id).ToList();
//ensure the variants are refreshed too
ids.AddRange(unpublished.SelectMany(x => x.VariantInfo.VariantIds));
ids.AddRange(unpublished.Select(x => x.VariantInfo.MasterDocId));
//run the un-published cache refresher
DistributedCache.Instance.RefreshUnpublishedPageCache(unpublished.ToArray());
DistributedCache.Instance.RefreshUnpublishedPageCache(ids.ToArray());
}
@@ -206,75 +206,48 @@ namespace Umbraco.Web.Cache
dc.RefreshAll(new Guid(DistributedCache.PageCacheRefresherId));
}
/// <summary>
/// Refreshes the cache amongst servers for a page
/// </summary>
/// <param name="dc"></param>
/// <param name="documentId"></param>
public static void RefreshPageCache(this DistributedCache dc, int documentId)
{
dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), documentId);
}
/// <summary>
/// Refreshes page cache for all instances passed in
/// </summary>
/// <param name="dc"></param>
/// <param name="content"></param>
public static void RefreshPageCache(this DistributedCache dc, params IContent[] content)
/// <param name="contentIds"></param>
public static void RefreshPageCache(this DistributedCache dc, params int[] contentIds)
{
dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), x => x.Id, content);
dc.RefreshByJson(new Guid(DistributedCache.PageCacheRefresherId),
PageCacheRefresher.SerializeToJsonPayload(PageCacheRefresher.OperationType.Saved, contentIds));
}
/// <summary>
/// Removes the cache amongst servers for a page
/// </summary>
/// <param name="dc"></param>
/// <param name="content"></param>
public static void RemovePageCache(this DistributedCache dc, params IContent[] content)
{
dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), x => x.Id, content);
}
/// <summary>
/// Removes the cache amongst servers for a page
/// </summary>
/// <param name="dc"></param>
/// <param name="documentId"></param>
public static void RemovePageCache(this DistributedCache dc, int documentId)
{
dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), documentId);
}
/// <summary>
/// invokes the unpublished page cache refresher
/// </summary>
/// <param name="dc"></param>
/// <param name="content"></param>
public static void RefreshUnpublishedPageCache(this DistributedCache dc, params IContent[] content)
{
dc.Refresh(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content);
}
/// <summary>
/// invokes the unpublished page cache refresher
/// </summary>
/// <param name="dc"></param>
/// <param name="content"></param>
public static void RemoveUnpublishedPageCache(this DistributedCache dc, params IContent[] content)
{
dc.Remove(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content);
}
/// <summary>
/// invokes the unpublished page cache refresher to mark all ids for permanent removal
/// </summary>
/// <param name="dc"></param>
/// <param name="contentIds"></param>
public static void RemoveUnpublishedCachePermanently(this DistributedCache dc, params int[] contentIds)
public static void RemovePageCache(this DistributedCache dc, params int[] contentIds)
{
dc.RefreshByJson(new Guid(DistributedCache.PageCacheRefresherId),
PageCacheRefresher.SerializeToJsonPayload(PageCacheRefresher.OperationType.Deleted, contentIds));
}
/// <summary>
/// invokes the unpublished page cache refresher
/// </summary>
/// <param name="dc"></param>
/// <param name="contentIds"></param>
public static void RefreshUnpublishedPageCache(this DistributedCache dc, params int[] contentIds)
{
dc.RefreshByJson(new Guid(DistributedCache.UnpublishedPageCacheRefresherId),
UnpublishedPageCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(contentIds));
UnpublishedPageCacheRefresher.SerializeToJsonPayload(UnpublishedPageCacheRefresher.OperationType.Saved, contentIds));
}
/// <summary>
/// invokes the unpublished page cache refresher
/// </summary>
/// <param name="dc"></param>
/// <param name="contentIds"></param>
public static void RemoveUnpublishedPageCache(this DistributedCache dc, params int[] contentIds)
{
dc.RefreshByJson(new Guid(DistributedCache.UnpublishedPageCacheRefresherId),
UnpublishedPageCacheRefresher.SerializeToJsonPayload(UnpublishedPageCacheRefresher.OperationType.Deleted, contentIds));
}
#endregion
+73 -12
View File
@@ -1,10 +1,12 @@
using System;
using System.Web.Script.Serialization;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Models;
using Umbraco.Core.Sync;
using umbraco;
using umbraco.cms.businesslogic.web;
using System.Linq;
namespace Umbraco.Web.Cache
{
@@ -15,7 +17,7 @@ namespace Umbraco.Web.Cache
/// If Load balancing is enabled (by default disabled, is set in umbracoSettings.config) PageCacheRefresher will be called
/// everytime content is added/updated/removed to ensure that the content cache is identical on all load balanced servers
/// </remarks>
public class PageCacheRefresher : TypedCacheRefresherBase<PageCacheRefresher, IContent>
public class PageCacheRefresher : JsonCacheRefresherBase<PageCacheRefresher>
{
protected override PageCacheRefresher Instance
@@ -44,6 +46,51 @@ namespace Umbraco.Web.Cache
get { return "Page Refresher"; }
}
#region Static helpers
/// <summary>
/// Converts the json to a JsonPayload object
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
internal static JsonPayload[] DeserializeFromJsonPayload(string json)
{
var serializer = new JavaScriptSerializer();
var jsonObject = serializer.Deserialize<JsonPayload[]>(json);
return jsonObject;
}
internal static string SerializeToJsonPayload(OperationType op, params int[] contentIds)
{
var serializer = new JavaScriptSerializer();
var items = contentIds.Select(x => new JsonPayload
{
Id = x,
Operation = op
}).ToArray();
var json = serializer.Serialize(items);
return json;
}
#endregion
#region Sub classes
internal enum OperationType
{
Deleted,
Saved
}
internal class JsonPayload
{
public int Id { get; set; }
public OperationType Operation { get; set; }
}
#endregion
/// <summary>
/// Refreshes all nodes in umbraco.
/// </summary>
@@ -79,22 +126,36 @@ namespace Umbraco.Web.Cache
base.Remove(id);
}
public override void Refresh(IContent instance)
/// <summary>
/// Implement the IJsonCacheRefresher so that we can bulk delete/refresh the cache based on multiple IDs
/// </summary>
/// <param name="jsonPayload"></param>
public override void Refresh(string jsonPayload)
{
ApplicationContext.Current.ApplicationCache.ClearPartialViewCache();
content.Instance.UpdateDocumentCache(new Document(instance));
DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer();
DistributedCache.Instance.ClearXsltCacheOnCurrentServer();
base.Refresh(instance);
}
var payloads = DeserializeFromJsonPayload(jsonPayload)
.Where(x => x.Id > 0)
.ToArray();
if (payloads.Any() == false) return;
public override void Remove(IContent instance)
{
ApplicationContext.Current.ApplicationCache.ClearPartialViewCache();
content.Instance.ClearDocumentCache(new Document(instance));
DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer();
DistributedCache.Instance.ClearXsltCacheOnCurrentServer();
base.Remove(instance);
foreach (var payload in payloads)
{
switch (payload.Operation)
{
case OperationType.Deleted:
content.Instance.ClearDocumentCache(payload.Id);
break;
case OperationType.Saved:
content.Instance.UpdateDocumentCache(payload.Id);
break;
}
}
base.Refresh(jsonPayload);
}
}
}
@@ -52,6 +52,11 @@ namespace Umbraco.Web.Cache
private void RemoveFromCache(int id)
{
//We'll need to clear all content cache because ITemplate is a property of IContent which
// is cached against each intance of IContent :(
// TODO: Re-think how we are caching full entities.
RuntimeCacheProvider.Current.Clear(typeof(IContent));
ApplicationContext.Current.ApplicationCache.ClearCacheItem(
string.Format("{0}{1}", CacheKeys.TemplateFrontEndCacheKey, id));
@@ -11,7 +11,7 @@ namespace Umbraco.Web.Cache
/// <summary>
/// A cache refresher used for non-published content, this is primarily to notify Examine indexes to update and to refresh the RuntimeCacheRefresher
/// </summary>
public sealed class UnpublishedPageCacheRefresher : TypedCacheRefresherBase<UnpublishedPageCacheRefresher, IContent>, IJsonCacheRefresher
public sealed class UnpublishedPageCacheRefresher : JsonCacheRefresherBase<UnpublishedPageCacheRefresher>
{
protected override UnpublishedPageCacheRefresher Instance
{
@@ -43,13 +43,13 @@ namespace Umbraco.Web.Cache
}
internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] contentIds)
internal static string SerializeToJsonPayload(OperationType op, params int[] contentIds)
{
var serializer = new JavaScriptSerializer();
var items = contentIds.Select(x => new JsonPayload
{
Id = x,
Operation = OperationType.Deleted
Operation = op
}).ToArray();
var json = serializer.Serialize(items);
return json;
@@ -61,7 +61,8 @@ namespace Umbraco.Web.Cache
internal enum OperationType
{
Deleted
Deleted,
Saved
}
internal class JsonPayload
@@ -90,31 +91,22 @@ namespace Umbraco.Web.Cache
base.Remove(id);
}
public override void Refresh(IContent instance)
{
RuntimeCacheProvider.Current.Delete(typeof(IContent), instance.Id);
base.Refresh(instance);
}
public override void Remove(IContent instance)
{
RuntimeCacheProvider.Current.Delete(typeof(IContent), instance.Id);
base.Remove(instance);
}
/// <summary>
/// Implement the IJsonCacheRefresher so that we can bulk delete the cache based on multiple IDs for when the recycle bin is emptied
/// Implement the IJsonCacheRefresher so that we can bulk delete/refresh the cache based on multiple IDs
/// </summary>
/// <param name="jsonPayload"></param>
public void Refresh(string jsonPayload)
public override void Refresh(string jsonPayload)
{
foreach (var payload in DeserializeFromJsonPayload(jsonPayload))
var payloads = DeserializeFromJsonPayload(jsonPayload)
.Where(x => x.Id > 0)
.ToArray();
foreach (var payload in payloads)
{
RuntimeCacheProvider.Current.Delete(typeof(IContent), payload.Id);
}
OnCacheUpdated(Instance, new CacheRefresherEventArgs(jsonPayload, MessageType.RefreshByJson));
base.Refresh(jsonPayload);
}
}
@@ -110,6 +110,10 @@ namespace Umbraco.Web.Editors
{"manifestAssetList", Url.Action("GetManifestAssetList", "BackOffice")},
{"serverVarsJs", Url.Action("Application", "BackOffice")},
//API URLs
{
"segmentDashboardApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl<SegmentDashboardController>(
controller => controller.GetProviders())
},
{
"embedApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl<RteEmbedController>(
controller => controller.GetEmbed("",0,0))
+29 -1
View File
@@ -8,18 +8,23 @@ using System.Net.Http.Formatting;
using System.Text;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using System.Web.Services.Description;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Publishing;
using Umbraco.Core.Services;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Models.Mapping;
using Umbraco.Web.Models.Segments;
using Umbraco.Web.Mvc;
using Umbraco.Web.Routing;
using Umbraco.Web.Routing.Segments;
using Umbraco.Web.Security;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Binders;
@@ -284,7 +289,7 @@ namespace Umbraco.Web.Editors
//return the updated model
var display = Mapper.Map<IContent, ContentItemDisplay>(contentItem.PersistedContent);
//lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
HandleInvalidModelState(display);
@@ -559,6 +564,24 @@ namespace Umbraco.Web.Editors
return content;
}
[EnsureUserPermissionForContent("id", 'C')]
public int PostCreateVariant(int id, string key)
{
var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id));
if (foundContent == null)
{
HandleContentNotFound(id);
return int.MinValue;
}
var contentVariant = Services.ContentService.CreateContentVariant(
foundContent,
key);
return contentVariant.Id;
}
/// <summary>
/// Ensures the item can be moved/copied to the new location
/// </summary>
@@ -737,5 +760,10 @@ namespace Umbraco.Web.Editors
return allowed;
}
}
}
@@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Web.Models.Segments;
using Umbraco.Web.Mvc;
using Umbraco.Web.Routing.Segments;
using Umbraco.Web.Security;
namespace Umbraco.Web.Editors
{
[PluginController("UmbracoApi")]
public class SegmentDashboardController : UmbracoAuthorizedJsonController
{
/// <summary>
/// Returns a json array of provider information
/// </summary>
/// <returns></returns>
public JArray GetProviders()
{
var providers = ContentSegmentProviderResolver.Current.Providers.ToArray();
var status = ContentSegmentProvidersStatus.GetProviderStatus();
var result = providers
.Select(x => new
{
typeName = x.GetType().FullName,
displayName = x.Name,
description = x.Description,
variants = x.AssignableContentVariants,
variantConfig = x.ReadVariantConfiguration(),
asConfigurable = x as ConfigurableSegmentProvider
})
.Select(x => new
{
x.typeName,
x.displayName,
x.description,
configurable = x.asConfigurable != null,
segmentConfig = x.asConfigurable != null ? x.asConfigurable.ReadSegmentConfiguration() : null,
enabled = status.ContainsKey(x.typeName) && status[x.typeName],
//SD: I don't feel like creating yet another simple model to send this information in json so I'm just making
// an anonymous one.
variantConfig = x.variants.Select(vari => new
{
name = vari.VariantName,
key = vari.SegmentMatchKey,
enabled = x.variantConfig.ContainsKey(vari.SegmentMatchKey) && x.variantConfig[vari.SegmentMatchKey],
asVariant = x.variantConfig.ContainsKey(vari.SegmentMatchKey) && x.variantConfig[vari.SegmentMatchKey]
})
});
return JArray.FromObject(result);
}
public HttpResponseMessage PostSaveProviderSegmentConfig([FromUri]string typeName, IEnumerable<SegmentProviderMatch> config)
{
//TODO: We need to validate the config to ensure there are no nulls like null keys
var providers = ContentSegmentProviderResolver.Current.Providers.ToArray();
var provider = providers.FirstOrDefault(x => x.GetType().FullName == typeName) as ConfigurableSegmentProvider;
if (provider == null)
{
return Request.CreateErrorResponse(
HttpStatusCode.InternalServerError,
"No provider found with name " + typeName + " or the provider type is not configurable");
}
provider.WriteSegmentConfiguration(config);
return Request.CreateResponse(HttpStatusCode.OK);
}
public HttpResponseMessage PostSaveProviderVariantConfig([FromUri]string typeName, JArray config)
{
//TODO: We need to validate the config
var asDictionary = config.ToDictionary(x => (string)x["key"], x => (bool)x["enabled"]);
var providers = ContentSegmentProviderResolver.Current.Providers.ToArray();
var provider = providers.FirstOrDefault(x => x.GetType().FullName == typeName);
if (provider == null)
{
return Request.CreateErrorResponse(
HttpStatusCode.InternalServerError,
"No provider found with name " + typeName + " or the provider type is not configurable");
}
provider.WriteVariantConfiguration(asDictionary);
return Request.CreateResponse(HttpStatusCode.OK);
}
/// <summary>
/// Enabled/disable a provider
/// </summary>
/// <param name="typeName"></param>
/// <returns></returns>
public HttpResponseMessage PostToggleProvider([FromUri]string typeName)
{
var providers = ContentSegmentProviderResolver.Current.Providers.ToArray();
var status = ContentSegmentProvidersStatus.GetProviderStatus();
var provider = providers.FirstOrDefault(x => x.GetType().FullName == typeName);
if (provider != null)
{
if (status.ContainsKey(typeName))
{
//reverse
status[typeName] = (status[typeName] == false);
}
else
{
//if it doesn't exist it's not currently enabled
status[typeName] = true;
}
ContentSegmentProvidersStatus.SaveProvidersStatus(status);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
}
}
@@ -8,6 +8,7 @@ using System.Web.Http.ModelBinding;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Validation;
using Umbraco.Web.Models.Segments;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Trees;
@@ -20,6 +21,11 @@ namespace Umbraco.Web.Models.ContentEditing
[DataContract(Name = "content", Namespace = "")]
public class ContentItemDisplay : ListViewAwareContentItemDisplayBase<ContentPropertyDisplay, IContent>
{
public ContentItemDisplay()
{
ContentVariants = Enumerable.Empty<ContentVariableSegment>();
}
[DataMember(Name = "publishDate")]
public DateTime? PublishDate { get; set; }
@@ -44,5 +50,25 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "allowedActions")]
public IEnumerable<char> AllowedActions { get; set; }
/// <summary>
/// A list of the content variant segments allowed/assigned to this content item
/// </summary>
/// <remarks>
/// If there are no variants then no drop down is shown
/// </remarks>
[DataMember(Name = "variants")]
public IEnumerable<ContentVariableSegment> ContentVariants { get; set; }
/// <summary>
/// If this is a variant, the master doc id will not be null
/// </summary>
[DataMember(Name = "masterDocId")]
public int? MasterDocId { get; set; }
/// <summary>
/// If this is a variant, this is it's key
/// </summary>
[DataMember(Name = "variantKey")]
public string VariantKey { get; set; }
}
}
@@ -9,12 +9,15 @@ using System.Web.Mvc;
using System.Web.Routing;
using AutoMapper;
using Newtonsoft.Json;
using umbraco.cms.businesslogic.web;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Mapping;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Models.Segments;
using Umbraco.Web.Routing.Segments;
using Umbraco.Web.Trees;
using umbraco;
using Umbraco.Web.Routing;
@@ -52,7 +55,7 @@ namespace Umbraco.Web.Models.Mapping
expression => expression.MapFrom(content => content.Parent().ContentType.IsContainer))
.ForMember(
dto => dto.PublishDate,
expression => expression.MapFrom(content => GetPublishedDate(content, applicationContext)))
expression => expression.MapFrom(content => GetPublishedDate(content, applicationContext.Services.ContentService)))
.ForMember(
dto => dto.TemplateAlias, expression => expression.MapFrom(content => content.Template.Alias))
.ForMember(
@@ -69,7 +72,7 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(display => display.Tabs, expression => expression.ResolveUsing<TabsAndPropertiesResolver>())
.ForMember(display => display.AllowedActions, expression => expression.ResolveUsing(
new ActionButtonsResolver(new Lazy<IUserService>(() => applicationContext.Services.UserService))))
.AfterMap(AfterMap);
.AfterMap((content, display) => AfterMap(content, display, applicationContext.Services.ContentService));
//FROM IContent TO ContentItemBasic<ContentPropertyBasic, IContent>
config.CreateMap<IContent, ContentItemBasic<ContentPropertyBasic, IContent>>()
@@ -105,7 +108,8 @@ namespace Umbraco.Web.Models.Mapping
/// </summary>
/// <param name="content"></param>
/// <param name="display"></param>
private static void AfterMap(IContent content, ContentItemDisplay display)
/// <param name="contentService"></param>
private static void AfterMap(IContent content, ContentItemDisplay display, IContentService contentService)
{
//map the tree node url
if (HttpContext.Current != null)
@@ -161,17 +165,17 @@ namespace Umbraco.Web.Models.Mapping
Value = string.Join(",", display.Urls),
View = "urllist" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor
});
MapVariants(content, display, contentService);
}
/// <summary>
/// Gets the published date value for the IContent object
/// </summary>
/// <param name="content"></param>
/// <param name="applicationContext"></param>
/// <param name="contentService"></param>
/// <returns></returns>
private static DateTime? GetPublishedDate(IContent content, ApplicationContext applicationContext)
private static DateTime? GetPublishedDate(IContent content, IContentService contentService)
{
if (content.Published)
{
@@ -179,7 +183,7 @@ namespace Umbraco.Web.Models.Mapping
}
if (content.HasPublishedVersion())
{
var published = applicationContext.Services.ContentService.GetPublishedVersion(content.Id);
var published = contentService.GetPublishedVersion(content.Id);
return published.UpdateDate;
}
return null;
@@ -240,5 +244,98 @@ namespace Umbraco.Web.Models.Mapping
}
}
//TODO: This uses other services which will need to be wired up in order for this mapper to execute, create overloaded ctor's so we ca
// test or inject these services.
private static void MapVariants(IContent content, ContentItemDisplay display, IContentService contentService)
{
if (content.HasIdentity == false) return;
//if it's not a variant - go get it's variants
if (content.VariantInfo.IsVariant == false)
{
var variantDef = contentService.GetByIds(content.VariantInfo.VariantIds);
//if it's not a variant, then it's a master-doc so go lookup the possible variant types it can have
var segmentProviderStatus = ContentSegmentProvidersStatus.GetProviderStatus();
var assignableVariants = ContentSegmentProviderResolver.Current.GetAssignableVariants(segmentProviderStatus);
//These are the assignable variants based on the installed providers (statically advertised variants)
// that are enabled via the back office. If they are not enabled, they will not show up.
var assignableSegments = assignableVariants
.Select(variantAttribute => new
{
variantAttribute,
assigned = variantDef.FirstOrDefault(k => k.VariantInfo.Key == variantAttribute.SegmentMatchKey)
})
.Select(x => x.assigned == null
? new ContentVariableSegment(x.variantAttribute.VariantName, x.variantAttribute.SegmentMatchKey, false)
: new ContentVariableSegment(x.variantAttribute.VariantName, x.variantAttribute.SegmentMatchKey, false, x.assigned.Id, x.assigned.Trashed, x.assigned.UpdateDate));
var assignedLanguages = GetAssignedLanguageVariants(display);
var languageSegments = assignedLanguages
.Select(x => new { lang = x, assigned = variantDef.FirstOrDefault(k => k.VariantInfo.Key == x) })
.Select(x => x.assigned == null
? new ContentVariableSegment(x.lang, true)
: new ContentVariableSegment(x.lang, true, x.assigned.Id, x.assigned.Trashed, x.assigned.UpdateDate));
//assign the variants, NOTE: languages always take precedence if there is overlap
display.ContentVariants = languageSegments.Union(assignableSegments);
}
else
{
display.MasterDocId = content.VariantInfo.MasterDocId;
display.VariantKey = content.VariantInfo.Key;
//We want to change the URL property because it shouldn't show urls if it's a variant, the URL will be specific
// to the master doc - if it is NOT a language variant.
//For language variants the URL should only reflect the one assigned by domain.
var genericTab = display.Tabs.Single(x => x.Id == 0);
var urlProp = genericTab.Properties.Single(x => x.Alias == string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
var assignedLanguages = GetAssignedLanguageVariants(display);
if (assignedLanguages.Contains(content.VariantInfo.Key) == false)
{
//it's not a language, so remove the url
//TODO: show a message? or just remove the prop?
//var labelEditor = PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View;
genericTab.Properties = genericTab.Properties.Except(new[] {urlProp});
//urlProp.Value = "The URL is the same as the master doc, custom variants do not have different URLs";
//urlProp.Label = "";
//urlProp.View = labelEditor;
}
else
{
//it is a language so only show that specific domain URL.
//TODO: How??? There doesn't seem to be an easy way to do this need to ask Stephen
//urlProp.Value = content.GetContentUrls();
}
}
}
private static IEnumerable<string> GetAssignedLanguageVariants(ContentItemDisplay display)
{
//These are the lanuages assigned to this node (i.e. based on domains assigned to this node or ancestor nodes)
var allDomains = DomainHelper.GetAllDomains(false);
//now get the ones assigned within the path
var splitPath = display.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).ToList();
if (display.MasterDocId.HasValue)
{
//we need to add the master doc id to the path, since that is really where the languages are assigned
splitPath.Insert(1, display.MasterDocId.Value.ToString(CultureInfo.InvariantCulture));
}
var assignedDomains = allDomains.Where(x => splitPath.Contains(x.RootNodeId.ToString(CultureInfo.InvariantCulture)));
return assignedDomains.Select(x => x.Language.CultureAlias);
}
}
}
@@ -0,0 +1,138 @@
using System;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.Segments
{
/// <summary>
/// A definition of a segment that can be used to create content variants
/// </summary>
[DataContract(Name = "segment", Namespace = "")]
public class ContentVariableSegment
{
/// <summary>
/// Constructor for a variant that does not exist yet
/// </summary>
/// <param name="key"></param>
/// <param name="isLanguage"></param>
public ContentVariableSegment(string key, bool isLanguage)
{
Name = key;
Key = key;
IsLanguage = isLanguage;
Exists = false;
}
/// <summary>
/// COnstructor for a variant that exists
/// </summary>
/// <param name="key"></param>
/// <param name="isLanguage"></param>
/// <param name="variantId"></param>
/// <param name="isTrashed"></param>
/// <param name="lastUpdated"></param>
public ContentVariableSegment(string key, bool isLanguage, int variantId, bool isTrashed, DateTime lastUpdated)
{
Name = key;
Key = key;
IsLanguage = isLanguage;
Exists = true;
VariantId = variantId;
IsTrashed = isTrashed;
LastUpdated = lastUpdated;
}
/// <summary>
/// Constructor for a variant that does not exist yet
/// </summary>
/// <param name="name"></param>
/// <param name="key"></param>
/// <param name="isLanguage"></param>
public ContentVariableSegment(string name, string key, bool isLanguage)
{
Name = name;
Key = key;
IsLanguage = isLanguage;
Exists = false;
}
/// <summary>
/// COnstructor for a variant that exists
/// </summary>
/// <param name="name"></param>
/// <param name="key"></param>
/// <param name="isLanguage"></param>
/// <param name="variantId"></param>
/// <param name="isTrashed"></param>
/// <param name="lastUpdated"></param>
public ContentVariableSegment(string name, string key, bool isLanguage, int variantId, bool isTrashed, DateTime lastUpdated)
{
Name = name;
Key = key;
IsLanguage = isLanguage;
Exists = true;
VariantId = variantId;
IsTrashed = isTrashed;
LastUpdated = lastUpdated;
}
/// <summary>
/// The friendly name of the segment
/// </summary>
[DataMember(Name = "name")]
public string Name { get; private set; }
/// <summary>
/// The segment Key associated with the content variant
/// </summary>
[DataMember(Name = "key")]
public string Key { get; private set; }
/// <summary>
/// Whether or not the key for this segment is based on a language
/// </summary>
/// <remarks>
/// In the case that it is based on a language, the key will be the ISO lang code like en-US
/// </remarks>
[DataMember(Name = "isLanguage")]
public bool IsLanguage { get; private set; }
[DataMember(Name = "lastUpdated")]
public DateTime? LastUpdated { get; private set; }
/// <summary>
/// Whether or not a content variant current exists for this segment for a content item
/// </summary>
[DataMember(Name = "exists")]
public bool Exists { get; private set; }
/// <summary>
/// The id of the variant if it exists
/// </summary>
[DataMember(Name = "id")]
public int VariantId { get; private set; }
/// <summary>
/// Whether or not the variant is in the trash or not
/// </summary>
[DataMember(Name = "isTrashed")]
public bool IsTrashed { get; private set; }
protected bool Equals(ContentVariableSegment other)
{
return string.Equals(Key, other.Key);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((ContentVariableSegment) obj);
}
public override int GetHashCode()
{
return Key.GetHashCode();
}
}
}
+104
View File
@@ -0,0 +1,104 @@
using System;
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace Umbraco.Web.Models.Segments
{
/// <summary>
/// Defines an assigned segment in a request
/// </summary>
/// <remarks>
/// The serialization names are only one letter - this is intentional to keep the cookie size small
/// </remarks>
[DataContract(Name = "s", Namespace = "")]
public class Segment
{
/// <summary>
/// Constructor, required for deserialization
/// </summary>
public Segment()
{
}
public Segment(string key, object value)
{
Key = key;
Value = value;
}
public Segment(string key, object value, bool persist)
{
Key = key;
Value = value;
Persist = persist;
}
internal Segment(string key, object value, bool persist, int? slidingDays)
{
Key = key;
Value = value;
Persist = persist;
SlidingDays = slidingDays;
}
internal Segment(string key, object value, bool persist, DateTime? absoluteExpiry)
{
Key = key;
Value = value;
Persist = persist;
AbsoluteExpiry = absoluteExpiry;
}
/// <summary>
/// The name of the segment
/// </summary>
[DataMember(Name = "k", IsRequired = true)]
public string Key { get; set; }
/// <summary>
/// The value of the segment
/// </summary>
[DataMember(Name = "v")]
public object Value { get; set; }
/// <summary>
/// Whether or not this segment is to be persisted (default is false)
/// </summary>
[DataMember(Name = "p")]
public bool Persist { get; set; }
//TODO: We should make use of these expiry settings!
/// <summary>
/// Defines the sliding expiration date in days from now for this particular segment
/// </summary>
[JsonIgnore]
[IgnoreDataMember]
internal int? SlidingDays { get; private set; }
/// <summary>
/// Defines the absolute expiration date for this particular segment
/// </summary>
[JsonIgnore]
[IgnoreDataMember]
internal DateTime? AbsoluteExpiry { get; private set; }
protected bool Equals(Segment other)
{
return string.Equals(Key, other.Key);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Segment) obj);
}
public override int GetHashCode()
{
return Key.GetHashCode();
}
}
}
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Umbraco.Web.Models.Segments
{
/// <summary>
/// A keyed collection of segments
/// </summary>
public class SegmentCollection : KeyedCollection<string, Segment>
{
public SegmentCollection(IEnumerable<Segment> segments)
{
foreach (var segment in segments)
{
//last one in wins
if (Contains(segment.Key))
{
Remove(segment.Key);
}
Add(segment);
}
}
internal void AddNew(Segment segment)
{
//last one in wins
if (Contains(segment.Key))
{
Remove(segment.Key);
}
Add(segment);
}
protected override string GetKeyForItem(Segment item)
{
return item.Key;
}
}
}
@@ -0,0 +1,45 @@
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.Segments
{
/// <summary>
/// Used as a configuration item for a configurable segment provider
/// </summary>
[DataContract(Name = "segment", Namespace = "")]
public class SegmentProviderMatch
{
/// <summary>
/// The Segment Key to add to the request
/// </summary>
[DataMember(Name = "key")]
public string Key { get; set; }
/// <summary>
/// The segment Value to add to the request for this match's Key
/// </summary>
[DataMember(Name = "value")]
public string Value { get; set; }
/// <summary>
/// The configured expression to match against the advertised value of a configurable segment provider
/// </summary>
[DataMember(Name = "matchExpression")]
public string MatchExpression { get; set; }
/// <summary>
/// A flag of whether or not to persist the segment match to the user's cookies (and to the member profile if they are logged in)
/// </summary>
/// <remarks>
/// This might be useful if you want to track a segment from a provider, perhaps if it's a referral provider a segment might be set
/// when coming from mysite.com and you want to know about that later on when the segment no longer exists in the request.
/// </remarks>
[DataMember(Name = "persist")]
public bool Persist { get; set; }
/// <summary>
/// A flag to determine if this segment is allowed to be a variant (advertised)
/// </summary>
[DataMember(Name = "asVariant")]
public bool AllowedAsVariant { get; set; }
}
}
@@ -9,6 +9,7 @@ using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Xml;
using Umbraco.Web.Routing;
@@ -187,6 +188,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
public string ChildDocumentByUrlNameVar { get; private set; }
public string RootDocumentWithLowestSortOrder { get; private set; }
//xpath statements to take into account variants
public string RootDocumentsAndVariants { get; private set; }
public string ChildDocumentOrVariantByUrlNameVar { get; private set; }
public string ChildDocumentOrVariantByUrlName { get; private set; }
public XPathStringsDefinition(int version)
{
Version = version;
@@ -195,20 +201,41 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
// legacy XML schema
case 0:
RootDocuments = "/root/node";
DescendantDocumentById = "//node [@id={0}]";
ChildDocumentByUrlName = "/node [@urlName='{0}']";
ChildDocumentByUrlNameVar = "/node [@urlName=${0}]";
RootDocumentWithLowestSortOrder = "/root/node [not(@sortOrder > ../node/@sortOrder)][1]";
throw new NotSupportedException("The legacy XML schema is no longer supported");
//RootDocuments = "/root/node";
//DescendantDocumentById = "//node [@id={0}]";
//ChildDocumentByUrlName = "/node [@urlName='{0}']";
//ChildDocumentByUrlNameVar = "/node [@urlName=${0}]";
//RootDocumentWithLowestSortOrder = "/root/node [not(@sortOrder > ../node/@sortOrder)][1]";
break;
// default XML schema as of 4.10
case 1:
RootDocuments = "/root/* [@isDoc]";
DescendantDocumentById = "//* [@isDoc and @id={0}]";
ChildDocumentByUrlName = "/* [@isDoc and @urlName='{0}']";
ChildDocumentByUrlNameVar = "/* [@isDoc and @urlName=${0}]";
RootDocumentWithLowestSortOrder = "/root/* [@isDoc and not(@sortOrder > ../* [@isDoc]/@sortOrder)][1]";
//variants have isDoc='variant' so we don't want those
RootDocuments = "/root/* [@isDoc='']";
//master + variants both have isDoc
RootDocumentsAndVariants = "/root/* [@isDoc]";
//variants have isDoc='variant' so we don't want those
DescendantDocumentById = "//* [@isDoc='' and @id={0}]";
//variants have isDoc='variant' so we don't want those
ChildDocumentByUrlName = "/* [@isDoc='' and @urlName='{0}']";
//master + variants both have isDoc
ChildDocumentOrVariantByUrlName = "/* [@isDoc and @urlName='{0}']";
//variants have isDoc='variant' so we don't want those
ChildDocumentByUrlNameVar = "/* [@isDoc='' and @urlName=${0}]";
//master + variants both have isDoc
ChildDocumentOrVariantByUrlNameVar = "/* [@isDoc and @urlName=${0}]";
//variants have isDoc='variant' so we don't want those
RootDocumentWithLowestSortOrder = "/root/* [@isDoc='' and not(@sortOrder > ../* [@isDoc='']/@sortOrder)][1]";
break;
default:
@@ -238,17 +265,17 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
#region Converters
private static IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing)
private static IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing, VariantInfo variantInfo)
{
return xmlNode == null
? null
: (new XmlPublishedContent(xmlNode, isPreviewing)).CreateModel();
? null
: (new XmlPublishedContent(xmlNode, isPreviewing, variantInfo)).CreateModel();
}
private static IEnumerable<IPublishedContent> ConvertToDocuments(XmlNodeList xmlNodes, bool isPreviewing)
private static IEnumerable<IPublishedContent> ConvertToDocuments(XmlNodeList xmlNodes, bool isPreviewing, VariantInfo variantInfo)
{
return xmlNodes.Cast<XmlNode>()
.Select(xmlNode => (new XmlPublishedContent(xmlNode, isPreviewing)).CreateModel());
.Select(xmlNode => (new XmlPublishedContent(xmlNode, isPreviewing, variantInfo)).CreateModel());
}
#endregion
@@ -257,12 +284,20 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
public virtual IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, int nodeId)
{
return ConvertToDocument(GetXml(umbracoContext, preview).GetElementById(nodeId.ToString(CultureInfo.InvariantCulture)), preview);
return ConvertToDocument(
GetXml(umbracoContext, preview).GetElementById(nodeId.ToString(CultureInfo.InvariantCulture)),
preview,
//TODO: Do something about this :(
umbracoContext.PublishedContentRequest == null ? new VariantInfo() : umbracoContext.PublishedContentRequest.VariantInfo);
}
public virtual IEnumerable<IPublishedContent> GetAtRoot(UmbracoContext umbracoContext, bool preview)
{
return ConvertToDocuments(GetXml(umbracoContext, preview).SelectNodes(XPathStrings.RootDocuments), preview);
return ConvertToDocuments(
GetXml(umbracoContext, preview).SelectNodes(XPathStrings.RootDocuments),
preview,
//TODO: Do something about this :(
umbracoContext.PublishedContentRequest == null ? new VariantInfo() : umbracoContext.PublishedContentRequest.VariantInfo);
}
public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, params XPathVariable[] vars)
@@ -274,7 +309,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
var node = vars == null
? xml.SelectSingleNode(xpath)
: xml.SelectSingleNode(xpath, vars);
return ConvertToDocument(node, preview);
return ConvertToDocument(node, preview,
//TODO: Do something about this :(
umbracoContext.PublishedContentRequest == null ? new VariantInfo() : umbracoContext.PublishedContentRequest.VariantInfo);
}
public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, params XPathVariable[] vars)
@@ -285,7 +322,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
var node = vars == null
? xml.SelectSingleNode(xpath)
: xml.SelectSingleNode(xpath, vars);
return ConvertToDocument(node, preview);
return ConvertToDocument(node, preview,
//TODO: Do something about this :(
umbracoContext.PublishedContentRequest == null ? new VariantInfo() : umbracoContext.PublishedContentRequest.VariantInfo);
}
public virtual IEnumerable<IPublishedContent> GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, params XPathVariable[] vars)
@@ -297,7 +336,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
var nodes = vars == null
? xml.SelectNodes(xpath)
: xml.SelectNodes(xpath, vars);
return ConvertToDocuments(nodes, preview);
return ConvertToDocuments(nodes, preview,
//TODO: Do something about this :(
umbracoContext.PublishedContentRequest == null ? new VariantInfo() : umbracoContext.PublishedContentRequest.VariantInfo);
}
public virtual IEnumerable<IPublishedContent> GetByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, params XPathVariable[] vars)
@@ -308,7 +349,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
var nodes = vars == null
? xml.SelectNodes(xpath)
: xml.SelectNodes(xpath, vars);
return ConvertToDocuments(nodes, preview);
return ConvertToDocuments(nodes, preview,
//TODO: Do something about this :(
umbracoContext.PublishedContentRequest == null ? new VariantInfo() : umbracoContext.PublishedContentRequest.VariantInfo);
}
public virtual bool HasContent(UmbracoContext umbracoContext, bool preview)
@@ -383,6 +426,14 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
static readonly char[] SlashChar = new[] { '/' };
/// <summary>
/// Creates an xpath query to lookup a node by route
/// </summary>
/// <param name="startNodeId"></param>
/// <param name="path"></param>
/// <param name="hideTopLevelNodeFromPath"></param>
/// <param name="vars"></param>
/// <returns></returns>
protected string CreateXpathQuery(int startNodeId, string path, bool hideTopLevelNodeFromPath, out IEnumerable<XPathVariable> vars)
{
string xpath;
@@ -407,7 +458,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
// read http://stackoverflow.com/questions/1128745/how-can-i-use-xpath-to-find-the-minimum-value-of-an-attribute-in-a-set-of-elemen
// so that one does not work, because min(@sortOrder) maybe 1
// xpath = "/root/*[@isDoc and @sortOrder='0']";
// xpath = "/root/*[@isDoc='' and @sortOrder='0']";
// and we can't use min() because that's XPath 2.0
// that one works
@@ -424,10 +475,14 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
if (startNodeId == 0)
{
if (hideTopLevelNodeFromPath)
xpathBuilder.Append(XPathStrings.RootDocuments); // first node is not in the url
else
xpathBuilder.Append(XPathStringsDefinition.Root);
if (hideTopLevelNodeFromPath)
{
xpathBuilder.Append(XPathStrings.RootDocumentsAndVariants); // first node is not in the url
}
else
{
xpathBuilder.Append(XPathStringsDefinition.Root);
}
}
else
{
@@ -444,11 +499,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
varsList = varsList ?? new List<XPathVariable>();
var varName = string.Format("var{0}", partsIndex);
varsList.Add(new XPathVariable(varName, part));
xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlNameVar, varName);
xpathBuilder.AppendFormat(XPathStrings.ChildDocumentOrVariantByUrlNameVar, varName);
}
else
{
xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlName, part);
xpathBuilder.AppendFormat(XPathStrings.ChildDocumentOrVariantByUrlName, part);
}
}
@@ -8,6 +8,7 @@ using System.Xml.XPath;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Models;
@@ -21,33 +22,39 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
[XmlType(Namespace = "http://umbraco.org/webservices/")]
internal class XmlPublishedContent : PublishedContentBase
{
/// <summary>
/// Initializes a new instance of the <c>XmlPublishedContent</c> class with an Xml node.
/// </summary>
/// <param name="xmlNode">The Xml node.</param>
/// <param name="isPreviewing">A value indicating whether the published content is being previewed.</param>
public XmlPublishedContent(XmlNode xmlNode, bool isPreviewing)
/// <summary>
/// Initializes a new instance of the <c>XmlPublishedContent</c> class with an Xml node.
/// </summary>
/// <param name="xmlNode">The Xml node.</param>
/// <param name="isPreviewing">A value indicating whether the published content is being previewed.</param>
/// <param name="variantInfo"></param>
public XmlPublishedContent(XmlNode xmlNode, bool isPreviewing, VariantInfo variantInfo)
{
_xmlNode = xmlNode;
if (variantInfo == null) throw new ArgumentNullException("variantInfo");
_xmlNode = xmlNode;
_isPreviewing = isPreviewing;
InitializeStructure();
_variantInfo = variantInfo;
InitializeStructure();
Initialize();
InitializeChildren();
}
/// <summary>
/// Initializes a new instance of the <c>XmlPublishedContent</c> class with an Xml node,
/// and a value indicating whether to lazy-initialize the instance.
/// </summary>
/// <param name="xmlNode">The Xml node.</param>
/// <param name="isPreviewing">A value indicating whether the published content is being previewed.</param>
/// <param name="lazyInitialize">A value indicating whether to lazy-initialize the instance.</param>
/// <remarks>Lazy-initializationg is NOT thread-safe.</remarks>
internal XmlPublishedContent(XmlNode xmlNode, bool isPreviewing, bool lazyInitialize)
/// <summary>
/// Initializes a new instance of the <c>XmlPublishedContent</c> class with an Xml node,
/// and a value indicating whether to lazy-initialize the instance.
/// </summary>
/// <param name="xmlNode">The Xml node.</param>
/// <param name="isPreviewing">A value indicating whether the published content is being previewed.</param>
/// <param name="lazyInitialize">A value indicating whether to lazy-initialize the instance.</param>
/// <param name="variantInfo"></param>
/// <remarks>Lazy-initializationg is NOT thread-safe.</remarks>
internal XmlPublishedContent(XmlNode xmlNode, bool isPreviewing, bool lazyInitialize, VariantInfo variantInfo)
{
_xmlNode = xmlNode;
if (variantInfo == null) throw new ArgumentNullException("variantInfo");
_xmlNode = xmlNode;
_isPreviewing = isPreviewing;
InitializeStructure();
_variantInfo = variantInfo;
InitializeStructure();
if (lazyInitialize == false)
{
Initialize();
@@ -82,6 +89,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
private int _level;
private bool _isDraft;
private readonly bool _isPreviewing;
private readonly VariantInfo _variantInfo;
private PublishedContentType _contentType;
public override IEnumerable<IPublishedContent> Children
@@ -337,8 +345,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
var parent = _xmlNode == null ? null : _xmlNode.ParentNode;
if (parent == null) return;
if (parent.Name == "node" || (parent.Attributes != null && parent.Attributes.GetNamedItem("isDoc") != null))
_parent = (new XmlPublishedContent(parent, _isPreviewing, true)).CreateModel();
if (parent.LocalName == "node" || (parent.Attributes != null && parent.Attributes.GetNamedItem("isDoc") != null))
_parent = (new XmlPublishedContent(parent, _isPreviewing, true, _variantInfo)).CreateModel();
}
private void Initialize()
@@ -381,7 +389,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
}
else
{
_docTypeAlias = _xmlNode.Name;
_docTypeAlias = _xmlNode.LocalName;
}
if (_xmlNode.Attributes.GetNamedItem("nodeType") != null)
@@ -412,7 +420,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
var alias = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema
? n.Attributes.GetNamedItem("alias").Value
: n.Name;
: n.LocalName;
propertyNodes[alias.ToLowerInvariant()] = n;
}
@@ -432,17 +440,69 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
if (_xmlNode == null) return;
var workingNode = _xmlNode;
//if we're current using a variant, we need to go lookup the master since that is
// how we navigate the tree - always via the master
if (_variantInfo.IsVariant)
{
var masterDocId = _xmlNode.AttributeValue<int>("masterDocId");
//get the master, then children
if (_xmlNode.ParentNode != null)
{
//TODO: There's probably a faster way with the old xml api to do this
var master = _xmlNode.ParentNode.ChildNodes.OfType<XmlElement>().FirstOrDefault(x => x.AttributeValue<int>("id") == masterDocId);
if (master != null)
{
workingNode = master;
}
}
}
// load children
var childXPath = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema ? "node" : "* [@isDoc]";
var nav = _xmlNode.CreateNavigator();
//variants have isDoc='variant' so we don't want those
var childXPath = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema ? "node" : "* [@isDoc='']";
var nav = workingNode.CreateNavigator();
var expr = nav.Compile(childXPath);
expr.AddSort("@sortOrder", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Number);
var iterator = nav.Select(expr);
while (iterator.MoveNext())
_children.Add(
(new XmlPublishedContent(((IHasXmlNode)iterator.Current).GetNode(), _isPreviewing, true)).CreateModel());
// warn: this is not thread-safe
while (iterator.MoveNext())
{
var xmlChild = ((IHasXmlNode) iterator.Current).GetNode();
var useVariant = false;
//determine which child to add based on the current VariantInfo
if (_variantInfo.IsVariant)
{
//TODO: There's probably a faster way with the old xml api to do this
//find a variant of the current child for the current variant key
var variant = workingNode.ChildNodes.OfType<XmlElement>()
.FirstOrDefault(x =>
//find all
x.AttributeValue<int>("masterDocId") == xmlChild.AttributeValue<int>("id")
&& x.AttributeValue<string>("variantKey") == _variantInfo.Key);
if (variant != null)
{
//we found a variant!
_children.Add(
(new XmlPublishedContent(variant, _isPreviewing, true, _variantInfo)).CreateModel());
useVariant = true;
}
}
//no variant used, use the normal child
if (useVariant == false)
{
_children.Add(
(new XmlPublishedContent(xmlChild, _isPreviewing, true, _variantInfo)).CreateModel());
}
}
// warn: this is not thread-safe
_childrenInitialized = true;
}
}
@@ -1,6 +1,9 @@
using System.Linq;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Web.Routing.Segments;
namespace Umbraco.Web.Routing
{
@@ -19,14 +22,18 @@ namespace Umbraco.Web.Routing
/// <returns>A value indicating whether an Umbraco document was found and assigned.</returns>
public virtual bool TryFindContent(PublishedContentRequest docRequest)
{
string route;
if (docRequest.HasDomain)
route = docRequest.Domain.RootNodeId.ToString() + DomainHelper.PathRelativeToDomain(docRequest.DomainUri, docRequest.Uri.GetAbsolutePathDecoded());
else
route = docRequest.Uri.GetAbsolutePathDecoded();
var node = FindContent(docRequest, route);
return node != null;
if (docRequest.HasDomain)
{
var route = docRequest.Domain.RootNodeId + DomainHelper.PathRelativeToDomain(docRequest.DomainUri, docRequest.Uri.GetAbsolutePathDecoded());
var node = FindContent(docRequest, route);
return node != null;
}
else
{
var route = docRequest.Uri.GetAbsolutePathDecoded();
var node = FindContent(docRequest, route);
return node != null;
}
}
/// <summary>
@@ -39,10 +46,89 @@ namespace Umbraco.Web.Routing
{
LogHelper.Debug<ContentFinderByNiceUrl>("Test route \"{0}\"", () => route);
var node = docreq.RoutingContext.UmbracoContext.ContentCache.GetByRoute(route);
if (node != null)
{
docreq.PublishedContent = node;
var hasVariant = false;
//so we have a node but we need to check if a domain is assigned, if one is then we might have a variant for this
// particular language, so let's check
if (docreq.HasDomain)
{
var variant = docreq.RoutingContext.UmbracoContext.ContentCache.GetSingleByXPath(
string.Format("/root//* [@masterDocId='{0}' and @variantKey='{1}']", node.Id, docreq.Domain.Language.CultureAlias));
//if there is no variant found, then continue using the master doc content
if (variant != null)
{
//TODO: If content is found, but it has variants of the language assigned to the domain found here do we
// match against the variant? Or do we match against the master? Or do we 404 ?
//there is a variant for this culture for this domain so we will use that
node = variant;
hasVariant = true;
//Set the pcr val
docreq.SetVariantInfo(
node,
new VariantInfo(node.Id, docreq.Domain.Language.CultureAlias));
}
}
var reqSegments = docreq.RoutingContext.UmbracoContext.RequestSegments;
//if no variant was assigned above, then we can check if there's any segment matches and assign a
// custom variant if one is found
// TODO: Do we want to allow this logic to execute when a Domain is detected?
if (hasVariant == false && reqSegments.AssignedSegments.Any())
{
var segmentProviderStatus = ContentSegmentProvidersStatus.GetProviderStatus();
var assignableVariants = ContentSegmentProviderResolver.Current.GetAssignableVariants(segmentProviderStatus);
//get all possible variants for this request if the keys exist
var allMatchedVariantsByKey = assignableVariants.Where(x => reqSegments.RequestIs(x.SegmentMatchKey));
var matchedVariant = allMatchedVariantsByKey.FirstOrDefault(x =>
reqSegments.RequestIs(x.SegmentMatchKey)
|| reqSegments.RequestEquals(x.SegmentMatchKey, x.SegmentMatchValue));
if (matchedVariant != null)
{
//an advertised key is matched, lets see if there's a match (either on boolean or value)
var isMatch = reqSegments.RequestIs(matchedVariant.SegmentMatchKey)
|| reqSegments.RequestEquals(matchedVariant.SegmentMatchKey, matchedVariant.SegmentMatchValue);
if (isMatch)
{
//if the request has a matched segment then let's try to lookup the variant
var variant = docreq.RoutingContext.UmbracoContext.ContentCache.GetSingleByXPath(
string.Format("/root//* [@masterDocId='{0}' and @variantKey='{1}']", node.Id, matchedVariant.SegmentMatchKey));
//assign the variant if there is one
if (variant != null)
{
node = variant;
hasVariant = true;
//Set the pcr val
docreq.SetVariantInfo(
node,
new VariantInfo(node.Id, matchedVariant.SegmentMatchKey));
}
}
}
}
if (hasVariant == false)
{
//assign the content as per normal
docreq.PublishedContent = node;
}
LogHelper.Debug<ContentFinderByNiceUrl>("Got content, id={0}", () => node.Id);
}
else
@@ -88,6 +88,10 @@ namespace Umbraco.Web.Routing
{
Version = version;
//TODO: Need to figure out where variants fit in here, probably in most cases below
// we'd want to do isDoc='' so that we don't match variants. I have done this for now
// since we'd rather not match variants than do match them.
switch (version)
{
// legacy XML schema
@@ -101,8 +105,10 @@ namespace Umbraco.Web.Routing
// default XML schema as of 4.10
case 1:
DescendantDocumentById = "//* [@isDoc and @id={0}]";
DescendantDocumentByAlias = "//* [@isDoc and ("
//variants have isDoc='variant' so we don't want those
DescendantDocumentById = "//* [@isDoc='' and @id={0}]";
DescendantDocumentByAlias = "//* [@isDoc='' and ("
+ "contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',{0},')"
+ " or contains(concat(',',translate(umbracoUrlAlias, ' ', ''),','),',/{0},')"
+ ")]";
@@ -6,6 +6,8 @@ using Umbraco.Core.Models;
using umbraco;
using umbraco.cms.businesslogic.web;
using Umbraco.Core.Models.ContentVariations;
using Umbraco.Web.PublishedCache;
using RenderingEngine = Umbraco.Core.RenderingEngine;
namespace Umbraco.Web.Routing
@@ -47,6 +49,7 @@ namespace Umbraco.Web.Routing
_engine = new PublishedContentRequestEngine(this);
RenderingEngine = RenderingEngine.Unknown;
VariantInfo = new VariantInfo();
}
/// <summary>
@@ -411,6 +414,36 @@ namespace Umbraco.Web.Routing
set { _umbracoPage = value; }
}
/// <summary>
/// Assigns the variant info to the request and re-get's the content item from cache so that it
/// is initialized with the correct variant info taken from this PCR
/// </summary>
/// <param name="current"></param>
/// <param name="variantInfo"></param>
internal void SetVariantInfo(IPublishedContent current, VariantInfo variantInfo)
{
//TODO: The way this works is real dodgy!
if (current == null) throw new ArgumentNullException("current");
if (variantInfo == null) throw new ArgumentNullException("variantInfo");
//set the info on the PCR
VariantInfo = variantInfo;
//re-get the item now that the variant info is set
var reGet = RoutingContext.UmbracoContext.ContentCache.GetById(
RoutingContext.UmbracoContext.InPreviewMode,
current.Id);
//re-assign
PublishedContent = reGet;
}
/// <summary>
/// Gets the current variant info assigned to the request
/// </summary>
public VariantInfo VariantInfo { get; private set; }
#region Status
/// <summary>
+68 -45
View File
@@ -1,77 +1,100 @@
using System;
using System.Collections.Generic;
using System.ServiceModel;
using Umbraco.Web.PublishedCache;
namespace Umbraco.Web.Routing
{
/// <summary>
/// Provides context for the routing of a request.
/// </summary>
/// <summary>
/// Provides context for the routing of a request.
/// </summary>
public class RoutingContext
{
private readonly Lazy<UrlProvider> _urlProvider;
private readonly Lazy<IEnumerable<IContentFinder>> _publishedContentFinders;
private readonly Lazy<IContentFinder> _publishedContentLastChanceFinder;
private readonly Lazy<UrlProvider> _urlProvider;
private readonly Lazy<IEnumerable<IContentFinder>> _publishedContentFinders;
private readonly Lazy<IContentFinder> _publishedContentLastChanceFinder;
/// <summary>
/// Initializes a new instance of the <see cref="RoutingContext"/> class.
/// </summary>
/// <param name="umbracoContext"> </param>
/// <param name="contentFinders">The document lookups resolver.</param>
/// <param name="contentLastChanceFinder"> </param>
/// <param name="urlProvider">The nice urls provider.</param>
internal RoutingContext(
UmbracoContext umbracoContext,
IEnumerable<IContentFinder> contentFinders,
IContentFinder contentLastChanceFinder,
/// <summary>
/// Initializes a new instance of the <see cref="RoutingContext"/> class.
/// </summary>
/// <param name="umbracoContext"> </param>
/// <param name="contentFinders">The document lookups resolver.</param>
/// <param name="contentLastChanceFinder"> </param>
/// <param name="urlProvider">The nice urls provider.</param>
internal RoutingContext(
UmbracoContext umbracoContext,
IEnumerable<IContentFinder> contentFinders,
IContentFinder contentLastChanceFinder,
UrlProvider urlProvider)
: this(umbracoContext)
{
UmbracoContext = umbracoContext;
_publishedContentFinders = new Lazy<IEnumerable<IContentFinder>>(() => contentFinders, false);
_publishedContentLastChanceFinder = new Lazy<IContentFinder>(() => contentLastChanceFinder, false);
_urlProvider = new Lazy<UrlProvider>(() => urlProvider, false);
_urlProvider = new Lazy<UrlProvider>(() => urlProvider, false);
}
/// <summary>
/// Initializes a new instance of the <see cref="RoutingContext"/> class.
/// </summary>
/// <param name="umbracoContext"></param>
/// <param name="contentFinders"></param>
/// <param name="contentLastChanceFinder"></param>
/// <param name="urlProvider"></param>
internal RoutingContext(
UmbracoContext umbracoContext,
Lazy<IEnumerable<IContentFinder>> contentFinders,
Lazy<IContentFinder> contentLastChanceFinder,
Lazy<UrlProvider> urlProvider)
: this(umbracoContext)
{
UmbracoContext = umbracoContext;
_publishedContentFinders = contentFinders;
_publishedContentLastChanceFinder = contentLastChanceFinder;
_urlProvider = urlProvider;
}
/// <summary>
/// Gets the Umbraco context.
/// </summary>
public UmbracoContext UmbracoContext { get; private set; }
/// <summary>
/// Gets the published content finders.
/// </summary>
internal IEnumerable<IContentFinder> PublishedContentFinders
{
get { return _publishedContentFinders.Value; }
/// <summary>
/// Creates the lazy assigned segments logic
/// </summary>
/// <param name="umbracoContext"></param>
private RoutingContext(UmbracoContext umbracoContext)
{
UmbracoContext = umbracoContext;
}
/// <summary>
/// Gets the published content last chance finder.
/// </summary>
internal IContentFinder PublishedContentLastChanceFinder
{
/// <summary>
/// Gets the Umbraco context.
/// </summary>
public UmbracoContext UmbracoContext { get; private set; }
/// <summary>
/// Gets the published content finders.
/// </summary>
internal IEnumerable<IContentFinder> PublishedContentFinders
{
get { return _publishedContentFinders.Value; }
}
/// <summary>
/// Gets the published content last chance finder.
/// </summary>
internal IContentFinder PublishedContentLastChanceFinder
{
get { return _publishedContentLastChanceFinder.Value; }
}
}
/// <summary>
/// Gets the urls provider.
/// </summary>
public UrlProvider UrlProvider
{
get { return _urlProvider.Value; }
}
/// <summary>
/// Gets the urls provider.
/// </summary>
public UrlProvider UrlProvider
{
get { return _urlProvider.Value; }
}
}
}
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Web;
using Umbraco.Web.Models.Segments;
namespace Umbraco.Web.Routing.Segments
{
/// <summary>
/// Assigns segments based on MS's HttpBrowserCapabilities object
/// </summary>
[DisplayName("Browser capabilities provider")]
[Description("Uses ASP.Net HttpBrowserCapabilities object to analyze the current request")]
[ContentVariant("Mobile users", "IsMobileDevice")]
[ContentVariant("Java applet people", "JavaApplets")]
internal class BrowserCapabilitiesProvider : ContentSegmentProvider
{
public BrowserCapabilitiesProvider()
{
_browserCapabilityProps = typeof(HttpBrowserCapabilitiesBase).GetProperties()
.Where(x => PropNames.Contains(x.Name))
.ToArray();
}
private readonly PropertyInfo[] _browserCapabilityProps;
private static readonly string[] PropNames =
{
"Browser",
"IsMobileDevice",
"JavaApplets",
"MajorVersion",
"MinorVersion",
"MobileDeviceModel",
"MobileDeviceManufacturer",
"Platform"
};
public override SegmentCollection GetSegmentsForRequest(Uri originalRequestUrl, Uri cleanedRequestUrl, HttpRequestBase httpRequest)
{
return new SegmentCollection(
_browserCapabilityProps
.Select(x => new Segment(x.Name, x.GetValue(httpRequest.Browser, null))));
}
}
}
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Web.Models.Segments;
namespace Umbraco.Web.Routing.Segments
{
/// <summary>
/// Similar to a normal segment provider but this lets admins of Umbraco choose a custom key/value to store in the current request
/// based on a value that the provider returns.
/// </summary>
/// <remarks>
/// An example of such a provider would be a 'ReferalProvider' for which the provider itself will inspect the current request's referrer, the
/// provider will return this value. If this provider is active, we will run the boolean logic configured for the provider
/// which would normally be a regex statement, if it matches the returned value then we will apply the configured key/value as a segment
/// in the request.
///
/// </remarks>
public abstract class ConfigurableSegmentProvider : ContentSegmentProvider
{
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
/// <summary>
/// Override to return the statically assigned variants for this provider as well as any segments
/// that are configured to be variants.
/// </summary>
public override IEnumerable<ContentVariantAttribute> AssignableContentVariants
{
get
{
return base.AssignableContentVariants.Union(
ReadSegmentConfiguration()
.Where(x => x.AllowedAsVariant)
.Select(x => new ContentVariantAttribute(x.Key, x.Key, x.Value)));
}
}
/// <summary>
/// Returns the current provider's value (i.e. if the provider was a referal provider, this would return the current referrer)
/// </summary>
public abstract object GetCurrentValue(Uri cleanedRequestUrl, HttpRequestBase httpRequest);
/// <summary>
/// By default this uses a regex statement to match but inheritors could do anything they want (i.e. dynamic compilation)
/// </summary>
/// <param name="matchStatement"></param>
/// <param name="cleanedRequestUrl"></param>
/// <param name="httpRequest"></param>
/// <returns></returns>
public virtual bool IsMatch(string matchStatement, Uri cleanedRequestUrl,
HttpRequestBase httpRequest)
{
if (matchStatement == null) throw new ArgumentNullException("matchStatement");
if (cleanedRequestUrl == null) throw new ArgumentNullException("cleanedRequestUrl");
if (httpRequest == null) throw new ArgumentNullException("httpRequest");
var val = GetCurrentValue(cleanedRequestUrl, httpRequest);
if (val == null) return false;
return Regex.IsMatch(val.ToString(), matchStatement);
}
public override SegmentCollection GetSegmentsForRequest(Uri originalRequestUrl,
Uri cleanedRequestUrl,
HttpRequestBase httpRequest)
{
if (originalRequestUrl == null) throw new ArgumentNullException("originalRequestUrl");
if (cleanedRequestUrl == null) throw new ArgumentNullException("cleanedRequestUrl");
if (httpRequest == null) throw new ArgumentNullException("httpRequest");
var config = ReadSegmentConfiguration();
var type = this.GetType();
var result = config
.Where(match => IsMatch(match.MatchExpression, cleanedRequestUrl, httpRequest))
.Select(match => new Segment(match.Key, match.Value, match.Persist));
return new SegmentCollection(result);
}
public IEnumerable<SegmentProviderMatch> ReadSegmentConfiguration()
{
using (new ReadLock(_lock))
{
var fileName = IOHelper.MapPath("~/App_Data/Segments/" + GetType().Namespace.EnsureEndsWith('.') + GetType().Name + ".segments.json");
if (File.Exists(fileName) == false) return Enumerable.Empty<SegmentProviderMatch>();
var content = File.ReadAllText(fileName);
var result = JsonConvert.DeserializeObject<IEnumerable<SegmentProviderMatch>>(content);
//remove any entries without keys - safety check
return result.Where(x => x.Key.IsNullOrWhiteSpace() == false);
}
}
public void WriteSegmentConfiguration(IEnumerable<SegmentProviderMatch> config)
{
using (new WriteLock(_lock))
{
var json = JsonConvert.SerializeObject(config);
var fileName = GetType().Namespace.EnsureEndsWith('.') + GetType().Name + ".segments.json";
Directory.CreateDirectory(IOHelper.MapPath("~/App_Data/Segments"));
File.WriteAllText(IOHelper.MapPath("~/App_Data/Segments/" + fileName), json);
}
}
}
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Web;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Web.Models.Segments;
namespace Umbraco.Web.Routing.Segments
{
/// <summary>
/// Returns the segment names to assign to the current request
/// </summary>
/// <remarks>
/// The provider also exposes via attributes which static segments can be applied to content variations
/// </remarks>
public abstract class ContentSegmentProvider
{
protected ContentSegmentProvider()
{
//ensure attributes exist
var type = GetType();
var nameAtt = type.GetCustomAttribute<DisplayNameAttribute>(false);
var descAtt = type.GetCustomAttribute<DescriptionAttribute>(false);
if (nameAtt == null || descAtt == null)
{
throw new ApplicationException(
"The segment provider " + type + " must be attributed with two attributes: " + typeof(DisplayNameAttribute) + " and " + typeof(DescriptionAttribute));
}
Name = nameAtt.DisplayName;
Description = descAtt.Description;
}
public string Name { get; private set; }
public string Description { get; private set; }
/// <summary>
/// Returns the Content Variants that this provider supports
/// </summary>
public virtual IEnumerable<ContentVariantAttribute> AssignableContentVariants
{
get { return GetType().GetCustomAttributes<ContentVariantAttribute>(false).ToArray(); }
}
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
/// <summary>
/// Returns the status of enabled/disabled variants for a given provider
/// </summary>
/// <returns></returns>
public IDictionary<string, bool> ReadVariantConfiguration()
{
using (new ReadLock(_lock))
{
var fileName = IOHelper.MapPath("~/App_Data/Segments/" + GetType().Namespace.EnsureEndsWith('.') + GetType().Name + ".variants.json");
if (File.Exists(fileName) == false) return new Dictionary<string, bool>();
var contents = File.ReadAllText(fileName);
var result = JsonConvert.DeserializeObject<IDictionary<string, bool>>(contents);
return result;
}
}
/// <summary>
/// Writes the status of enabled/disabled variants for a given provider
/// </summary>
/// <param name="variantConfig"></param>
public void WriteVariantConfiguration(IDictionary<string, bool> variantConfig)
{
using (new WriteLock(_lock))
{
var json = JsonConvert.SerializeObject(variantConfig);
var fileName = GetType().Namespace.EnsureEndsWith('.') + GetType().Name + ".variants.json";
Directory.CreateDirectory(IOHelper.MapPath("~/App_Data/Segments"));
File.WriteAllText(IOHelper.MapPath("~/App_Data/Segments/" + fileName), json);
}
}
/// <summary>
/// Returns the segment names and values to assign to the current request
/// </summary>
/// <param name="originalRequestUrl"></param>
/// <param name="cleanedRequestUrl"></param>
/// <param name="httpRequest"></param>
/// <returns></returns>
public abstract SegmentCollection GetSegmentsForRequest(
Uri originalRequestUrl,
Uri cleanedRequestUrl,
HttpRequestBase httpRequest);
}
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.ObjectResolution;
using System.Linq;
namespace Umbraco.Web.Routing.Segments
{
/// <summary>
/// Resolver for content variation providers
/// </summary>
public sealed class ContentSegmentProviderResolver : ManyObjectsResolverBase<ContentSegmentProviderResolver, ContentSegmentProvider>
{
/// <summary>
/// Initializes a new instance of the <see cref="ContentSegmentProviderResolver"/> class with
/// an initial list of provider types.
/// </summary>
/// <param name="converters">The list of provider types</param>
/// <remarks>The resolver is created by the <c>WebBootManager</c> and thus the constructor remains internal.</remarks>
internal ContentSegmentProviderResolver(IEnumerable<Type> converters)
: base(converters)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="ContentSegmentProviderResolver"/> class with
/// an initial list of provider types.
/// </summary>
/// <param name="converters">The list of provider types</param>
/// <remarks>The resolver is created by the <c>WebBootManager</c> and thus the constructor remains internal.</remarks>
internal ContentSegmentProviderResolver(params Type[] converters)
: base(converters)
{ }
/// <summary>
/// Gets the providers.
/// </summary>
public IEnumerable<ContentSegmentProvider> Providers
{
get { return Values; }
}
public IEnumerable<ContentVariantAttribute> GetAssignableVariants(IDictionary<string, bool> segmentProviderStatus)
{
//These are the assignable variants based on the installed providers (statically advertised variants) or
// configured segments that have been flagged as variants
// that are enabled via the back office. If they are not enabled, they will not show up.
var assignableSegments = this.Providers
//don't lookup anything in any providers that are not enabled
.Where(provider => segmentProviderStatus[provider.GetType().FullName] == true)
.Select(provider => new
{
instance = provider,
//get the keys that have been allowed
enabledVariants = provider.ReadVariantConfiguration()
.Where(vari => vari.Value) // the value == true
.Select(vari => vari.Key).ToArray() // get the key
})
//only allow the onces that are enabled
.SelectMany(x =>
x.instance.AssignableContentVariants.Where(vari => x.enabledVariants.Contains(vari.SegmentMatchKey)));
return assignableSegments;
}
}
}
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.IO;
namespace Umbraco.Web.Routing.Segments
{
public class ContentSegmentProvidersStatus
{
private readonly static ReaderWriterLockSlim Lock = new ReaderWriterLockSlim();
/// <summary>
/// Returns a dictionary containing the provider type name and whether it is enabled or not
/// </summary>
/// <returns></returns>
public static IDictionary<string, bool> GetProviderStatus()
{
using (new ReadLock(Lock))
{
var file = IOHelper.MapPath("~/App_Data/Segments/providers.config.json");
if (File.Exists(file) == false)
{
//empty
return new Dictionary<string, bool>();
}
var contents = File.ReadAllText(file);
var result = JsonConvert.DeserializeObject<IDictionary<string, bool>>(contents);
return result;
}
}
public static void SaveProvidersStatus(IDictionary<string, bool> providersStatus)
{
using (new WriteLock(Lock))
{
var file = IOHelper.MapPath("~/App_Data/Segments/providers.config.json");
var result = JsonConvert.SerializeObject(providersStatus);
Directory.CreateDirectory(IOHelper.MapPath("~/App_Data/Segments"));
File.WriteAllText(file, result);
}
}
}
}
@@ -0,0 +1,46 @@
using System;
namespace Umbraco.Web.Routing.Segments
{
/// <summary>
/// Defines a custom Content Variant exposed by a segment provider
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ContentVariantAttribute : Attribute
{
public string VariantName { get; set; }
public string SegmentMatchKey { get; set; }
public object SegmentMatchValue { get; set; }
/// <summary>
/// Constructor used to specify the content variant name and the segment key to match, in
/// order for the request to match on this variant the value stored against the segmentMatchKey must
/// be a boolean and must be true.
/// </summary>
/// <param name="variantName"></param>
/// <param name="segmentMatchKey"></param>
public ContentVariantAttribute(string variantName, string segmentMatchKey)
{
VariantName = variantName;
SegmentMatchKey = segmentMatchKey;
//must equal true for this overload
SegmentMatchValue = true;
}
/// <summary>
/// Constructor used to specify the content variant name and the segment key to match, in
/// order for the request to match on this variant the value stored against the segmentMatchKey must
/// be equal to the segmentMatchValue specified
/// </summary>
/// <param name="variantName"></param>
/// <param name="segmentMatchKey"></param>
/// <param name="segmentMatchValue"></param>
public ContentVariantAttribute(string variantName, string segmentMatchKey, object segmentMatchValue)
{
VariantName = variantName;
SegmentMatchKey = segmentMatchKey;
SegmentMatchValue = segmentMatchValue;
}
}
}
@@ -0,0 +1,19 @@
using System;
using System.ComponentModel;
using System.Web;
namespace Umbraco.Web.Routing.Segments
{
/// <summary>
/// A configurable provider to match against the referrer
/// </summary>
[DisplayName("Referrer provider")]
[Description("A configurable provider that analyzes the current request's referrer")]
public class ReferrerSegmentProvider : ConfigurableSegmentProvider
{
public override object GetCurrentValue(Uri cleanedRequestUrl, HttpRequestBase httpRequest)
{
return httpRequest.UrlReferrer == null ? "" : httpRequest.UrlReferrer.OriginalString;
}
}
}
@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Web.Models.Segments;
namespace Umbraco.Web.Routing.Segments
{
/// <summary>
/// Used to retreive (and persist) the segments that have been found based on the current request information
/// </summary>
/// <remarks>
/// When retreiving the segments, this is done lazily and during that execution it will also persist the
/// matched advertised values to a cookie.
///
/// This class is NOT thread safe
///
/// </remarks>
public class RequestSegments
{
private readonly IEnumerable<ContentSegmentProvider> _segmentProviders;
private readonly Lazy<SegmentCollection> _assignedSegments;
public RequestSegments(IEnumerable<ContentSegmentProvider> segmentProviders,
Uri originalRequestUrl,
Uri cleanedRequestUrl,
HttpRequestBase httpRequest)
{
_segmentProviders = segmentProviders;
_assignedSegments = new Lazy<SegmentCollection>(() =>
GetAllSegmentsForRequest(
_segmentProviders,
originalRequestUrl,
cleanedRequestUrl,
httpRequest,
ContentSegmentProvidersStatus.GetProviderStatus()),
//This class should ONLY be used by one thread at a time (i.e. current request)
LazyThreadSafetyMode.None);
}
/// <summary>
/// This will add a segment to the current instance - this is only used to manually add a segment during a request when
/// using the MemberSegment and is for performance improvements so we don't have to go to the db on each usage of MemberSegment.
/// </summary>
/// <param name="segment"></param>
internal void Add(Segment segment)
{
_assignedSegments.Value.AddNew(segment);
}
/// <summary>
/// This method must be called in order to persist the segments to cookie
/// </summary>
/// <remarks>
/// This will not overwrite cookie data that isn't contained in the current request segments
/// </remarks>
internal void EnsurePersisted(HttpResponseBase response, HttpRequestBase request)
{
//NOTE: The cookie data will alraedy be part of the PersistedSegments (see GetAllSegmentsForRequest)
var toPersist = PersistedSegments.ToList();
if (toPersist.Any())
{
var json = JsonConvert.SerializeObject(toPersist);
//TODO: Implement the expiry dates in each individual segment - otherwise
// what is going to happen is that any persisted segment will have a sliding expiration
// of 30 days!
var cookie = new HttpCookie(Constants.Web.SegmentCookieName, json)
{
//sliding 30 day expiry?
Expires = DateTime.Now.AddDays(30),
HttpOnly = true
};
response.SetCookie(cookie);
}
else
{
response.SetCookie(new HttpCookie(Constants.Web.SegmentCookieName)
{
//remove it!
Expires = DateTime.Now.AddDays(-30)
});
}
}
/// <summary>
/// Returns the assigned segments for the current request
/// </summary>
public IEnumerable<Segment> AssignedSegments
{
get { return _assignedSegments.Value; }
}
/// <summary>
/// Returns the segments that are persisted (cookie and should be part of the member profile)
/// </summary>
internal IEnumerable<Segment> PersistedSegments
{
get { return _assignedSegments.Value.Where(x => x.Persist); }
}
/// <summary>
/// Returns true if the segment key exists in the request
/// </summary>
/// <param name="segmentKey"></param>
/// <returns></returns>
/// <remarks>
/// Example: RequestIs("Mobile") if a segment key is "Mobile" and it's value is a boolean true.
/// </remarks>
public bool RequestIs(string segmentKey)
{
return AssignedSegments.Any(x => x.Key == segmentKey);
}
/// <summary>
/// Returns true if any assigned segment key + value matches the specified parameters
/// </summary>
/// <param name="key"></param>
/// <param name="val"></param>
/// <returns></returns>
public bool RequestEquals(string key, object val)
{
return AssignedSegments.Any(x => x.Key == key && x.Value.Equals(val));
}
/// <summary>
/// Internal so it can be tested - collects all segments from providers and the ones in the cookie
/// </summary>
/// <param name="segmentProviders"></param>
/// <param name="originalRequestUrl"></param>
/// <param name="cleanedRequestUrl"></param>
/// <param name="httpRequest"></param>
/// <param name="providersStatus"></param>
/// <returns>
/// </returns>
internal static SegmentCollection GetAllSegmentsForRequest(
IEnumerable<ContentSegmentProvider> segmentProviders,
Uri originalRequestUrl,
Uri cleanedRequestUrl,
HttpRequestBase httpRequest,
IDictionary<string, bool> providersStatus)
{
//get all key/vals, there might be duplicates so we will simply take the last one in
var d = new List<Segment>();
foreach (var provider in segmentProviders
.Select(x => new
{
instance = x,
typeName = x.GetType().FullName
})
//ensure it is enabled
.Where(x => providersStatus.ContainsKey(x.typeName) && providersStatus[x.typeName])
.Select(x => x.instance))
{
var segments = provider.GetSegmentsForRequest(originalRequestUrl, cleanedRequestUrl, httpRequest).ToArray();
d.AddRange(segments);
}
var cookieData = httpRequest.Cookies[Constants.Web.SegmentCookieName] == null
? new List<Segment>()
//TODO: try/catch
: JsonConvert.DeserializeObject<IEnumerable<Segment>>(httpRequest.Cookies[Constants.Web.SegmentCookieName].Value);
//Add anything in the cookie that is not already in the list
d.AddRange(cookieData.Where(x => d.Contains(x) == false));
return new SegmentCollection(d);
}
}
}
@@ -1,32 +1,24 @@
using System.Collections.Generic;
using Umbraco.Core.Models;
using umbraco;
using Umbraco.Core.Models.Membership;
using Umbraco.Web.Security;
namespace Umbraco.Web.Routing
{
internal static class UrlProviderExtensions
{
/// <summary>
/// Gets the URLs for the content item
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
/// <remarks>
/// Use this when displaying URLs, if there are errors genertaing the urls the urls themselves will
/// contain the errors.
/// </remarks>
public static IEnumerable<string> GetContentUrls(this IContent content)
{
public static IEnumerable<string> GetContentUrls(this IContent content, IUser user, UrlProvider urlProvider)
{
var urls = new List<string>();
if (content.HasPublishedVersion() == false)
{
urls.Add(ui.Text("content", "itemNotPublished", UmbracoContext.Current.Security.CurrentUser));
urls.Add(ui.Text("content", "itemNotPublished", user));
return urls;
}
var urlProvider = UmbracoContext.Current.RoutingContext.UrlProvider;
var url = urlProvider.GetUrl(content.Id);
var url = urlProvider.GetUrl(content.Id);
if (url == "#")
{
// document as a published version yet it's url is "#" => a parent must be
@@ -39,9 +31,9 @@ namespace Umbraco.Web.Routing
while (parent != null && parent.Published);
if (parent == null) // oops - internal error
urls.Add(ui.Text("content", "parentNotPublishedAnomaly", UmbracoContext.Current.Security.CurrentUser));
urls.Add(ui.Text("content", "parentNotPublishedAnomaly", user));
else
urls.Add(ui.Text("content", "parentNotPublished", parent.Name, UmbracoContext.Current.Security.CurrentUser));
urls.Add(ui.Text("content", "parentNotPublished", parent.Name, user));
}
else
{
@@ -50,5 +42,21 @@ namespace Umbraco.Web.Routing
}
return urls;
}
/// <summary>
/// Gets the URLs for the content item
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
/// <remarks>
/// Use this when displaying URLs, if there are errors genertaing the urls the urls themselves will
/// contain the errors.
/// </remarks>
public static IEnumerable<string> GetContentUrls(this IContent content)
{
return content.GetContentUrls(
UmbracoContext.Current.Security.CurrentUser,
UmbracoContext.Current.RoutingContext.UrlProvider);
}
}
}
+12 -4
View File
@@ -342,10 +342,18 @@ namespace Umbraco.Web.Search
DeleteIndexForEntity(payload.Id, false);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
case UnpublishedPageCacheRefresher.OperationType.Saved:
if (payload.Id > 0)
{
var c5 = ApplicationContext.Current.Services.ContentService.GetById(payload.Id);
ReIndexForContent(c5, false);
}
break;
}
}
}
break;
+123
View File
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Models.Segments;
using Umbraco.Web.Routing.Segments;
namespace Umbraco.Web.Security
{
/// <summary>
/// This is similar tothe RequestSegments class, however the member segments is not just based on current request data, this will check for the segments
/// based on the current request, the segments cookie and finally the member instance.
/// </summary>
/// <remarks>
/// Ensure this class is only created in a Request scope!
/// </remarks>
public class MemberSegments
{
private readonly MembershipHelper _membershipHelper;
private readonly RequestSegments _reguestSegments;
private readonly ServiceContext _services;
public MemberSegments(MembershipHelper membershipHelper, RequestSegments reguestSegments, ServiceContext services)
{
if (membershipHelper == null) throw new ArgumentNullException("membershipHelper");
if (reguestSegments == null) throw new ArgumentNullException("reguestSegments");
if (services == null) throw new ArgumentNullException("services");
_membershipHelper = membershipHelper;
_reguestSegments = reguestSegments;
_services = services;
}
public IEnumerable<Segment> AssignedSegments
{
get
{
var requestAssigned = _reguestSegments.AssignedSegments.ToArray();
var memberPersisted = PersistedSegments;
//combine and return, the request supercedes the persisted
return requestAssigned.Union(memberPersisted.Except(requestAssigned));
}
}
public bool Is(string segmentKey)
{
//this will check the request (+ cookie)
var contains = _reguestSegments.RequestIs(segmentKey);
//if the request has the key, then return from the request
if (contains) return _reguestSegments.RequestIs(segmentKey);
//lookup from member
var fromMember = GetSegmentByKeyFromMember(segmentKey);
return fromMember != null;
}
public bool Equals(string segmentKey, object val)
{
//this will check the request (+ cookie)
var contains = _reguestSegments.RequestIs(segmentKey);
//if the request has the key, then return from the request
if (contains) return _reguestSegments.RequestEquals(segmentKey, val);
//lookup from member
var fromMember = GetSegmentByKeyFromMember(segmentKey);
return fromMember != null && fromMember.Value == val;
}
private IMember _member;
private bool _hasLoaded = false;
/// <summary>
/// Lazy loads and only loads once per request
/// </summary>
internal IMember CurrentMember
{
get
{
if (_member == null && _hasLoaded == false)
{
var memberId = _membershipHelper.GetCurrentMemberId();
if (memberId > 0)
{
_member = _services.MemberService.GetById(memberId);
}
_hasLoaded = true;
}
return _member;
}
}
private Segment[] _persistedSegments;
/// <summary>
/// Lazy loads the persisted once per request
/// </summary>
internal IEnumerable<Segment> PersistedSegments
{
get
{
if (_persistedSegments == null && CurrentMember != null && CurrentMember.HasProperty(Constants.Conventions.Member.Segments))
{
var val = CurrentMember.Properties[Constants.Conventions.Member.Segments].Value.ToString();
_persistedSegments = JsonConvert.DeserializeObject<Segment[]>(val);
}
return (_persistedSegments == null || _persistedSegments.Length == 0)
? Enumerable.Empty<Segment>()
: _persistedSegments;
}
}
private Segment GetSegmentByKeyFromMember(string segmentKey)
{
var segment = PersistedSegments.FirstOrDefault(x => x.Key == segmentKey);
if (segment == null) return null;
//add it to the request so we don't have to lookup in db again
_reguestSegments.Add(segment);
return segment;
}
}
}
+53 -15
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
@@ -9,8 +10,11 @@ using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Models;
using Umbraco.Web.Models.Segments;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing.Segments;
using MPE = global::Umbraco.Core.Security.MembershipProviderExtensions;
namespace Umbraco.Web.Security
@@ -21,24 +25,46 @@ namespace Umbraco.Web.Security
/// </summary>
public class MembershipHelper
{
private readonly ApplicationContext _applicationContext;
private readonly ServiceContext _services;
private readonly HttpContextBase _httpContext;
private readonly RequestSegments _requestSegments;
#region Constructors
[Obsolete("Use the other constructor accepting a RequestSegments instance")]
[EditorBrowsable(EditorBrowsableState.Never)]
public MembershipHelper(ApplicationContext applicationContext, HttpContextBase httpContext)
: this(applicationContext.Services, httpContext, UmbracoContext.Current.RequestSegments)
{
if (applicationContext == null) throw new ArgumentNullException("applicationContext");
if (httpContext == null) throw new ArgumentNullException("httpContext");
_applicationContext = applicationContext;
_httpContext = httpContext;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="services"></param>
/// <param name="httpContext"></param>
/// <param name="requestSegments"></param>
public MembershipHelper(ServiceContext services, HttpContextBase httpContext, RequestSegments requestSegments)
{
if (services == null) throw new ArgumentNullException("services");
if (httpContext == null) throw new ArgumentNullException("httpContext");
_services = services;
_httpContext = httpContext;
_requestSegments = requestSegments;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="umbracoContext"></param>
public MembershipHelper(UmbracoContext umbracoContext)
{
if (umbracoContext == null) throw new ArgumentNullException("umbracoContext");
_httpContext = umbracoContext.HttpContext;
_applicationContext = umbracoContext.Application;
_services = umbracoContext.Application.Services;
_requestSegments = umbracoContext.RequestSegments;
}
#endregion
/// <summary>
@@ -111,7 +137,7 @@ namespace Umbraco.Web.Security
}
}
_applicationContext.Services.MemberService.Save(member);
_services.MemberService.Save(member);
//reset the FormsAuth cookie since the username might have changed
FormsAuthentication.SetAuthCookie(member.Username, true);
@@ -146,7 +172,7 @@ namespace Umbraco.Web.Security
if (status != MembershipCreateStatus.Success) return null;
var member = _applicationContext.Services.MemberService.GetByUsername(membershipUser.UserName);
var member = _services.MemberService.GetByUsername(membershipUser.UserName);
member.Name = model.Name;
if (model.MemberProperties != null)
@@ -158,7 +184,7 @@ namespace Umbraco.Web.Security
}
}
_applicationContext.Services.MemberService.Save(member);
_services.MemberService.Save(member);
}
else
{
@@ -224,7 +250,7 @@ namespace Umbraco.Web.Security
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
}
var result = _applicationContext.Services.MemberService.GetByProviderKey(key);
var result = _services.MemberService.GetByProviderKey(key);
return result == null ? null : new MemberPublishedContent(result, provider.GetUser(result.Username, false));
}
@@ -236,7 +262,7 @@ namespace Umbraco.Web.Security
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
}
var result = _applicationContext.Services.MemberService.GetById(memberId);
var result = _services.MemberService.GetById(memberId);
return result == null ? null : new MemberPublishedContent(result, provider.GetUser(result.Username, false));
}
@@ -248,7 +274,7 @@ namespace Umbraco.Web.Security
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
}
var result = _applicationContext.Services.MemberService.GetByUsername(username);
var result = _services.MemberService.GetByUsername(username);
return result == null ? null : new MemberPublishedContent(result, provider.GetUser(result.Username, false));
}
@@ -260,7 +286,7 @@ namespace Umbraco.Web.Security
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
}
var result = _applicationContext.Services.MemberService.GetByEmail(email);
var result = _services.MemberService.GetByEmail(email);
return result == null ? null : new MemberPublishedContent(result, provider.GetUser(result.Username, false));
}
@@ -366,7 +392,7 @@ namespace Umbraco.Web.Security
if (provider.IsUmbracoMembershipProvider())
{
memberTypeAlias = memberTypeAlias ?? Constants.Conventions.MemberTypes.DefaultAlias;
var memberType = _applicationContext.Services.MemberTypeService.Get(memberTypeAlias);
var memberType = _services.MemberTypeService.Get(memberTypeAlias);
if (memberType == null)
throw new InvalidOperationException("Could not find a member type with alias " + memberTypeAlias);
@@ -789,6 +815,17 @@ namespace Umbraco.Web.Security
return Attempt<MembershipUser>.Fail(member);
}
#region Segments
private MemberSegments _segments;
public MemberSegments Segments
{
get { return _segments ?? (_segments = new MemberSegments(this, _requestSegments, _services)); }
}
#endregion
/// <summary>
/// Returns the currently logged in IMember object - this should never be exposed to the front-end since it's returning a business logic entity!
/// </summary>
@@ -802,9 +839,10 @@ namespace Umbraco.Web.Security
throw new NotSupportedException("An IMember model can only be retreived when using the built-in Umbraco membership providers");
}
var username = provider.GetCurrentUserName();
var member = _applicationContext.Services.MemberService.GetByUsername(username);
var member = _services.MemberService.GetByUsername(username);
return member;
}
}
}
@@ -62,7 +62,13 @@ namespace Umbraco.Web.Strategies.Publishing
/// </summary>
private void UpdateMultipleContentCache(IEnumerable<IContent> content)
{
DistributedCache.Instance.RefreshPageCache(content.ToArray());
var asArray = content.ToArray();
var ids = asArray.Select(x => x.Id).ToList();
//ensure the variants are refreshed too
ids.AddRange(asArray.SelectMany(x => x.VariantInfo.VariantIds));
ids.AddRange(asArray.Select(x => x.VariantInfo.MasterDocId));
DistributedCache.Instance.RefreshPageCache(ids.ToArray());
}
/// <summary>
@@ -70,7 +76,11 @@ namespace Umbraco.Web.Strategies.Publishing
/// </summary>
private void UpdateSingleContentCache(IContent content)
{
DistributedCache.Instance.RefreshPageCache(content);
var ids = new List<int> {content.Id};
//ensure the variants are refreshed too
ids.AddRange(content.VariantInfo.VariantIds);
ids.Add(content.VariantInfo.MasterDocId);
DistributedCache.Instance.RefreshPageCache(ids.ToArray());
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Configuration;
@@ -53,7 +54,11 @@ namespace Umbraco.Web.Strategies.Publishing
/// </summary>
private void UnPublishSingle(IContent content)
{
DistributedCache.Instance.RemovePageCache(content);
var ids = new List<int> { content.Id };
//ensure the variants unpublished too if it's a master
ids.AddRange(content.VariantInfo.VariantIds);
DistributedCache.Instance.RemovePageCache(ids.ToArray());
}
}
}
+27 -2
View File
@@ -77,8 +77,26 @@ namespace Umbraco.Web.Trees
protected override TreeNodeCollection PerformGetTreeNodes(string id, FormDataCollection queryStrings)
{
var nodes = new TreeNodeCollection();
var entities = GetChildEntities(id);
nodes.AddRange(entities.Select(entity => GetSingleTreeNode(entity, id, queryStrings)).Where(node => node != null));
var entities = GetChildEntities(id).ToArray();
//we don't want to show children that are variants
int iid;
if (int.TryParse(id, out iid) == false)
{
throw new InvalidCastException("The id for the media tree must be an integer");
}
//all ids of the entities to be rendered
var entityIds = entities.Select(x => x.Id).ToArray();
//now go lookup all relations of type umbContentVariants by child id, anything returned is
// a variant (not a master doc) so we need to exclude them.
var variantIds = Services.RelationService.GetByChildIds(entityIds, "umbContentVariants")
.Select(x => x.ChildId)
.ToArray();
//these are the master docs
var nonVariants = entities.Where(x => variantIds.Contains(x.Id) == false);
nodes.AddRange(nonVariants.Select(entity => GetSingleTreeNode(entity, id, queryStrings)).Where(node => node != null));
return nodes;
}
@@ -122,6 +140,13 @@ namespace Umbraco.Web.Trees
if (Access.IsProtected(e.Id, e.Path))
node.SetProtectedStyle();
//add some data describing that this node is a variant
var variantRelation = Services.RelationService.GetByChild(entity, "umbContentVariants").FirstOrDefault();
if (variantRelation != null)
{
node.AdditionalData["masterDocId"] = variantRelation.ParentId;
}
return node;
}
@@ -29,7 +29,7 @@ namespace Umbraco.Web.Trees
/// <param name="queryStrings"></param>
/// <returns></returns>
[HttpQueryStringFilter("queryStrings")]
public TreeNode GetTreeNode(string id, FormDataCollection queryStrings)
public virtual TreeNode GetTreeNode(string id, FormDataCollection queryStrings)
{
int asInt;
if (int.TryParse(id, out asInt) == false)
+17
View File
@@ -307,6 +307,7 @@
<Compile Include="Editors\EntityControllerActionSelector.cs" />
<Compile Include="Editors\EntityControllerConfigurationAttribute.cs" />
<Compile Include="Editors\ImagesController.cs" />
<Compile Include="Editors\SegmentDashboardController.cs" />
<Compile Include="Models\ContentEditing\ContentBaseItemSave.cs" />
<Compile Include="Models\ContentEditing\MediaItemSave.cs" />
<Compile Include="Models\DetachedContent.cs" />
@@ -357,6 +358,7 @@
<Compile Include="Models\PartialViewMacroModelExtensions.cs" />
<Compile Include="Models\PostRedirectModel.cs" />
<Compile Include="Models\PublishedProperty.cs" />
<Compile Include="Models\Segments\ContentVariableSegment.cs" />
<Compile Include="Mvc\ActionExecutedEventArgs.cs" />
<Compile Include="Mvc\NotFoundHandler.cs" />
<Compile Include="Mvc\PreRenderViewActionFilterAttribute.cs" />
@@ -470,7 +472,22 @@
<Compile Include="PublishedContentQuery.cs" />
<Compile Include="ImageCropperTemplateExtensions.cs" />
<Compile Include="Mvc\UmbracoVirtualNodeRouteHandler.cs" />
<Compile Include="Routing\Segments\ConfigurableSegmentProvider.cs" />
<Compile Include="Routing\Segments\ContentSegmentProvidersStatus.cs" />
<Compile Include="Routing\Segments\ReferrerSegmentProvider.cs" />
<Compile Include="Routing\Segments\BrowserCapabilitiesProvider.cs" />
<Compile Include="Routing\Segments\RequestSegments.cs" />
<Compile Include="Routing\Segments\ContentVariantAttribute.cs" />
<Compile Include="Routing\Segments\ContentSegmentProvider.cs" />
<Compile Include="Routing\Segments\ContentSegmentProviderResolver.cs" />
<Compile Include="Models\Segments\Segment.cs" />
<Compile Include="Models\Segments\SegmentCollection.cs" />
<Compile Include="Models\Segments\SegmentProviderMatch.cs" />
<Compile Include="Models\Segments\SegmentCollection.cs" />
<Compile Include="Models\Segments\SegmentProviderMatch.cs" />
<Compile Include="Routing\UrlProviderExtensions.cs" />
<Compile Include="Security\MemberSegments.cs" />
<Compile Include="Strategies\Migrations\ClearCsrfCookiesAfterUpgrade.cs" />
<Compile Include="Strategies\Migrations\ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs" />
<Compile Include="Strategies\NotificationsHandler.cs" />
+17
View File
@@ -2,8 +2,10 @@
using System.Collections.Generic;
using System.Web;
using Umbraco.Core;
using Umbraco.Web.Models.Segments;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Routing.Segments;
using Umbraco.Web.Security;
using umbraco;
using umbraco.BusinessLogic;
@@ -315,6 +317,21 @@ namespace Umbraco.Web
}
}
private RequestSegments _requestSegments;
/// <summary>
/// Return the current request segments
/// </summary>
public RequestSegments RequestSegments
{
get
{
return _requestSegments ?? (_requestSegments = new RequestSegments(
ContentSegmentProviderResolver.Current.Providers,
OriginalRequestUrl, CleanedUmbracoUrl, HttpContext.Request));
}
}
/// <summary>
/// This is used internally for performance calculations, the ObjectCreated DateTime is set as soon as this
/// object is instantiated which in the web site is created during the BeginRequest phase.
+83 -6
View File
@@ -13,6 +13,7 @@ using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Security;
using Umbraco.Web.Editors;
using Umbraco.Web.Routing;
@@ -147,10 +148,18 @@ namespace Umbraco.Web
if (HandleHttpResponseStatus(httpContext, pcr))
return;
if (!pcr.HasPublishedContent)
httpContext.RemapHandler(new PublishedContentNotFoundHandler());
else
RewriteToUmbracoHandler(httpContext, pcr);
if (pcr.HasPublishedContent == false)
{
httpContext.RemapHandler(new PublishedContentNotFoundHandler());
}
else
{
//It's a front-end request, we'll make sure the segments are written
umbracoContext.RequestSegments.EnsurePersisted(httpContext.Response, httpContext.Request);
RewriteToUmbracoHandler(httpContext, pcr);
}
}
/// <summary>
@@ -512,6 +521,73 @@ namespace Umbraco.Web
urlRouting.PostResolveRequestCache(context);
}
/// <summary>
/// Check if the request is a front-end request, check if the member is logged in, then persist the request segments
/// to the member.
/// </summary>
/// <param name="httpContext"></param>
static void PersistMemberSegmentData(HttpContextBase httpContext)
{
if (httpContext.User != null && httpContext.User.Identity.IsAuthenticated
&& UmbracoContext.Current != null
&& UmbracoContext.Current.IsFrontEndUmbracoRequest
&& Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider().IsUmbracoMembershipProvider())
{
var persisted = UmbracoContext.Current.RequestSegments.PersistedSegments.ToArray();
if (persisted.Any())
{
var member = ApplicationContext.Current.Services.MemberService.GetByUsername(
httpContext.User.Identity.Name);
if (member != null)
{
var segmentsProp = member.Properties.FirstOrDefault(x => x.Alias == Constants.Conventions.Member.Segments);
if (segmentsProp == null || segmentsProp.HasIdentity == false)
{
//we need to create the segments property
var memberType = member.ContentType;
try
{
if (segmentsProp == null)
{
//if there is no property, we'll create it, chances are it exists by default with an empty identity
memberType.AddPropertyType(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Ntext, true)
{
Alias = Constants.Conventions.Member.Segments,
Name = Constants.Conventions.Member.SegmentsLabel
});
}
ApplicationContext.Current.Services.MemberTypeService.Save(memberType);
}
catch (Exception ex)
{
LogHelper.Error<UmbracoModule>("Could not create the segments property type for the current member", ex);
return;
}
//re-get the member
member = ApplicationContext.Current.Services.MemberService.GetByUsername(
httpContext.User.Identity.Name);
}
try
{
//save as json
member.SetValue(Constants.Conventions.Member.Segments, JsonConvert.SerializeObject(persisted));
ApplicationContext.Current.Services.MemberService.Save(member);
}
catch (Exception ex)
{
LogHelper.Error<UmbracoModule>("Could not save the segments for the current member", ex);
}
}
}
}
}
/// <summary>
/// Checks if the xml cache file needs to be updated/persisted
/// </summary>
@@ -600,8 +676,9 @@ namespace Umbraco.Web
// used to check if the xml cache file needs to be updated/persisted
app.PostRequestHandlerExecute += (sender, e) =>
{
var httpContext = ((HttpApplication)sender).Context;
PersistXmlCache(new HttpContextWrapper(httpContext));
var httpContext = new HttpContextWrapper(((HttpApplication)sender).Context);
PersistXmlCache(httpContext);
PersistMemberSegmentData(httpContext);
};
app.EndRequest += (sender, args) =>
+4
View File
@@ -30,6 +30,7 @@ using Umbraco.Web.PropertyEditors;
using Umbraco.Web.PropertyEditors.ValueConverters;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Routing.Segments;
using Umbraco.Web.Security;
using Umbraco.Web.UI.JavaScript;
using Umbraco.Web.WebApi;
@@ -291,6 +292,9 @@ namespace Umbraco.Web
{
base.InitializeResolvers();
ContentSegmentProviderResolver.Current = new ContentSegmentProviderResolver(
PluginManager.Current.ResolveTypes<ContentSegmentProvider>());
XsltExtensionsResolver.Current = new XsltExtensionsResolver(() => PluginManager.Current.ResolveXsltExtensions());
//set the default RenderMvcController
+27 -43
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
@@ -252,7 +253,9 @@ namespace umbraco
var doctype = xmlDoc.DocumentType;
var subset = doctype.InternalSubset;
if (!subset.Contains(string.Format("<!ATTLIST {0} id ID #REQUIRED>", docTypeAlias)))
//do the normal aliases
if (subset.Contains(string.Format("<!ATTLIST {0} id ID #REQUIRED>", docTypeAlias)) == false)
{
subset = string.Format("<!ELEMENT {0} ANY>\r\n<!ATTLIST {0} id ID #REQUIRED>\r\n{1}", docTypeAlias, subset);
var xmlDoc2 = new XmlDocument();
@@ -264,7 +267,7 @@ namespace umbraco
// apply
xmlDoc = xmlDoc2;
}
return xmlDoc;
}
@@ -381,7 +384,7 @@ namespace umbraco
public static XmlDocument AppendDocumentXml(int id, int level, int parentId, XmlNode docNode, XmlDocument xmlContentCopy)
{
// Find the document in the xml cache
XmlNode currentNode = xmlContentCopy.GetElementById(id.ToString());
XmlNode currentNode = xmlContentCopy.GetElementById(id.ToString(CultureInfo.InvariantCulture));
// 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
@@ -396,14 +399,17 @@ namespace umbraco
// Find the parent (used for sortering and maybe creation of new node)
XmlNode parentNode = level == 1
? xmlContentCopy.DocumentElement
: xmlContentCopy.GetElementById(parentId.ToString());
: xmlContentCopy.GetElementById(parentId.ToString(CultureInfo.InvariantCulture));
var isVariant = docNode.AttributeValue<string>("masterDocId").IsNullOrWhiteSpace() == false;
if (parentNode != null)
{
if (currentNode == null)
{
currentNode = docNode;
parentNode.AppendChild(currentNode);
parentNode.AppendChild(currentNode);
}
else
{
@@ -412,7 +418,7 @@ namespace umbraco
//update the node with it's new values
TransferValuesFromDocumentXmlToPublishedXml(docNode, currentNode);
//If the node is being moved we also need to ensure that it exists under the new parent!
// http://issues.umbraco.org/issue/U4-2312
// we were never checking this before and instead simply changing the parentId value but not
@@ -531,25 +537,10 @@ namespace umbraco
/// Updates the document cache for multiple documents
/// </summary>
/// <param name="Documents">The documents.</param>
[Obsolete("This is not used and will be removed from the codebase in future versions")]
[Obsolete("This is not used and will be removed from the codebase in future versions", true)]
public virtual void UpdateDocumentCache(List<Document> Documents)
{
// We need to lock content cache here, because we cannot allow other threads
// making changes at the same time, they need to be queued
int parentid = Documents[0].Id;
lock (XmlContentInternalSyncLock)
{
// Make copy of memory content, we cannot make changes to the same document
// the is read from elsewhere
XmlDocument xmlContentCopy = CloneXmlDoc(XmlContentInternal);
foreach (Document d in Documents)
{
PublishNodeDo(d, xmlContentCopy, true);
}
XmlContentInternal = xmlContentCopy;
ClearContextCache();
}
//NOTE: The obsolete attribute with throw a YSOD
}
/// <summary>
@@ -559,12 +550,7 @@ namespace umbraco
[Obsolete("Method obsolete in version 4.1 and later, please use UpdateDocumentCache", true)]
public virtual void UpdateDocumentCacheAsync(int documentId)
{
//SD: WE've obsoleted this but then didn't make it call the method it should! So we've just
// left a bug behind...???? ARGH.
//.... changed now.
//ThreadPool.QueueUserWorkItem(delegate { UpdateDocumentCache(documentId); });
UpdateDocumentCache(documentId);
//NOTE: The obsolete attribute with throw a YSOD
}
/// <summary>
@@ -574,12 +560,7 @@ namespace umbraco
[Obsolete("Method obsolete in version 4.1 and later, please use ClearDocumentCache", true)]
public virtual void ClearDocumentCacheAsync(int documentId)
{
//SD: WE've obsoleted this but then didn't make it call the method it should! So we've just
// left a bug behind...???? ARGH.
//.... changed now.
//ThreadPool.QueueUserWorkItem(delegate { ClearDocumentCache(documentId); });
ClearDocumentCache(documentId);
//NOTE: The obsolete attribute with throw a YSOD
}
public virtual void ClearDocumentCache(int documentId)
@@ -607,7 +588,7 @@ namespace umbraco
doc.XmlRemoveFromDB();
// Check if node present, before cloning
x = XmlContentInternal.GetElementById(doc.Id.ToString());
x = XmlContentInternal.GetElementById(doc.Id.ToString(CultureInfo.InvariantCulture));
if (x == null)
return;
@@ -620,7 +601,7 @@ namespace umbraco
XmlDocument xmlContentCopy = CloneXmlDoc(XmlContentInternal);
// Find the document in the xml cache
x = xmlContentCopy.GetElementById(doc.Id.ToString());
x = xmlContentCopy.GetElementById(doc.Id.ToString(CultureInfo.InvariantCulture));
if (x != null)
{
// The document already exists in cache, so repopulate it
@@ -1027,7 +1008,7 @@ namespace umbraco
private static void InitContentDocument(XmlDocument xmlDoc, string dtd)
{
// Prime the xml document with an inline dtd and a root element
xmlDoc.LoadXml(String.Format("<?xml version=\"1.0\" encoding=\"utf-8\" ?>{0}{1}{0}<root id=\"-1\"/>",
xmlDoc.LoadXml(String.Format("<?xml version=\"1.0\" encoding=\"utf-8\" ?>{0}{1}{0}<root id=\"-1\" />",
Environment.NewLine,
dtd));
}
@@ -1176,6 +1157,9 @@ order by umbracoNode.level, umbracoNode.sortOrder";
return null;
}
//TODO: Here we need to deal with variants
private static void GenerateXmlDocument(IDictionary<int, List<int>> hierarchy,
IDictionary<int, XmlNode> nodeIndex, int parentId, XmlNode parentNode)
{
@@ -1183,14 +1167,14 @@ order by umbracoNode.level, umbracoNode.sortOrder";
if (hierarchy.TryGetValue(parentId, out children))
{
XmlNode childContainer = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema ||
var childContainer = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema ||
String.IsNullOrEmpty(UmbracoSettings.TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME)
? parentNode
: parentNode.SelectSingleNode(
UmbracoSettings.TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME);
if (!UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema &&
!String.IsNullOrEmpty(UmbracoSettings.TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME))
if (UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema == false &&
String.IsNullOrEmpty(UmbracoSettings.TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME) == false)
{
if (childContainer == null)
{
@@ -1203,8 +1187,8 @@ order by umbracoNode.level, umbracoNode.sortOrder";
foreach (int childId in children)
{
XmlNode childNode = nodeIndex[childId];
var childNode = nodeIndex[childId];
if (UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema ||
String.IsNullOrEmpty(UmbracoSettings.TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME))
{
@@ -1291,7 +1291,7 @@ namespace umbraco
var nav = contentNavigator.Clone();
if (nav.MoveToId(contentId))
{
var descendantIterator = nav.Select("./* [@isDoc]");
var descendantIterator = nav.Select("./* [@isDoc='']");
if (descendantIterator.MoveNext())
{
// not empty - and won't change
@@ -148,7 +148,8 @@ namespace umbraco
}
else
{
return uQuery.GetNodesByXPath(string.Concat("descendant::", documentTypeAlias, "[@isDoc]"));
//variants have isDoc='variant' so we don't want those
return uQuery.GetNodesByXPath(string.Concat("descendant::", documentTypeAlias, "[@isDoc='']"));
}
}
+2
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
@@ -14,6 +15,7 @@ using umbraco.businesslogic.Exceptions;
namespace umbraco.IO
{
[Obsolete("Use Umbraco.Core.IO.IOHelper instead")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static class IOHelper
{
public static char DirSepChar
+5 -5
View File
@@ -402,7 +402,7 @@ namespace umbraco.cms.businesslogic
public virtual void XmlPopulate(XmlDocument xd, ref XmlNode x, bool Deep)
{
var props = this.GenericProperties;
foreach (property.Property p in props)
foreach (var p in props)
if (p != null && p.Value != null && string.IsNullOrEmpty(p.Value.ToString()) == false)
x.AppendChild(p.ToXml(xd));
@@ -534,7 +534,7 @@ namespace umbraco.cms.businesslogic
SqlHelper.CreateParameter("@updateDate", versionDate));
List<PropertyType> pts = ContentType.PropertyTypes;
foreach (propertytype.PropertyType pt in pts)
foreach (var pt in pts)
{
object oldValue = "";
if (tempHasVersion)
@@ -545,7 +545,7 @@ namespace umbraco.cms.businesslogic
}
catch { }
}
property.Property p = this.addProperty(pt, newVersion);
var p = this.addProperty(pt, newVersion);
if (oldValue != null && oldValue.ToString() != "") p.Value = oldValue;
}
this.Version = newVersion;
@@ -554,8 +554,8 @@ namespace umbraco.cms.businesslogic
protected virtual XmlNode generateXmlWithoutSaving(XmlDocument xd)
{
string nodeName = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema ? "node" : Casing.SafeAliasWithForcingCheck(ContentType.Alias);
XmlNode x = xd.CreateNode(XmlNodeType.Element, nodeName, "");
var nodeName = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema ? "node" : Casing.SafeAliasWithForcingCheck(ContentType.Alias);
var x = xd.CreateNode(XmlNodeType.Element, nodeName, "");
XmlPopulate(xd, ref x, false);
return x;
}
+15 -61
View File
@@ -5,6 +5,7 @@ using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Xml;
using System.Xml.Linq;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models;
@@ -1230,7 +1231,7 @@ namespace umbraco.cms.businesslogic.web
{
XmlNode x = generateXmlWithoutSaving(xd);
// Save to db
saveXml(x);
SaveXml(x);
}
/// <summary>
@@ -1248,13 +1249,13 @@ namespace umbraco.cms.businesslogic.web
if (_xml == null)
{
// Load xml from db if _xml hasn't been loaded yet
_xml = importXml();
_xml = ImportXml();
// Generate xml if xml still null (then it hasn't been initialized before)
if (_xml == null)
{
XmlGenerate(new XmlDocument());
_xml = importXml();
_xml = ImportXml();
}
}
@@ -1284,57 +1285,10 @@ namespace umbraco.cms.businesslogic.web
/// <param name="Deep">If true the documents childrens xmlrepresentation will be appended to the Xmlnode recursive</param>
public override void XmlPopulate(XmlDocument xd, ref XmlNode x, bool Deep)
{
string urlName = this.Content.GetUrlSegment().ToLower();
foreach (Property p in GenericProperties.Where(p => p != null && p.Value != null && string.IsNullOrEmpty(p.Value.ToString()) == false))
x.AppendChild(p.ToXml(xd));
var resultXml = ApplicationContext.Current.Services.PackagingService.Export(Content, Deep);
// attributes
x.Attributes.Append(addAttribute(xd, "id", Id.ToString()));
// x.Attributes.Append(addAttribute(xd, "version", Version.ToString()));
if (Level > 1)
x.Attributes.Append(addAttribute(xd, "parentID", Parent.Id.ToString()));
else
x.Attributes.Append(addAttribute(xd, "parentID", "-1"));
x.Attributes.Append(addAttribute(xd, "level", Level.ToString()));
x.Attributes.Append(addAttribute(xd, "writerID", Writer.Id.ToString()));
x.Attributes.Append(addAttribute(xd, "creatorID", Creator.Id.ToString()));
if (ContentType != null)
x.Attributes.Append(addAttribute(xd, "nodeType", ContentType.Id.ToString()));
x.Attributes.Append(addAttribute(xd, "template", _template.ToString()));
x.Attributes.Append(addAttribute(xd, "sortOrder", sortOrder.ToString()));
x.Attributes.Append(addAttribute(xd, "createDate", CreateDateTime.ToString("s")));
x.Attributes.Append(addAttribute(xd, "updateDate", VersionDate.ToString("s")));
x.Attributes.Append(addAttribute(xd, "nodeName", Text));
x.Attributes.Append(addAttribute(xd, "urlName", urlName));
x.Attributes.Append(addAttribute(xd, "writerName", Writer.Name));
x.Attributes.Append(addAttribute(xd, "creatorName", Creator.Name.ToString()));
if (ContentType != null && UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema)
x.Attributes.Append(addAttribute(xd, "nodeTypeAlias", ContentType.Alias));
x.Attributes.Append(addAttribute(xd, "path", Path));
if (!UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema)
{
x.Attributes.Append(addAttribute(xd, "isDoc", ""));
}
if (Deep)
{
//store children array here because iterating over an Array object is very inneficient.
var c = Children;
foreach (Document d in c)
{
XmlNode xml = d.ToXml(xd, true);
if (xml != null)
{
x.AppendChild(xml);
}
else
{
LogHelper.Debug<Document>(string.Format("Document {0} not published so XML cannot be generated", d.Id));
}
}
}
//convert it from the result
x = resultXml.ToXmlNode(xd);
}
/// <summary>
@@ -1347,19 +1301,19 @@ namespace umbraco.cms.businesslogic.web
{
if (_xml == null)
// Load xml from db if _xml hasn't been loaded yet
_xml = importXml();
_xml = ImportXml();
// Generate xml if xml still null (then it hasn't been initialized before)
if (_xml == null)
{
XmlGenerate(new XmlDocument());
_xml = importXml();
_xml = ImportXml();
}
else
{
// Update the sort order attr
_xml.Attributes.GetNamedItem("sortOrder").Value = sortOrder.ToString();
saveXml(_xml);
SaveXml(_xml);
}
}
@@ -1529,10 +1483,10 @@ namespace umbraco.cms.businesslogic.web
VersionDate = versionDate;
}
private XmlAttribute addAttribute(XmlDocument Xd, string Name, string Value)
private XmlAttribute AddAttribute(XmlDocument xd, string name, string value)
{
XmlAttribute temp = Xd.CreateAttribute(Name);
temp.Value = Value;
XmlAttribute temp = xd.CreateAttribute(name);
temp.Value = value;
return temp;
}
@@ -1541,7 +1495,7 @@ namespace umbraco.cms.businesslogic.web
/// </summary>
/// <param name="x"></param>
[MethodImpl(MethodImplOptions.Synchronized)]
private void saveXml(XmlNode x)
private void SaveXml(XmlNode x)
{
bool exists = (SqlHelper.ExecuteScalar<int>("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId=@nodeId",
SqlHelper.CreateParameter("@nodeId", Id)) != 0);
@@ -1552,7 +1506,7 @@ namespace umbraco.cms.businesslogic.web
SqlHelper.CreateParameter("@xml", x.OuterXml));
}
private XmlNode importXml()
private XmlNode ImportXml()
{
XmlDocument xmlDoc = new XmlDocument();
XmlReader xmlRdr = SqlHelper.ExecuteXmlReader(string.Format(
@@ -108,6 +108,7 @@ namespace umbraco.cms.businesslogic.web
{
strictSchemaBuilder.AppendLine(String.Format("<!ELEMENT {0} ANY>", safeAlias));
strictSchemaBuilder.AppendLine(String.Format("<!ATTLIST {0} id ID #REQUIRED>", safeAlias));
}
}