using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Exceptions; namespace Umbraco.Core.Models { /// /// Represents a Content object /// [Serializable] [DataContract(IsReference = true)] public class Content : ContentBase, IContent { private IContentType _contentType; private ITemplate _template; private ContentScheduleCollection _schedule; private bool _published; private PublishedState _publishedState; private ContentCultureInfosCollection _publishInfos; private ContentCultureInfosCollection _publishInfosOrig; private HashSet _editedCultures; private static readonly Lazy Ps = new Lazy(); /// /// Constructor for creating a Content object /// /// Name of the content /// Parent object /// ContentType for the current Content object /// An optional culture. public Content(string name, IContent parent, IContentType contentType, string culture = null) : this(name, parent, contentType, new PropertyCollection(), culture) { } /// /// Constructor for creating a Content object /// /// Name of the content /// Parent object /// ContentType for the current Content object /// Collection of properties /// An optional culture. public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties, string culture = null) : base(name, parent, contentType, properties, culture) { _contentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); _publishedState = PublishedState.Unpublished; PublishedVersionId = 0; } /// /// Constructor for creating a Content object /// /// Name of the content /// Id of the Parent content /// ContentType for the current Content object /// An optional culture. public Content(string name, int parentId, IContentType contentType, string culture = null) : this(name, parentId, contentType, new PropertyCollection(), culture) { } /// /// Constructor for creating a Content object /// /// Name of the content /// Id of the Parent content /// ContentType for the current Content object /// Collection of properties /// An optional culture. public Content(string name, int parentId, IContentType contentType, PropertyCollection properties, string culture = null) : base(name, parentId, contentType, properties, culture) { _contentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); _publishedState = PublishedState.Unpublished; PublishedVersionId = 0; } // ReSharper disable once ClassNeverInstantiated.Local private class PropertySelectors { public readonly PropertyInfo TemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.Template); public readonly PropertyInfo PublishedSelector = ExpressionHelper.GetPropertyInfo(x => x.Published); public readonly PropertyInfo ContentScheduleSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentSchedule); public readonly PropertyInfo PublishCultureInfosSelector = ExpressionHelper.GetPropertyInfo>(x => x.PublishCultureInfos); } /// [DoNotClone] public ContentScheduleCollection ContentSchedule { get { if (_schedule == null) { _schedule = new ContentScheduleCollection(); _schedule.CollectionChanged += ScheduleCollectionChanged; } return _schedule; } set { if(_schedule != null) _schedule.CollectionChanged -= ScheduleCollectionChanged; SetPropertyValueAndDetectChanges(value, ref _schedule, Ps.Value.ContentScheduleSelector); if (_schedule != null) _schedule.CollectionChanged += ScheduleCollectionChanged; } } /// /// Collection changed event handler to ensure the schedule field is set to dirty when the schedule changes /// /// /// private void ScheduleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(Ps.Value.ContentScheduleSelector); } /// /// Gets or sets the template used by the Content. /// This is used to override the default one from the ContentType. /// /// /// If no template is explicitly set on the Content object, /// the Default template from the ContentType will be returned. /// [DataMember] public ITemplate Template { get => _template ?? _contentType.DefaultTemplate; set => SetPropertyValueAndDetectChanges(value, ref _template, Ps.Value.TemplateSelector); } /// /// Gets or sets a value indicating whether this content item is published or not. /// [DataMember] public bool Published { get => _published; // the setter is internal and should only be invoked from // - the ContentFactory when creating a content entity from a dto // - the ContentRepository when updating a content entity internal set { SetPropertyValueAndDetectChanges(value, ref _published, Ps.Value.PublishedSelector); _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; } } /// /// Gets the published state of the content item. /// /// The state should be Published or Unpublished, depending on whether Published /// is true or false, but can also temporarily be Publishing or Unpublishing when the /// content item is about to be saved. [DataMember] public PublishedState PublishedState { get => _publishedState; set { if (value != PublishedState.Publishing && value != PublishedState.Unpublishing) throw new ArgumentException("Invalid state, only Publishing and Unpublishing are accepted."); _publishedState = value; } } [IgnoreDataMember] public bool Edited { get; internal set; } /// /// Gets the ContentType used by this content object /// [IgnoreDataMember] public IContentType ContentType => _contentType; /// [IgnoreDataMember] public DateTime? PublishDate { get; internal set; } // set by persistence /// [IgnoreDataMember] public int? PublisherId { get; internal set; } // set by persistence /// [IgnoreDataMember] public ITemplate PublishTemplate { get; internal set; } // set by persistence /// [IgnoreDataMember] public string PublishName { get; internal set; } // set by persistence /// [IgnoreDataMember] public IEnumerable EditedCultures => CultureInfos.Keys.Where(IsCultureEdited); /// [IgnoreDataMember] public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); /// public bool IsCulturePublished(string culture) // just check _publishInfos // a non-available culture could not become published anyways => _publishInfos != null && _publishInfos.ContainsKey(culture); /// public bool WasCulturePublished(string culture) // just check _publishInfosOrig - a copy of _publishInfos // a non-available culture could not become published anyways => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); // adjust dates to sync between version, cultures etc // used by the repo when persisting internal void AdjustDates(DateTime date) { foreach (var culture in PublishedCultures.ToList()) { if (_publishInfos == null || !_publishInfos.TryGetValue(culture, out var publishInfos)) continue; if (_publishInfosOrig != null && _publishInfosOrig.TryGetValue(culture, out var publishInfosOrig) && publishInfosOrig.Date == publishInfos.Date) continue; _publishInfos.AddOrUpdate(culture, publishInfos.Name, date); if (CultureInfos.TryGetValue(culture, out var infos)) SetCultureInfo(culture, infos.Name, date); } } /// public bool IsCultureEdited(string culture) => IsCultureAvailable(culture) && // is available, and (!IsCulturePublished(culture) || // is not published, or (_editedCultures != null && _editedCultures.Contains(culture))); // is edited /// [IgnoreDataMember] public IReadOnlyDictionary PublishCultureInfos => _publishInfos ?? NoInfos; /// public string GetPublishName(string culture) { if (culture.IsNullOrWhiteSpace()) return PublishName; if (!ContentTypeBase.VariesByCulture()) return null; if (_publishInfos == null) return null; return _publishInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } /// public DateTime? GetPublishDate(string culture) { if (culture.IsNullOrWhiteSpace()) return PublishDate; if (!ContentTypeBase.VariesByCulture()) return null; if (_publishInfos == null) return null; return _publishInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; } // internal for repository internal void SetPublishInfo(string culture, string name, DateTime date) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); if (_publishInfos == null) { _publishInfos = new ContentCultureInfosCollection(); _publishInfos.CollectionChanged += PublishNamesCollectionChanged; } _publishInfos.AddOrUpdate(culture, name, date); } private void ClearPublishInfos() { _publishInfos = null; } private void ClearPublishInfo(string culture) { if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); if (_publishInfos == null) return; _publishInfos.Remove(culture); if (_publishInfos.Count == 0) _publishInfos = null; // set the culture to be dirty - it's been modified TouchCultureInfo(culture); } // sets a publish edited internal void SetCultureEdited(string culture) { if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); if (_editedCultures == null) _editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); _editedCultures.Add(culture.ToLowerInvariant()); } // sets all publish edited internal void SetCultureEdited(IEnumerable cultures) { if (cultures == null) { _editedCultures = null; } else { var editedCultures = new HashSet(cultures.Where(x => !x.IsNullOrWhiteSpace()), StringComparer.OrdinalIgnoreCase); _editedCultures = editedCultures.Count > 0 ? editedCultures : null; } } /// /// Handles culture infos collection changes. /// private void PublishNamesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(Ps.Value.PublishCultureInfosSelector); } [IgnoreDataMember] public int PublishedVersionId { get; internal set; } [DataMember] public bool Blueprint { get; internal set; } /// public bool PublishCulture(string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); // the variation should be supported by the content type properties // if the content type is invariant, only '*' and 'null' is ok // if the content type varies, everything is ok because some properties may be invariant if (!ContentType.SupportsPropertyVariation(culture, "*", true)) throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\"."); // the values we want to publish should be valid if (ValidateProperties(culture).Any()) return false; var alsoInvariant = false; if (culture == "*") // all cultures { foreach (var c in AvailableCultures) { var name = GetCultureName(c); if (string.IsNullOrWhiteSpace(name)) return false; SetPublishInfo(c, name, DateTime.Now); } } else if (culture == null) // invariant culture { if (string.IsNullOrWhiteSpace(Name)) return false; // PublishName set by repository - nothing to do here } else // one single culture { var name = GetCultureName(culture); if (string.IsNullOrWhiteSpace(name)) return false; SetPublishInfo(culture, name, DateTime.Now); alsoInvariant = true; // we also want to publish invariant values } // property.PublishValues only publishes what is valid, variation-wise foreach (var property in Properties) { property.PublishValues(culture); if (alsoInvariant) property.PublishValues(null); } _publishedState = PublishedState.Publishing; return true; } /// public void UnpublishCulture(string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); // the variation should be supported by the content type properties if (!ContentType.SupportsPropertyVariation(culture, "*", true)) throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\"."); if (culture == "*") // all cultures ClearPublishInfos(); else // one single culture ClearPublishInfo(culture); // property.PublishValues only publishes what is valid, variation-wise foreach (var property in Properties) property.UnpublishValues(culture); _publishedState = PublishedState.Publishing; } /// /// Changes the for the current content object /// /// New ContentType for this content /// Leaves PropertyTypes intact after change public void ChangeContentType(IContentType contentType) { ContentTypeId = contentType.Id; _contentType = contentType; ContentTypeBase = contentType; Properties.EnsurePropertyTypes(PropertyTypes); Properties.CollectionChanged -= PropertiesChanged; // be sure not to double add Properties.CollectionChanged += PropertiesChanged; } /// /// Changes the for the current content object and removes PropertyTypes, /// which are not part of the new ContentType. /// /// New ContentType for this content /// Boolean indicating whether to clear PropertyTypes upon change public void ChangeContentType(IContentType contentType, bool clearProperties) { if(clearProperties) { ContentTypeId = contentType.Id; _contentType = contentType; ContentTypeBase = contentType; Properties.EnsureCleanPropertyTypes(PropertyTypes); Properties.CollectionChanged -= PropertiesChanged; // be sure not to double add Properties.CollectionChanged += PropertiesChanged; return; } ChangeContentType(contentType); } public override void ResetDirtyProperties(bool rememberDirty) { base.ResetDirtyProperties(rememberDirty); if (Template != null) Template.ResetDirtyProperties(rememberDirty); if (ContentType != null) ContentType.ResetDirtyProperties(rememberDirty); // take care of the published state _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; // Make a copy of the _publishInfos, this is purely so that we can detect // if this entity's previous culture publish state (regardless of the rememberDirty flag) _publishInfosOrig = _publishInfos == null ? null : new ContentCultureInfosCollection(_publishInfos); if (_publishInfos == null) return; foreach (var infos in _publishInfos) infos.ResetDirtyProperties(rememberDirty); } /// /// Creates a deep clone of the current entity with its identity and it's property identities reset /// /// public IContent DeepCloneWithResetIdentities() { var clone = (Content)DeepClone(); clone.Key = Guid.Empty; clone.VersionId = clone.PublishedVersionId = 0; clone.ResetIdentity(); foreach (var property in clone.Properties) property.ResetIdentity(); return clone; } protected override void PerformDeepClone(object clone) { base.PerformDeepClone(clone); var clonedContent = (Content)clone; //need to manually clone this since it's not settable clonedContent._contentType = (IContentType) ContentType.DeepClone(); //if culture infos exist then deal with event bindings if (clonedContent._publishInfos != null) { clonedContent._publishInfos.CollectionChanged -= PublishNamesCollectionChanged; //clear this event handler if any clonedContent._publishInfos = (ContentCultureInfosCollection) _publishInfos.DeepClone(); //manually deep clone clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; //re-assign correct event handler } //if properties exist then deal with event bindings if (clonedContent._schedule != null) { clonedContent._schedule.CollectionChanged -= ScheduleCollectionChanged; //clear this event handler if any clonedContent._schedule = (ContentScheduleCollection)_schedule.DeepClone(); //manually deep clone clonedContent._schedule.CollectionChanged += clonedContent.ScheduleCollectionChanged; //re-assign correct event handler } } } }