using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Web; using Umbraco.Core.Exceptions; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { /// /// Represents an abstract class for base Content properties and methods /// [Serializable] [DataContract(IsReference = true)] [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentTypeBase.Alias}")] public abstract class ContentBase : TreeEntityBase, IContentBase { protected static readonly Dictionary NoNames = new Dictionary(); private static readonly Lazy Ps = new Lazy(); private int _contentTypeId; protected IContentTypeComposition ContentTypeBase; private int _writerId; private PropertyCollection _properties; private Dictionary _cultureInfos; /// /// Initializes a new instance of the class. /// protected ContentBase(string name, int parentId, IContentTypeComposition contentType, PropertyCollection properties, string culture = null) : this(name, contentType, properties, culture) { if (parentId == 0) throw new ArgumentOutOfRangeException(nameof(parentId)); ParentId = parentId; } /// /// Initializes a new instance of the class. /// protected ContentBase(string name, IContentBase parent, IContentTypeComposition contentType, PropertyCollection properties, string culture = null) : this(name, contentType, properties, culture) { if (parent == null) throw new ArgumentNullException(nameof(parent)); SetParent(parent); } private ContentBase(string name, IContentTypeComposition contentType, PropertyCollection properties, string culture = null) { ContentTypeBase = contentType ?? throw new ArgumentNullException(nameof(contentType)); // initially, all new instances have Id = 0; // no identity VersionId = 0; // no versions SetCultureName(name, culture); _contentTypeId = contentType.Id; _properties = properties ?? throw new ArgumentNullException(nameof(properties)); _properties.EnsurePropertyTypes(PropertyTypes); } // ReSharper disable once ClassNeverInstantiated.Local private class PropertySelectors { public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); public readonly PropertyInfo NamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.CultureNames); } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(Ps.Value.PropertyCollectionSelector); } /// /// Id of the user who wrote/updated this entity /// [DataMember] public virtual int WriterId { get => _writerId; set => SetPropertyValueAndDetectChanges(value, ref _writerId, Ps.Value.WriterSelector); } [IgnoreDataMember] public int VersionId { get; internal set; } /// /// Integer Id of the default ContentType /// [DataMember] public virtual int ContentTypeId { get { //There will be cases where this has not been updated to reflect the true content type ID. //This will occur when inserting new content. if (_contentTypeId == 0 && ContentTypeBase != null && ContentTypeBase.HasIdentity) { _contentTypeId = ContentTypeBase.Id; } return _contentTypeId; } protected set => SetPropertyValueAndDetectChanges(value, ref _contentTypeId, Ps.Value.DefaultContentTypeIdSelector); } /// /// Gets or sets the collection of properties for the entity. /// [DataMember] public virtual PropertyCollection Properties { get => _properties; set { _properties = value; _properties.CollectionChanged += PropertiesChanged; } } /// /// Gets the enumeration of property groups for the entity. /// fixme is a proxy, kill this /// [IgnoreDataMember] public IEnumerable PropertyGroups => ContentTypeBase.CompositionPropertyGroups; /// /// Gets the numeration of property types for the entity. /// fixme is a proxy, kill this /// [IgnoreDataMember] public IEnumerable PropertyTypes => ContentTypeBase.CompositionPropertyTypes; #region Cultures // notes - common rules // - setting a variant value on an invariant content type throws // - getting a variant value on an invariant content type returns null // - setting and getting the invariant value is always possible // - setting a null value clears the value /// public IEnumerable AvailableCultures => _cultureInfos?.Select(x => x.Key) ?? Enumerable.Empty(); /// public bool IsCultureAvailable(string culture) => _cultureInfos != null && _cultureInfos.ContainsKey(culture); /// [DataMember] public virtual IReadOnlyDictionary CultureNames => _cultureInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; /// public virtual string GetCultureName(string culture) { if (culture.IsNullOrWhiteSpace()) return Name; if (!ContentTypeBase.VariesByCulture()) return null; if (_cultureInfos == null) return null; return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } /// public DateTime? GetCultureDate(string culture) { if (culture.IsNullOrWhiteSpace()) return null; if (!ContentTypeBase.VariesByCulture()) return null; if (_cultureInfos == null) return null; return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; } /// public virtual void SetCultureName(string name, string culture) { if (ContentTypeBase.VariesByCulture()) // set on variant content type { if (culture.IsNullOrWhiteSpace()) // invariant is ok { Name = name; // may be null } else if (name.IsNullOrWhiteSpace()) // clear { ClearCultureInfo(culture); } else // set { SetCultureInfo(culture, name, DateTime.Now); } } else // set on invariant content type { if (!culture.IsNullOrWhiteSpace()) // invariant is NOT ok throw new NotSupportedException("Content type does not vary by culture."); Name = name; // may be null } } protected void ClearCultureInfos() { _cultureInfos = null; OnPropertyChanged(Ps.Value.NamesSelector); } protected void ClearCultureInfo(string culture) { if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); if (_cultureInfos == null) return; _cultureInfos.Remove(culture); if (_cultureInfos.Count == 0) _cultureInfos = null; OnPropertyChanged(Ps.Value.NamesSelector); } // internal for repository internal void SetCultureInfo(string culture, string name, DateTime date) { if (name.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(name)); if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); if (_cultureInfos == null) _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); _cultureInfos[culture.ToLowerInvariant()] = (name, date); OnPropertyChanged(Ps.Value.NamesSelector); } #endregion #region Has, Get, Set, Publish Property Value /// public virtual bool HasProperty(string propertyTypeAlias) => Properties.Contains(propertyTypeAlias); /// public virtual object GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false) { return Properties.TryGetValue(propertyTypeAlias, out var property) ? property.GetValue(culture, segment, published) : null; } /// public virtual TValue GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false) { if (!Properties.TryGetValue(propertyTypeAlias, out var property)) return default; var convertAttempt = property.GetValue(culture, segment, published).TryConvertTo(); return convertAttempt.Success ? convertAttempt.Result : default; } /// public virtual void SetValue(string propertyTypeAlias, object value, string culture = null, string segment = null) { if (Properties.Contains(propertyTypeAlias)) { Properties[propertyTypeAlias].SetValue(value, culture, segment); return; } var propertyType = PropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (propertyType == null) throw new InvalidOperationException($"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); var property = propertyType.CreateProperty(); property.SetValue(value, culture, segment); Properties.Add(property); } // HttpPostedFileBase is the base class that can be mocked // HttpPostedFile is what we get in ASP.NET // HttpPostedFileWrapper wraps sealed HttpPostedFile as HttpPostedFileBase /// /// Sets the posted file value of a property. /// public virtual void SetValue(string propertyTypeAlias, HttpPostedFile value, string culture = null, string segment = null) { ContentExtensions.SetValue(this, propertyTypeAlias, new HttpPostedFileWrapper(value), culture, segment); } /// /// Sets the posted file value of a property. /// public virtual void SetValue(string propertyTypeAlias, HttpPostedFileBase value, string culture = null, string segment = null) { ContentExtensions.SetValue(this, propertyTypeAlias, value, culture, segment); } #endregion #region Validation /// public bool IsValid(string culture = null, string segment = null) { var name = GetCultureName(culture); if (name.IsNullOrWhiteSpace()) return false; return ValidateProperties(culture, segment).Length == 0; } /// public virtual Property[] ValidateProperties(string culture = null, string segment = null) { return Properties.Where(x => // select properties... x.PropertyType.SupportsVariation(culture, segment, true) && // that support the variation !x.IsValid(culture, segment)) // and are not valid .ToArray(); } #endregion #region Dirty /// /// Overriden to include user properties. public override void ResetDirtyProperties(bool rememberDirty) { base.ResetDirtyProperties(rememberDirty); // also reset dirty changes made to user's properties foreach (var prop in Properties) prop.ResetDirtyProperties(rememberDirty); } /// /// Overriden to include user properties. public override bool IsDirty() { return IsEntityDirty() || this.IsAnyUserPropertyDirty(); } /// /// Overriden to include user properties. public override bool WasDirty() { return WasEntityDirty() || this.WasAnyUserPropertyDirty(); } /// /// Gets a value indicating whether the current entity's own properties (not user) are dirty. /// public bool IsEntityDirty() { return base.IsDirty(); } /// /// Gets a value indicating whether the current entity's own properties (not user) were dirty. /// public bool WasEntityDirty() { return base.WasDirty(); } /// /// Overriden to include user properties. public override bool IsPropertyDirty(string propertyName) { if (base.IsPropertyDirty(propertyName)) return true; return Properties.Contains(propertyName) && Properties[propertyName].IsDirty(); } /// /// Overriden to include user properties. public override bool WasPropertyDirty(string propertyName) { if (base.WasPropertyDirty(propertyName)) return true; return Properties.Contains(propertyName) && Properties[propertyName].WasDirty(); } /// /// Overriden to include user properties. public override IEnumerable GetDirtyProperties() { var instanceProperties = base.GetDirtyProperties(); var propertyTypes = Properties.Where(x => x.IsDirty()).Select(x => x.Alias); return instanceProperties.Concat(propertyTypes); } /// /// Overriden to include user properties. public override IEnumerable GetWereDirtyProperties() { var instanceProperties = base.GetWereDirtyProperties(); var propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias); return instanceProperties.Concat(propertyTypes); } #endregion } }