diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 51a39e15df..2bab497b2b 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -139,6 +139,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings internal CommaDelimitedConfigurationElement DisallowedUploadFiles { get { return GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd"}); } + } + + [ConfigurationProperty("allowedUploadFiles")] + internal CommaDelimitedConfigurationElement AllowedUploadFiles + { + get { return GetOptionalDelimitedElement("allowedUploadFiles", new string[0]); } } [ConfigurationProperty("cloneXmlContent")] @@ -307,6 +313,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings IEnumerable IContentSection.DisallowedUploadFiles { get { return DisallowedUploadFiles; } + } + + IEnumerable IContentSection.AllowedUploadFiles + { + get { return AllowedUploadFiles; } } bool IContentSection.CloneXmlContent diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentSectionExtensions.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentSectionExtensions.cs new file mode 100644 index 0000000000..a4f182b373 --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentSectionExtensions.cs @@ -0,0 +1,19 @@ +using System.Linq; + +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + public static class ContentSectionExtensions + { + /// + /// Determines if file extension is allowed for upload based on (optional) white list and black list + /// held in settings. + /// Allow upload if extension is whitelisted OR if there is no whitelist and extension is NOT blacklisted. + /// + public static bool IsFileAllowedForUpload(this IContentSection contentSection, string extension) + { + return contentSection.AllowedUploadFiles.Any(x => x.InvariantEquals(extension)) || + (contentSection.AllowedUploadFiles.Any() == false && + contentSection.DisallowedUploadFiles.Any(x => x.InvariantEquals(extension)) == false); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index d73b3b9e41..7e874c9582 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -52,7 +52,9 @@ namespace Umbraco.Core.Configuration.UmbracoSettings MacroErrorBehaviour MacroErrorBehaviour { get; } - IEnumerable DisallowedUploadFiles { get; } + IEnumerable DisallowedUploadFiles { get; } + + IEnumerable AllowedUploadFiles { get; } bool CloneXmlContent { get; } diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index eba5cef23d..1f3986eeaf 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -437,6 +437,11 @@ namespace Umbraco.Core /// public const string EmailAddressAlias = "Umbraco.EmailAddress"; + /// + /// Alias for the nested content property editor. + /// + public const string NestedContentAlias = "Umbraco.NestedContent"; + public static class PreValueKeys { /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs index 56745ece66..209dbcc521 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs @@ -28,7 +28,7 @@ namespace Umbraco.Core.Models.PublishedContent if (_index.HasValue) return _index.Value; // slow -- and don't cache, not in a set - if (_contentSet == null) return Content.GetIndex(); + if (_contentSet == null) return WrappedContentInternal.GetIndex(); // slow -- but cache for next time var index = _contentSet.FindIndex(x => x.Id == Id); @@ -147,7 +147,7 @@ namespace Umbraco.Core.Models.PublishedContent public override IEnumerable ContentSet { - get { return _contentSet ?? Content.ContentSet; } + get { return _contentSet ?? WrappedContentInternal.ContentSet; } } #endregion @@ -161,8 +161,8 @@ namespace Umbraco.Core.Models.PublishedContent get { return _properties == null - ? Content.Properties - : Content.Properties.Union(_properties).ToList(); + ? WrappedContentInternal.Properties + : WrappedContentInternal.Properties.Union(_properties).ToList(); } } @@ -175,15 +175,15 @@ namespace Umbraco.Core.Models.PublishedContent var property = _properties.FirstOrDefault(prop => prop.PropertyTypeAlias.InvariantEquals(alias)); if (property != null) return property.HasValue ? property.Value : null; } - return Content[alias]; + return WrappedContentInternal[alias]; } } public override IPublishedProperty GetProperty(string alias) { return _properties == null - ? Content.GetProperty(alias) - : _properties.FirstOrDefault(prop => prop.PropertyTypeAlias.InvariantEquals(alias)) ?? Content.GetProperty(alias); + ? WrappedContentInternal.GetProperty(alias) + : _properties.FirstOrDefault(prop => prop.PropertyTypeAlias.InvariantEquals(alias)) ?? WrappedContentInternal.GetProperty(alias); } #endregion diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyExtended.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyExtended.cs index 492fd79796..4386c5daaf 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyExtended.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyExtended.cs @@ -9,6 +9,6 @@ namespace Umbraco.Core.Models.PublishedContent : base(content) { } - public Guid Key { get { return ((IPublishedContentWithKey) Content).Key; } } + public Guid Key { get { return ((IPublishedContentWithKey) WrappedContentInternal).Key; } } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyModel.cs index 4761a52617..1ee7c13daf 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyModel.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyModel.cs @@ -8,6 +8,6 @@ namespace Umbraco.Core.Models.PublishedContent : base (content) { } - public Guid Key { get { return ((IPublishedContentWithKey) Content).Key; } } + public Guid Key { get { return ((IPublishedContentWithKey) WrappedContentInternal).Key; } } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyWrapped.cs index 35d7dd6f1f..45d28a0086 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWithKeyWrapped.cs @@ -12,6 +12,6 @@ namespace Umbraco.Core.Models.PublishedContent : base(content) { } - public virtual Guid Key { get { return ((IPublishedContentWithKey) Content).Key; } } + public virtual Guid Key { get { return ((IPublishedContentWithKey) WrappedContentInternal).Key; } } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index 2767dc6b8b..d5793c3b4d 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -27,7 +27,7 @@ namespace Umbraco.Core.Models.PublishedContent /// public abstract class PublishedContentWrapped : IPublishedContent { - protected readonly IPublishedContent Content; + protected readonly IPublishedContent WrappedContentInternal; /// /// Initialize a new instance of the class @@ -36,7 +36,7 @@ namespace Umbraco.Core.Models.PublishedContent /// The content to wrap and extend. protected PublishedContentWrapped(IPublishedContent content) { - Content = content; + WrappedContentInternal = content; } /// @@ -45,21 +45,21 @@ namespace Umbraco.Core.Models.PublishedContent /// The wrapped content, that was passed as an argument to the constructor. public IPublishedContent Unwrap() { - return Content; + return WrappedContentInternal; } #region ContentSet public virtual IEnumerable ContentSet { - get { return Content.ContentSet; } + get { return WrappedContentInternal.ContentSet; } } #endregion #region ContentType - public virtual PublishedContentType ContentType { get { return Content.ContentType; } } + public virtual PublishedContentType ContentType { get { return WrappedContentInternal.ContentType; } } #endregion @@ -67,102 +67,102 @@ namespace Umbraco.Core.Models.PublishedContent public virtual int Id { - get { return Content.Id; } + get { return WrappedContentInternal.Id; } } public virtual int TemplateId { - get { return Content.TemplateId; } + get { return WrappedContentInternal.TemplateId; } } public virtual int SortOrder { - get { return Content.SortOrder; } + get { return WrappedContentInternal.SortOrder; } } public virtual string Name { - get { return Content.Name; } + get { return WrappedContentInternal.Name; } } public virtual string UrlName { - get { return Content.UrlName; } + get { return WrappedContentInternal.UrlName; } } public virtual string DocumentTypeAlias { - get { return Content.DocumentTypeAlias; } + get { return WrappedContentInternal.DocumentTypeAlias; } } public virtual int DocumentTypeId { - get { return Content.DocumentTypeId; } + get { return WrappedContentInternal.DocumentTypeId; } } public virtual string WriterName { - get { return Content.WriterName; } + get { return WrappedContentInternal.WriterName; } } public virtual string CreatorName { - get { return Content.CreatorName; } + get { return WrappedContentInternal.CreatorName; } } public virtual int WriterId { - get { return Content.WriterId; } + get { return WrappedContentInternal.WriterId; } } public virtual int CreatorId { - get { return Content.CreatorId; } + get { return WrappedContentInternal.CreatorId; } } public virtual string Path { - get { return Content.Path; } + get { return WrappedContentInternal.Path; } } public virtual DateTime CreateDate { - get { return Content.CreateDate; } + get { return WrappedContentInternal.CreateDate; } } public virtual DateTime UpdateDate { - get { return Content.UpdateDate; } + get { return WrappedContentInternal.UpdateDate; } } public virtual Guid Version { - get { return Content.Version; } + get { return WrappedContentInternal.Version; } } public virtual int Level { - get { return Content.Level; } + get { return WrappedContentInternal.Level; } } public virtual string Url { - get { return Content.Url; } + get { return WrappedContentInternal.Url; } } public virtual PublishedItemType ItemType { - get { return Content.ItemType; } + get { return WrappedContentInternal.ItemType; } } public virtual bool IsDraft { - get { return Content.IsDraft; } + get { return WrappedContentInternal.IsDraft; } } public virtual int GetIndex() { - return Content.GetIndex(); + return WrappedContentInternal.GetIndex(); } #endregion @@ -171,12 +171,12 @@ namespace Umbraco.Core.Models.PublishedContent public virtual IPublishedContent Parent { - get { return Content.Parent; } + get { return WrappedContentInternal.Parent; } } public virtual IEnumerable Children { - get { return Content.Children; } + get { return WrappedContentInternal.Children; } } #endregion @@ -185,22 +185,22 @@ namespace Umbraco.Core.Models.PublishedContent public virtual ICollection Properties { - get { return Content.Properties; } + get { return WrappedContentInternal.Properties; } } public virtual object this[string alias] { - get { return Content[alias]; } + get { return WrappedContentInternal[alias]; } } public virtual IPublishedProperty GetProperty(string alias) { - return Content.GetProperty(alias); + return WrappedContentInternal.GetProperty(alias); } public virtual IPublishedProperty GetProperty(string alias, bool recurse) { - return Content.GetProperty(alias, recurse); + return WrappedContentInternal.GetProperty(alias, recurse); } #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs index 4865a77ab8..b8141711d7 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddRelationTypeForDocumentOnDelete.cs @@ -2,6 +2,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero @@ -16,7 +17,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe public override void Up() { - var exists = Context.Database.FirstOrDefault("WHERE alias=@alias", new {alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias}); + var exists = Context.Database.FirstOrDefault("WHERE alias=@alias", new {alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias}); if (exists == null) { Insert.IntoTable("umbracoRelationType").Row(new @@ -28,13 +29,42 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias }); } - - - } public override void Down() + { } + + // need to capture the DTO as it is modified in later migrations + + [TableName("umbracoRelationType")] + [PrimaryKey("id")] + [ExplicitColumns] + internal class RelationTypeDtoCapture { + public const int NodeIdSeed = 3; + + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] + public int Id { get; set; } + + [Column("dual")] + public bool Dual { get; set; } + + [Column("parentObjectType")] + public Guid ParentObjectType { get; set; } + + [Column("childObjectType")] + public Guid ChildObjectType { get; set; } + + [Column("name")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] + public string Name { get; set; } + + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(100)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] + public string Alias { get; set; } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs index 5c0227247e..ba1df58879 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs @@ -42,7 +42,16 @@ namespace Umbraco.Core.PropertyEditors internal PropertyEditorResolver(IServiceProvider serviceProvider, ILogger logger, Func> typeListProducerList, ManifestBuilder builder) : base(serviceProvider, logger, typeListProducerList, ObjectLifetimeScope.Application) { - _unioned = new Lazy>(() => Values.Union(builder.PropertyEditors).ToList()); + _unioned = new Lazy>(() => SanitizeNames(Values.Union(builder.PropertyEditors).ToList())); + } + + private static List SanitizeNames(List editors) + { + var nestedContentEditorFromPackage = editors.FirstOrDefault(x => x.Alias == "Our.Umbraco.NestedContent"); + if (nestedContentEditorFromPackage != null) + nestedContentEditorFromPackage.Name = "(Obsolete) " + nestedContentEditorFromPackage.Name; + return editors; + } private readonly Lazy> _unioned; diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs index c2dfd687dd..ef089a3c22 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs @@ -83,7 +83,7 @@ namespace Umbraco.Core.Services /// public bool HasContainerInPath(string contentPath) { - using (var uow = UowProvider.GetUnitOfWork()) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { // can use same repo for both content and media var repository = RepositoryFactory.CreateContentTypeRepository(uow); diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index ea926db5b9..ca014c5d8e 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -95,6 +95,16 @@ namespace Umbraco.Core.Services return result.HasValue ? Attempt.Succeed(result.Value) : Attempt.Fail(); } + public Attempt GetIdForUdi(Udi udi) + { + var guidUdi = udi as GuidUdi; + if (guidUdi == null) + return Attempt.Fail(); + + var umbracoType = Constants.UdiEntityType.ToUmbracoObjectType(guidUdi.EntityType); + return GetIdForKey(guidUdi.Guid, umbracoType); + } + /// /// Returns the GUID for a given integer id /// @@ -124,9 +134,9 @@ namespace Umbraco.Core.Services private static Guid GetNodeObjectTypeGuid(UmbracoObjectTypes umbracoObjectType) { - var guid = umbracoObjectType.GetGuid(); + var guid = umbracoObjectType.GetGuid(); if (guid == Guid.Empty) - throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); + throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); return guid; } diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs index 71ad0fcce9..2d2baeeb07 100644 --- a/src/Umbraco.Core/Services/ExternalLoginService.cs +++ b/src/Umbraco.Core/Services/ExternalLoginService.cs @@ -14,8 +14,7 @@ namespace Umbraco.Core.Services { public ExternalLoginService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - } + { } /// /// Returns all user logins assigned @@ -23,30 +22,32 @@ namespace Umbraco.Core.Services /// /// public IEnumerable GetAll(int userId) - { - using (var uow = UowProvider.GetUnitOfWork()) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + // ToList is important here, must evaluate within uow! var repo = RepositoryFactory.CreateExternalLoginRepository(uow); - var ret = repo.GetByQuery(new Query().Where(x => x.UserId == userId)); - uow.Commit(); - return ret; + return repo.GetByQuery(new Query() + .Where(x => x.UserId == userId)) + .ToList(); } } /// - /// Returns all logins matching the login info - generally there should only be one but in some cases + /// Returns all logins matching the login info - generally there should only be one but in some cases /// there might be more than one depending on if an adminstrator has been editing/removing members /// /// /// public IEnumerable Find(UserLoginInfo login) { - using (var uow = UowProvider.GetUnitOfWork()) + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { + // ToList is important here, must evaluate within uow! var repo = RepositoryFactory.CreateExternalLoginRepository(uow); - var ret = repo.GetByQuery(new Query().Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)); - uow.Commit(); - return ret; + return repo.GetByQuery(new Query() + .Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)) + .ToList(); } } @@ -78,7 +79,5 @@ namespace Umbraco.Core.Services uow.Commit(); } } - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 46200287a5..82e5227cf2 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -28,7 +28,14 @@ namespace Umbraco.Core.Services /// /// /// - Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType); + Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType); + + /// + /// Returns the integer id for a given Udi + /// + /// + /// + Attempt GetIdForUdi(Udi udi); /// /// Returns the GUID for a given integer id diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index fbf3a7ee8f..b11404c760 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -89,18 +89,18 @@ namespace Umbraco.Core.Services /// public string FetchPackageFile(Guid packageId, Version umbracoVersion, int userId) { - var packageRepo = UmbracoConfig.For.UmbracoSettings().PackageRepositories.GetDefault(); + var packageRepo = UmbracoConfig.For.UmbracoSettings().PackageRepositories.GetDefault(); using (var httpClient = new HttpClient()) using (var uow = _uowProvider.GetUnitOfWork()) - { + { //includeHidden = true because we don't care if it's hidden we want to get the file regardless var url = string.Format("{0}/{1}?version={2}&includeHidden=true&asFile=true", packageRepo.RestApiUrl, packageId, umbracoVersion.ToString(3)); byte[] bytes; - try - { - bytes = httpClient.GetByteArrayAsync(url).GetAwaiter().GetResult(); - } + try + { + bytes = httpClient.GetByteArrayAsync(url).GetAwaiter().GetResult(); + } catch (HttpRequestException ex) { throw new ConnectionException("An error occuring downloading the package from " + url, ex); @@ -109,20 +109,20 @@ namespace Umbraco.Core.Services //successfull if (bytes.Length > 0) { - var packagePath = IOHelper.MapPath(SystemDirectories.Packages); + var packagePath = IOHelper.MapPath(SystemDirectories.Packages); // Check for package directory if (Directory.Exists(packagePath) == false) Directory.CreateDirectory(packagePath); - var packageFilePath = Path.Combine(packagePath, packageId + ".umb"); + var packageFilePath = Path.Combine(packagePath, packageId + ".umb"); using (var fs1 = new FileStream(packageFilePath, FileMode.Create)) { fs1.Write(bytes, 0, bytes.Length); return "packages\\" + packageId + ".umb"; } - } + } Audit(uow, AuditType.PackagerInstall, string.Format("Package {0} fetched from {1}", packageId, packageRepo.Id), userId, -1); return null; @@ -133,7 +133,7 @@ namespace Umbraco.Core.Services { var auditRepo = _repositoryFactory.CreateAuditRepository(uow); auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - } + } #region Content @@ -285,6 +285,7 @@ namespace Umbraco.Core.Services var nodeName = element.Attribute("nodeName").Value; var path = element.Attribute("path").Value; var template = element.Attribute("template").Value; + var key = Guid.Empty; var properties = from property in element.Elements() where property.Attribute("isDoc") == null @@ -302,6 +303,12 @@ namespace Umbraco.Core.Services SortOrder = int.Parse(sortOrder) }; + if (element.Attribute("key") != null && Guid.TryParse(element.Attribute("key").Value, out key)) + { + // update the Guid (for UDI support) + content.Key = key; + } + foreach (var property in properties) { string propertyTypeAlias = isLegacySchema ? property.Attribute("alias").Value : property.Name.LocalName; @@ -520,7 +527,7 @@ namespace Umbraco.Core.Services { var foldersAttribute = documentType.Attribute("Folders"); var infoElement = documentType.Element("Info"); - if (foldersAttribute != null && infoElement != null + if (foldersAttribute != null && infoElement != null //don't import any folder if this is a child doc type - the parent doc type will need to //exist which contains it's folders && ((string)infoElement.Element("Master")).IsNullOrWhiteSpace()) @@ -1041,7 +1048,7 @@ namespace Umbraco.Core.Services { _logger.Error("Could not create folder: " + rootFolder, tryCreateFolder.Exception); throw tryCreateFolder.Exception; - } + } current = _dataTypeService.GetContainer(tryCreateFolder.Result.Entity.Id); } @@ -1091,14 +1098,14 @@ namespace Umbraco.Core.Services if (dataTypeDefinition != null) { var valuesWithoutKeys = prevaluesElement.Elements("PreValue") - .Where(x => ((string) x.Attribute("Alias")).IsNullOrWhiteSpace()) + .Where(x => ((string)x.Attribute("Alias")).IsNullOrWhiteSpace()) .Select(x => x.Attribute("Value").Value); var valuesWithKeys = prevaluesElement.Elements("PreValue") - .Where(x => ((string) x.Attribute("Alias")).IsNullOrWhiteSpace() == false) + .Where(x => ((string)x.Attribute("Alias")).IsNullOrWhiteSpace() == false) .ToDictionary( - key => (string) key.Attribute("Alias"), - val => new PreValue((string) val.Attribute("Value"))); + key => (string)key.Attribute("Alias"), + val => new PreValue((string)val.Attribute("Value"))); //save the values with keys _dataTypeService.SavePreValues(dataTypeDefinition, valuesWithKeys); diff --git a/src/Umbraco.Core/StringUdi.cs b/src/Umbraco.Core/StringUdi.cs index 0f42f4b5f6..59eb40af7e 100644 --- a/src/Umbraco.Core/StringUdi.cs +++ b/src/Umbraco.Core/StringUdi.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Linq; namespace Umbraco.Core { @@ -20,7 +21,7 @@ namespace Umbraco.Core /// The entity type part of the udi. /// The string id part of the udi. public StringUdi(string entityType, string id) - : base(entityType, "umb://" + entityType + "/" + Uri.EscapeUriString(id)) + : base(entityType, "umb://" + entityType + "/" + EscapeUriString(id)) { Id = id; } @@ -35,6 +36,19 @@ namespace Umbraco.Core Id = Uri.UnescapeDataString(uriValue.AbsolutePath.TrimStart('/')); } + private static string EscapeUriString(string s) + { + // Uri.EscapeUriString preserves / but also [ and ] which is bad + // Uri.EscapeDataString does not preserve / which is bad + + // reserved = : / ? # [ ] @ ! $ & ' ( ) * + , ; = + // unreserved = alpha digit - . _ ~ + + // we want to preserve the / and the unreserved + // so... + return string.Join("/", s.Split('/').Select(Uri.EscapeDataString)); + } + /// /// Converts the string representation of an entity identifier into the equivalent StringUdi instance. /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 3b6caadbcb..1c2ca4da88 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -237,6 +237,7 @@ + diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index ef5d8133f6..2eb045e988 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -79,6 +79,7 @@ + diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index 06dab42556..0287159cd9 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using NUnit.Framework; using Umbraco.Core; @@ -177,6 +178,39 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings public void DisallowedUploadFiles() { Assert.IsTrue(SettingsSection.Content.DisallowedUploadFiles.All(x => "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd".Split(',').Contains(x))); + } + + [Test] + public void AllowedUploadFiles() + { + Assert.IsTrue(SettingsSection.Content.AllowedUploadFiles.All(x => "jpg,gif,png".Split(',').Contains(x))); + } + + [Test] + [TestCase("png", true)] + [TestCase("jpg", true)] + [TestCase("gif", true)] + // TODO: Why does it flip to TestingDefaults=true for these two tests on AppVeyor. WHY? + //[TestCase("bmp", false)] + //[TestCase("php", false)] + [TestCase("ashx", false)] + [TestCase("config", false)] + public void IsFileAllowedForUpload_WithWhitelist(string extension, bool expected) + { + // Make really sure that defaults are NOT used + TestingDefaults = false; + + Debug.WriteLine("Extension being tested", extension); + Debug.WriteLine("AllowedUploadFiles: {0}", SettingsSection.Content.AllowedUploadFiles); + Debug.WriteLine("DisallowedUploadFiles: {0}", SettingsSection.Content.DisallowedUploadFiles); + + var allowedContainsExtension = SettingsSection.Content.AllowedUploadFiles.Any(x => x.InvariantEquals(extension)); + var disallowedContainsExtension = SettingsSection.Content.DisallowedUploadFiles.Any(x => x.InvariantEquals(extension)); + + Debug.WriteLine("AllowedContainsExtension: {0}", allowedContainsExtension); + Debug.WriteLine("DisallowedContainsExtension: {0}", disallowedContainsExtension); + + Assert.AreEqual(SettingsSection.Content.IsFileAllowedForUpload(extension), expected); } } } diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/UmbracoSettingsTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/UmbracoSettingsTests.cs index 8342bd13a3..01768dd903 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/UmbracoSettingsTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/UmbracoSettingsTests.cs @@ -1,7 +1,7 @@ using System.Configuration; +using System.Diagnostics; using System.IO; using NUnit.Framework; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Tests.TestHelpers; @@ -9,20 +9,17 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings { public abstract class UmbracoSettingsTests { - - protected virtual bool TestingDefaults - { - get { return false; } - } + protected virtual bool TestingDefaults { get; set; } [SetUp] public void Init() { var config = new FileInfo(TestHelper.MapPathForTest("~/Configurations/UmbracoSettings/web.config")); - - var fileMap = new ExeConfigurationFileMap() { ExeConfigFilename = config.FullName }; - var configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None); + var fileMap = new ExeConfigurationFileMap() { ExeConfigFilename = config.FullName }; + var configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None); + + Debug.WriteLine("Testing defaults? {0}", TestingDefaults); if (TestingDefaults) { SettingsSection = configuration.GetSection("umbracoConfiguration/defaultSettings") as UmbracoSettingsSection; @@ -32,8 +29,6 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings SettingsSection = configuration.GetSection("umbracoConfiguration/settings") as UmbracoSettingsSection; } - - Assert.IsNotNull(SettingsSection); } diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index 80eaee77d3..a6f9826492 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -100,6 +100,9 @@ ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd + + jpg,png,gif + Textstring diff --git a/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/TypedModelBase.cs b/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/TypedModelBase.cs index 0de99c76ad..3af65457d3 100644 --- a/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/TypedModelBase.cs +++ b/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/TypedModelBase.cs @@ -48,7 +48,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels protected T Resolve(string propertyTypeAlias) { - return Content.GetPropertyValue(propertyTypeAlias); + return WrappedContentInternal.GetPropertyValue(propertyTypeAlias); } protected T Resolve(MethodBase methodBase, T ifCannotConvert) @@ -59,7 +59,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels protected T Resolve(string propertyTypeAlias, T ifCannotConvert) { - return Content.GetPropertyValue(propertyTypeAlias, false, ifCannotConvert); + return WrappedContentInternal.GetPropertyValue(propertyTypeAlias, false, ifCannotConvert); } protected T Resolve(MethodBase methodBase, bool recursive, T ifCannotConvert) @@ -70,7 +70,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels protected T Resolve(string propertyTypeAlias, bool recursive, T ifCannotConvert) { - return Content.GetPropertyValue(propertyTypeAlias, recursive, ifCannotConvert); + return WrappedContentInternal.GetPropertyValue(propertyTypeAlias, recursive, ifCannotConvert); } #endregion @@ -81,7 +81,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels if (constructorInfo == null) throw new Exception("No valid constructor found"); - return (T) constructorInfo.Invoke(new object[] {Content.Parent}); + return (T) constructorInfo.Invoke(new object[] {WrappedContentInternal.Parent}); } protected IEnumerable Children(MethodBase methodBase) where T : TypedModelBase @@ -98,7 +98,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels string singularizedDocTypeAlias = docTypeAlias.ToSingular(); - return Content.Children.Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) + return WrappedContentInternal.Children.Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) .Select(x => (T)constructorInfo.Invoke(new object[] { x })); } @@ -116,7 +116,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels string singularizedDocTypeAlias = docTypeAlias.ToSingular(); - return Content.Ancestors().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) + return WrappedContentInternal.Ancestors().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) .Select(x => (T)constructorInfo.Invoke(new object[] { x })); } @@ -134,7 +134,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels string singularizedDocTypeAlias = docTypeAlias.ToSingular(); - return Content.Descendants().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) + return WrappedContentInternal.Descendants().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) .Select(x => (T)constructorInfo.Invoke(new object[] { x })); } #endregion diff --git a/src/Umbraco.Tests/UdiTests.cs b/src/Umbraco.Tests/UdiTests.cs index 6e7e4671a9..24db54a98f 100644 --- a/src/Umbraco.Tests/UdiTests.cs +++ b/src/Umbraco.Tests/UdiTests.cs @@ -59,6 +59,29 @@ namespace Umbraco.Tests Assert.AreEqual("umb://" + Constants.UdiEntityType.AnyString + "/path%20to/this%20is%20a%20test.xyz", udi3.ToString()); } + [Test, Ignore] + public void StringEncodingTest2() + { + // reserved = : / ? # [ ] @ ! $ & ' ( ) * + , ; = + // unreserved = alpha digit - . _ ~ + + Assert.AreEqual("%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2B%2C%3B%3D.-_~%25", Uri.EscapeDataString(":/?#[]@!$&'()+,;=.-_~%")); + Assert.AreEqual(":/?#[]@!$&'()+,;=.-_~%25", Uri.EscapeUriString(":/?#[]@!$&'()+,;=.-_~%")); + + // we cannot have reserved chars at random places + // we want to keep the / in string udis + + var r = string.Join("/", "path/to/View[1].cshtml".Split('/').Select(Uri.EscapeDataString)); + Assert.AreEqual("path/to/View%5B1%5D.cshtml", r); + Assert.IsTrue(Uri.IsWellFormedUriString("umb://partial-view-macro/" + r, UriKind.Absolute)); + + // with the proper fix in StringUdi this should work: + var udi1 = new StringUdi("partial-view-macro", "path/to/View[1].cshtml"); + Assert.AreEqual("umb://partial-view-macro/path/to/View%5B1%5D.cshtml", udi1.ToString()); + var udi2 = Udi.Parse("umb://partial-view-macro/path/to/View%5B1%5D.cshtml"); + Assert.AreEqual("path/to/View[1].cshtml", ((StringUdi) udi2).Id); + } + [Test] public void GuidEntityCtorTest() { diff --git a/src/Umbraco.Tests/packages.config b/src/Umbraco.Tests/packages.config index c010dcb45c..533d2206dd 100644 --- a/src/Umbraco.Tests/packages.config +++ b/src/Umbraco.Tests/packages.config @@ -29,6 +29,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js index 4960a11f19..c0bd7a4eff 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js @@ -134,25 +134,42 @@ Use this directive to generate a thumbnail grid of media items. } function setItemData(item) { - item.isFolder = !mediaHelper.hasFilePropertyType(item); + + // check if item is a folder + if(item.image) { + // if is has an image path, it is not a folder + item.isFolder = false; + } else { + item.isFolder = !mediaHelper.hasFilePropertyType(item); + } + if (!item.isFolder) { - item.thumbnail = mediaHelper.resolveFile(item, true); - item.image = mediaHelper.resolveFile(item, false); + + // handle entity + if(item.image) { + item.thumbnail = mediaHelper.resolveFileFromEntity(item, true); + item.extension = mediaHelper.getFileExtension(item.image); + // handle full media object + } else { + item.thumbnail = mediaHelper.resolveFile(item, true); + item.image = mediaHelper.resolveFile(item, false); + + var fileProp = _.find(item.properties, function (v) { + return (v.alias === "umbracoFile"); + }); - var fileProp = _.find(item.properties, function (v) { - return (v.alias === "umbracoFile"); - }); + if (fileProp && fileProp.value) { + item.file = fileProp.value; + } - if (fileProp && fileProp.value) { - item.file = fileProp.value; - } + var extensionProp = _.find(item.properties, function (v) { + return (v.alias === "umbracoExtension"); + }); - var extensionProp = _.find(item.properties, function (v) { - return (v.alias === "umbracoExtension"); - }); + if (extensionProp && extensionProp.value) { + item.extension = extensionProp.value; + } - if (extensionProp && extensionProp.value) { - item.extension = extensionProp.value; } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js new file mode 100644 index 0000000000..b8b1776dfe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js @@ -0,0 +1,97 @@ +angular.module("umbraco.directives").directive('nestedContentEditor', [ + + function () { + + var link = function ($scope) { + + // Clone the model because some property editors + // do weird things like updating and config values + // so we want to ensure we start from a fresh every + // time, we'll just sync the value back when we need to + $scope.model = angular.copy($scope.ngModel); + $scope.nodeContext = $scope.model; + + // Find the selected tab + var selectedTab = $scope.model.tabs[0]; + + if ($scope.tabAlias) { + angular.forEach($scope.model.tabs, function (tab) { + if (tab.alias.toLowerCase() === $scope.tabAlias.toLowerCase()) { + selectedTab = tab; + return; + } + }); + } + + $scope.tab = selectedTab; + + // Listen for sync request + var unsubscribe = $scope.$on("ncSyncVal", function (ev, args) { + if (args.key === $scope.model.key) { + + // Tell inner controls we are submitting + $scope.$broadcast("formSubmitting", { scope: $scope }); + + // Sync the values back + angular.forEach($scope.ngModel.tabs, function (tab) { + if (tab.alias.toLowerCase() === selectedTab.alias.toLowerCase()) { + + var localPropsMap = selectedTab.properties.reduce(function (map, obj) { + map[obj.alias] = obj; + return map; + }, {}); + + angular.forEach(tab.properties, function (prop) { + if (localPropsMap.hasOwnProperty(prop.alias)) { + prop.value = localPropsMap[prop.alias].value; + } + }); + + } + }); + } + }); + + $scope.$on('$destroy', function () { + unsubscribe(); + }); + }; + + return { + restrict: "E", + replace: true, + templateUrl: Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/views/propertyeditors/nestedcontent/nestedcontent.editor.html", + scope: { + ngModel: '=', + tabAlias: '=' + }, + link: link + }; + + } +]); + +//angular.module("umbraco.directives").directive('nestedContentSubmitWatcher', function () { +// var link = function (scope) { +// // call the load callback on scope to obtain the ID of this submit watcher +// var id = scope.loadCallback(); +// scope.$on("formSubmitting", function (ev, args) { +// // on the "formSubmitting" event, call the submit callback on scope to notify the nestedContent controller to do it's magic +// if (id === scope.activeSubmitWatcher) { +// scope.submitCallback(); +// } +// }); +// } + +// return { +// restrict: "E", +// replace: true, +// template: "", +// scope: { +// loadCallback: '=', +// submitCallback: '=', +// activeSubmitWatcher: '=' +// }, +// link: link +// } +//}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js new file mode 100644 index 0000000000..76e4e4a822 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js @@ -0,0 +1,47 @@ +// Filter to take a node id and grab it's name instead +// Usage: {{ pickerAlias | ncNodeName }} + +// Cache for node names so we don't make a ton of requests +var ncNodeNameCache = { + id: "", + keys: {} +}; + +angular.module("umbraco.filters").filter("ncNodeName", function (editorState, entityResource) { + + return function (input) { + + // Check we have a value at all + if (input === "" || input.toString() === "0") { + return ""; + } + + var currentNode = editorState.getCurrent(); + + // Ensure a unique cache per editor instance + var key = "ncNodeName_" + currentNode.key; + if (ncNodeNameCache.id !== key) { + ncNodeNameCache.id = key; + ncNodeNameCache.keys = {}; + } + + // See if there is a value in the cache and use that + if (ncNodeNameCache.keys[input]) { + return ncNodeNameCache.keys[input]; + } + + // No value, so go fetch one + // We'll put a temp value in the cache though so we don't + // make a load of requests while we wait for a response + ncNodeNameCache.keys[input] = "Loading..."; + + entityResource.getById(input, "Document") + .then(function (ent) { + ncNodeNameCache.keys[input] = ent.name; + }); + + // Return the current value for now + return ncNodeNameCache.keys[input]; + }; + +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js index 8059975fc1..d74c7d82ac 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js @@ -68,7 +68,7 @@ function logResource($q, $http, umbRequestHelper) { "logApiBaseUrl", "GetCurrentUserLog", [{ logtype: type, sinceDate: since }])), - 'Failed to retrieve user data for id ' + id); + 'Failed to retrieve log data for current user of type ' + type + ' since ' + since); }, /** @@ -99,7 +99,7 @@ function logResource($q, $http, umbRequestHelper) { "logApiBaseUrl", "GetLog", [{ logtype: type, sinceDate: since }])), - 'Failed to retrieve user data for id ' + id); + 'Failed to retrieve log data of type ' + type + ' since ' + since); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/nestedcontent.resources.js b/src/Umbraco.Web.UI.Client/src/common/resources/nestedcontent.resources.js new file mode 100644 index 0000000000..b3488fdff4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/nestedcontent.resources.js @@ -0,0 +1,12 @@ +angular.module('umbraco.resources').factory('Umbraco.PropertyEditors.NestedContent.Resources', + function ($q, $http, umbRequestHelper) { + return { + getContentTypes: function () { + var url = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/backoffice/UmbracoApi/NestedContent/GetContentTypes"; + return umbRequestHelper.resourcePromise( + $http.get(url), + 'Failed to retrieve content types' + ); + }, + }; + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js index 114e1a0962..636437e387 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js @@ -365,6 +365,28 @@ function mediaHelper(umbRequestHelper) { return newFileTypesArray.join(","); + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getFileExtension + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns file extension + * + * @param {string} filePath File path, ex /media/1234/my-image.jpg + */ + getFileExtension: function(filePath) { + + if (!filePath) { + return false; + } + + var lowered = filePath.toLowerCase(); + var ext = lowered.substr(lowered.lastIndexOf(".") + 1); + return ext; } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index c31eac7697..3732e25403 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -103,15 +103,15 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ * @description * This returns a promise with an underlying http call, it is a helper method to reduce * the amount of duplicate code needed to query http resources and automatically handle any - * 500 Http server errors. + * Http errors. See /docs/source/using-promises-resources.md * - * @param {object} opts A mixed object which can either be a `string` representing the error message to be - * returned OR an `object` containing either: + * @param {object} opts A mixed object which can either be a string representing the error message to be + * returned OR an object containing either: * { success: successCallback, errorMsg: errorMessage } * OR * { success: successCallback, error: errorCallback } - * In both of the above, the successCallback must accept these parameters: `data`, `status`, `headers`, `config` - * If using the errorCallback it must accept these parameters: `data`, `status`, `headers`, `config` + * In both of the above, the successCallback must accept these parameters: data, status, headers, config + * If using the errorCallback it must accept these parameters: data, status, headers, config * The success callback must return the data which will be resolved by the deferred object. * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } */ diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js index 5cb8e6230f..3ba655d934 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -17,7 +17,7 @@ angular.module("umbraco.install").factory('installerService', function($rootScop //add to umbraco installer facts here var facts = ['Umbraco helped millions of people watch a man jump from the edge of space', - 'Over 370 000 websites are currently powered by Umbraco', + 'Over 420 000 websites are currently powered by Umbraco', "At least 2 people have named their cat 'Umbraco'", 'On an average day, more than 1000 people download Umbraco', 'umbraco.tv is the premier source of Umbraco video tutorials to get you started', @@ -31,7 +31,7 @@ angular.module("umbraco.install").factory('installerService', function($rootScop "At least 4 people have the Umbraco logo tattooed on them", "'Umbraco' is the danish name for an allen key", "Umbraco has been around since 2005, that's a looong time in IT", - "More than 400 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", + "More than 550 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", "You can extend Umbraco without modifying the source code using either JavaScript or C#", "Umbraco was installed in more than 165 countries in 2015" diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/starterkit.html b/src/Umbraco.Web.UI.Client/src/installer/steps/starterkit.html index 0b9e22a0f3..b2453c5a82 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/starterkit.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/starterkit.html @@ -1,23 +1,16 @@
-

Install a starter website

- -

- Installing a starter website helps you learn how Umbraco works, and gives you a solid - and simple foundation to build on top of. -

- - Loading... - - +

Would you like to learn or demo Umbraco?

+ {{pck.name}} +

The Starter Kit is a great way to experience some of the ways you can use Umbraco. It's a complete website with textpages, landing pages, blog, product listings and more that's easy to get started with Umbraco. +

+

+ It's also a great way to learn Umbraco as the Starter Kit comes with a set of Lessons that'll teach you how to implement and extend Umbraco using short 5-15 minute tasks. +

+

+ Yes, I'd like a Starter Kit +   - No thanks, I do not want to install a starter website + No thanks

diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 90b43a2b2f..5776a297fa 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -121,6 +121,7 @@ @import "components/umb-pagination.less"; @import "components/umb-mini-list-view.less"; @import "components/umb-badge.less"; +@import "components/umb-nested-content.less"; @import "components/umb-checkmark.less"; @import "components/buttons/umb-button.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less new file mode 100644 index 0000000000..38de65d7d8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -0,0 +1,192 @@ +.nested-content +{ + text-align: center; +} + +.nested-content__item +{ + position: relative; + text-align: left; + border-top: solid 1px transparent; + background: white; + + +} + +.nested-content__item--active:not(.nested-content__item--single) +{ + background: #f8f8f8; +} + +.nested-content__item.ui-sortable-placeholder +{ + background: #f8f8f8; + border: 1px dashed #d9d9d9; + visibility: visible !important; + height: 55px; + margin-top: -1px; +} + +.nested-content__item--single > .nested-content__content +{ + border: 0; +} + +.nested-content__item--single > .nested-content__content > .umb-pane +{ + margin: 0; +} + +.nested-content__header-bar +{ + padding: 15px 20px; + border-bottom: 1px dashed #e0e0e0; + text-align: right; + cursor: pointer; + background-color: white; + + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + -o-user-select: none; +} + +.nested-content__heading +{ + float: left; + line-height: 20px; +} + +.nested-content__heading i +{ + vertical-align: text-top; + color: #999; /* same icon color as the icons in the item type picker */ + margin-right: 10px; +} + +.nested-content__icons +{ + margin: -6px 0; + opacity: 0; + + transition: opacity .15s ease-in-out; + -moz-transition: opacity .15s ease-in-out; + -webkit-transition: opacity .15s ease-in-out; +} + +.nested-content__header-bar:hover .nested-content__icons, +.nested-content__item--active > .nested-content__header-bar .nested-content__icons +{ + opacity: 1; +} + +.nested-content__icon, +.nested-content__icon.nested-content__icon--disabled:hover +{ + display: inline-block; + padding: 4px 6px; + margin: 2px; + cursor: pointer; + background: #fff; + border: 1px solid #b6b6b6; + border-radius: 200px; + text-decoration: none !important; +} + +.nested-content__icon:hover, +.nested-content__icon--active +{ + color: white; + background: #2e8aea; + border-color: #2e8aea; + text-decoration: none; +} + +.nested-content__icon .icon, +.nested-content__icon.nested-content__icon--disabled:hover .icon +{ + display: block; + font-size: 16px !important; + color: #5f5f5f; +} + +.nested-content__icon:hover .icon, +.nested-content__icon--active .icon +{ + color: white; +} + +.nested-content__icon--disabled +{ + opacity: 0.3; +} + + +.nested-content__footer-bar +{ + text-align: center; + padding-top: 20px; +} + +.nested-content__content +{ + border-bottom: 1px dashed #e0e0e0; +} + +.nested-content__content .umb-control-group { + padding-bottom: 0; +} + +.nested-content__item.ui-sortable-helper .nested-content__content +{ + display: none !important; +} + +.nested-content__help-text +{ + display: inline-block; + padding: 10px 20px 10px 20px; + clear: both; + font-size: 14px; + color: #555; + background: #f8f8f8; + border-radius: 15px; +} + +.nested-content__doctypepicker table input, .nested-content__doctypepicker table select { + width: 100%; + padding-right: 0; +} + +.nested-content__doctypepicker table td.icon-navigation, .nested-content__doctypepicker i.nested-content__help-icon { + vertical-align: middle; + color: #CCC; +} + +.nested-content__doctypepicker table td.icon-navigation:hover, .nested-content__doctypepicker i.nested-content__help-icon:hover { + color: #343434; +} + +.nested-content__doctypepicker i.nested-content__help-icon { + margin-left: 10px; +} + +.form-horizontal .nested-content--narrow .controls-row +{ + margin-left: 40% !important; +} + +.form-horizontal .nested-content--narrow .controls-row .umb-textstring, +.form-horizontal .nested-content--narrow .controls-row .umb-textarea +{ + width: 95%; +} + +.form-horizontal .nested-content--narrow .controls-row .umb-dropdown { + width: 99%; +} + +.usky-grid.nested-content__node-type-picker .cell-tools-menu { + position: relative; + transform: translate(-50%, -25%); +} diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less index f2815f445c..df1bc4114a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/tree.less @@ -49,7 +49,7 @@ .umb-tree li.current > div i.icon, .umb-tree li.current > div ins { color: @white !important; - background: @turquoise-d1; + background-color: @turquoise-d1; border-color: @turquoise-d1; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index 7010a0db9d..1992bed068 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -16,14 +16,22 @@ angular.module("umbraco") $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; $scope.cropSize = dialogOptions.cropSize; $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); + + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; + var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); if ($scope.onlyImages) { - $scope.acceptedFileTypes = mediaHelper - .formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); + $scope.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); } else { - $scope.acceptedFileTypes = !mediaHelper - .formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); + // Use whitelist of allowed file types if provided + if (allowedUploadFiles !== '') { + $scope.acceptedFileTypes = allowedUploadFiles; + } else { + // If no whitelist, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + $scope.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); + } } - $scope.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; + + $scope.maxFileSize = umbracoSettings.maxFileSize + "KB"; $scope.model.selectedImages = []; @@ -131,8 +139,16 @@ angular.module("umbraco") } else { eventsService.emit("dialogs.mediaPicker.select", image); if ($scope.showDetails) { + $scope.target = image; - $scope.target.url = mediaHelper.resolveFile(image); + + // handle both entity and full media object + if(image.image) { + $scope.target.url = image.image; + } else { + $scope.target.url = mediaHelper.resolveFile(image); + } + $scope.openDetailsDialog(); } else { selectImage(image); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 7c5f0f728f..412a57288d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -15,7 +15,7 @@ {{item.name}} - {{item.name}} + {{item.name}} {{item.name}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js index b8ba4f880b..9a28627aa1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js @@ -6,118 +6,124 @@ * @description * The controller for the content type editor */ -(function() { - "use strict"; +(function () { + "use strict"; - function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource, $location, listViewHelper, mediaTypeHelper) { + function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource, $location, listViewHelper, mediaTypeHelper) { - var vm = this; + var vm = this; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; - vm.nodeId = $scope.contentId; - //we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles - vm.acceptedFileTypes = !mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); - vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; - vm.activeDrag = false; - vm.mediaDetailsTooltip = {}; - vm.itemsWithoutFolders = []; - vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; - vm.acceptedMediatypes = []; + vm.nodeId = $scope.contentId; + // Use whitelist of allowed file types if provided + vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); + if (vm.acceptedFileTypes === '') { + // If not provided, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); + } - vm.dragEnter = dragEnter; - vm.dragLeave = dragLeave; - vm.onFilesQueue = onFilesQueue; - vm.onUploadComplete = onUploadComplete; + vm.maxFileSize = umbracoSettings.maxFileSize + "KB"; + vm.activeDrag = false; + vm.mediaDetailsTooltip = {}; + vm.itemsWithoutFolders = []; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + vm.acceptedMediatypes = []; - vm.hoverMediaItemDetails = hoverMediaItemDetails; - vm.selectContentItem = selectContentItem; - vm.selectItem = selectItem; - vm.selectFolder = selectFolder; - vm.goToItem = goToItem; + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; - function activate() { - vm.itemsWithoutFolders = filterOutFolders($scope.items); + vm.hoverMediaItemDetails = hoverMediaItemDetails; + vm.selectContentItem = selectContentItem; + vm.selectItem = selectItem; + vm.selectFolder = selectFolder; + vm.goToItem = goToItem; - //no need to make another REST/DB call if this data is not used when we are browsing the bin - if ($scope.entityType === 'media' && !vm.isRecycleBin) { - mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { - vm.acceptedMediatypes = types; - }); - } + function activate() { + vm.itemsWithoutFolders = filterOutFolders($scope.items); - } + //no need to make another REST/DB call if this data is not used when we are browsing the bin + if ($scope.entityType === 'media' && !vm.isRecycleBin) { + mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { + vm.acceptedMediatypes = types; + }); + } - function filterOutFolders(items) { + } - var newArray = []; + function filterOutFolders(items) { - if(items && items.length ) { + var newArray = []; - for (var i = 0; items.length > i; i++) { - var item = items[i]; - var isFolder = !mediaHelper.hasFilePropertyType(item); + if (items && items.length) { - if (!isFolder) { - newArray.push(item); - } - } + for (var i = 0; items.length > i; i++) { + var item = items[i]; + var isFolder = !mediaHelper.hasFilePropertyType(item); - } + if (!isFolder) { + newArray.push(item); + } + } - return newArray; - } + } - function dragEnter(el, event) { - vm.activeDrag = true; - } + return newArray; + } - function dragLeave(el, event) { - vm.activeDrag = false; - } + function dragEnter(el, event) { + vm.activeDrag = true; + } - function onFilesQueue() { - vm.activeDrag = false; - } + function dragLeave(el, event) { + vm.activeDrag = false; + } - function onUploadComplete() { - $scope.getContent($scope.contentId); - } + function onFilesQueue() { + vm.activeDrag = false; + } - function hoverMediaItemDetails(item, event, hover) { + function onUploadComplete() { + $scope.getContent($scope.contentId); + } - if (hover && !vm.mediaDetailsTooltip.show) { + function hoverMediaItemDetails(item, event, hover) { - vm.mediaDetailsTooltip.event = event; - vm.mediaDetailsTooltip.item = item; - vm.mediaDetailsTooltip.show = true; + if (hover && !vm.mediaDetailsTooltip.show) { - } else if (!hover && vm.mediaDetailsTooltip.show) { + vm.mediaDetailsTooltip.event = event; + vm.mediaDetailsTooltip.item = item; + vm.mediaDetailsTooltip.show = true; - vm.mediaDetailsTooltip.show = false; + } else if (!hover && vm.mediaDetailsTooltip.show) { - } + vm.mediaDetailsTooltip.show = false; - } + } - function selectContentItem(item, $event, $index) { - listViewHelper.selectHandler(item, $index, $scope.items, $scope.selection, $event); - } + } - function selectItem(item, $event, $index) { - listViewHelper.selectHandler(item, $index, vm.itemsWithoutFolders, $scope.selection, $event); - } + function selectContentItem(item, $event, $index) { + listViewHelper.selectHandler(item, $index, $scope.items, $scope.selection, $event); + } - function selectFolder(folder, $event, $index) { - listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event); - } + function selectItem(item, $event, $index) { + listViewHelper.selectHandler(item, $index, vm.itemsWithoutFolders, $scope.selection, $event); + } - function goToItem(item, $event, $index) { - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); - } + function selectFolder(folder, $event, $index) { + listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event); + } - activate(); + function goToItem(item, $event, $index) { + $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); + } - } + activate(); - angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.GridLayoutController", ListViewGridLayoutController); + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.GridLayoutController", ListViewGridLayoutController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js index 15c2042477..799cc5894c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js @@ -1,89 +1,96 @@ (function () { - "use strict"; + "use strict"; - function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) { + function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) { - var vm = this; + var vm = this; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; - vm.nodeId = $scope.contentId; - //we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles - vm.acceptedFileTypes = !mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); - vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; - vm.activeDrag = false; - vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; - vm.acceptedMediatypes = []; + vm.nodeId = $scope.contentId; - vm.selectItem = selectItem; - vm.clickItem = clickItem; - vm.selectAll = selectAll; - vm.isSelectedAll = isSelectedAll; - vm.isSortDirection = isSortDirection; - vm.sort = sort; - vm.dragEnter = dragEnter; - vm.dragLeave = dragLeave; - vm.onFilesQueue = onFilesQueue; - vm.onUploadComplete = onUploadComplete; - - function activate() { - - if ($scope.entityType === 'media') { - mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { - vm.acceptedMediatypes = types; - }); + // Use whitelist of allowed file types if provided + vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); + if (vm.acceptedFileTypes === '') { + // If not provided, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); } - } + vm.maxFileSize = umbracoSettings.maxFileSize + "KB"; + vm.activeDrag = false; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + vm.acceptedMediatypes = []; - function selectAll($event) { - listViewHelper.selectAllItems($scope.items, $scope.selection, $event); - } + vm.selectItem = selectItem; + vm.clickItem = clickItem; + vm.selectAll = selectAll; + vm.isSelectedAll = isSelectedAll; + vm.isSortDirection = isSortDirection; + vm.sort = sort; + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; - function isSelectedAll() { - return listViewHelper.isSelectedAll($scope.items, $scope.selection); - } + function activate() { - function selectItem(selectedItem, $index, $event) { - listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); - } + if ($scope.entityType === 'media') { + mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { + vm.acceptedMediatypes = types; + }); + } - function clickItem(item) { - // if item.id is 2147483647 (int.MaxValue) use item.key - $location.path($scope.entityType + '/' +$scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id)); - } + } - function isSortDirection(col, direction) { - return listViewHelper.setSortingDirection(col, direction, $scope.options); - } + function selectAll($event) { + listViewHelper.selectAllItems($scope.items, $scope.selection, $event); + } - function sort(field, allow, isSystem) { - if (allow) { - $scope.options.orderBySystemField = isSystem; - listViewHelper.setSorting(field, allow, $scope.options); + function isSelectedAll() { + return listViewHelper.isSelectedAll($scope.items, $scope.selection); + } + + function selectItem(selectedItem, $index, $event) { + listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); + } + + function clickItem(item) { + // if item.id is 2147483647 (int.MaxValue) use item.key + $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id)); + } + + function isSortDirection(col, direction) { + return listViewHelper.setSortingDirection(col, direction, $scope.options); + } + + function sort(field, allow, isSystem) { + if (allow) { + $scope.options.orderBySystemField = isSystem; + listViewHelper.setSorting(field, allow, $scope.options); + $scope.getContent($scope.contentId); + } + } + + // Dropzone upload functions + function dragEnter(el, event) { + vm.activeDrag = true; + } + + function dragLeave(el, event) { + vm.activeDrag = false; + } + + function onFilesQueue() { + vm.activeDrag = false; + } + + function onUploadComplete() { $scope.getContent($scope.contentId); - } - } + } - // Dropzone upload functions - function dragEnter(el, event) { - vm.activeDrag = true; - } + activate(); - function dragLeave(el, event) { - vm.activeDrag = false; - } + } - function onFilesQueue() { - vm.activeDrag = false; - } + angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); - function onUploadComplete() { - $scope.getContent($scope.contentId); - } - - activate(); - - } - -angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); - -}) (); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js new file mode 100644 index 0000000000..10b9cffc8f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -0,0 +1,417 @@ +angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.DocTypePickerController", [ + + "$scope", + "Umbraco.PropertyEditors.NestedContent.Resources", + + function ($scope, ncResources) { + + $scope.add = function () { + $scope.model.value.push({ + // As per PR #4, all stored content type aliases must be prefixed "nc" for easier recognition. + // For good measure we'll also prefix the tab alias "nc" + ncAlias: "", + ncTabAlias: "", + nameTemplate: "" + } + ); + } + + $scope.remove = function (index) { + $scope.model.value.splice(index, 1); + } + + $scope.sortableOptions = { + axis: 'y', + cursor: "move", + handle: ".icon-navigation" + }; + + $scope.selectedDocTypeTabs = {}; + + ncResources.getContentTypes().then(function (docTypes) { + $scope.model.docTypes = docTypes; + + // Populate document type tab dictionary + docTypes.forEach(function (value) { + $scope.selectedDocTypeTabs[value.alias] = value.tabs; + }); + }); + + if (!$scope.model.value) { + $scope.model.value = []; + $scope.add(); + } + } +]); + +angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.PropertyEditorController", [ + + "$scope", + "$interpolate", + "$filter", + "$timeout", + "contentResource", + "localizationService", + "iconHelper", + "Umbraco.PropertyEditors.NestedContent.Resources", + + function ($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, ncResources) { + + //$scope.model.config.contentTypes; + //$scope.model.config.minItems; + //$scope.model.config.maxItems; + //console.log($scope); + + var inited = false; + + _.each($scope.model.config.contentTypes, function (contentType) { + contentType.nameExp = !!contentType.nameTemplate + ? $interpolate(contentType.nameTemplate) + : undefined; + }); + + $scope.editIconTitle = ''; + $scope.moveIconTitle = ''; + $scope.deleteIconTitle = ''; + + // localize the edit icon title + localizationService.localize('general_edit').then(function (value) { + $scope.editIconTitle = value; + }); + + // localize the delete icon title + localizationService.localize('general_delete').then(function (value) { + $scope.deleteIconTitle = value; + }); + + // localize the move icon title + localizationService.localize('actions_move').then(function (value) { + $scope.moveIconTitle = value; + }); + + $scope.nodes = []; + $scope.currentNode = undefined; + $scope.realCurrentNode = undefined; + $scope.scaffolds = undefined; + $scope.sorting = false; + + $scope.minItems = $scope.model.config.minItems || 0; + $scope.maxItems = $scope.model.config.maxItems || 0; + + if ($scope.maxItems == 0) + $scope.maxItems = 1000; + + $scope.singleMode = $scope.minItems == 1 && $scope.maxItems == 1; + $scope.showIcons = $scope.model.config.showIcons || true; + $scope.wideMode = $scope.model.config.hideLabel == "1"; + + $scope.overlayMenu = { + show: false, + style: {} + }; + + // helper to force the current form into the dirty state + $scope.setDirty = function () { + if ($scope.propertyForm) { + $scope.propertyForm.$setDirty(); + } + }; + + $scope.addNode = function (alias) { + var scaffold = $scope.getScaffold(alias); + + var newNode = initNode(scaffold, null); + + $scope.currentNode = newNode; + $scope.setDirty(); + + $scope.closeNodeTypePicker(); + }; + + $scope.openNodeTypePicker = function (event) { + if ($scope.nodes.length >= $scope.maxItems) { + return; + } + + // this could be used for future limiting on node types + $scope.overlayMenu.scaffolds = []; + _.each($scope.scaffolds, function (scaffold) { + $scope.overlayMenu.scaffolds.push({ + alias: scaffold.contentTypeAlias, + name: scaffold.contentTypeName, + icon: iconHelper.convertFromLegacyIcon(scaffold.icon) + }); + }); + + if ($scope.overlayMenu.scaffolds.length == 0) { + return; + } + + if ($scope.overlayMenu.scaffolds.length == 1) { + // only one scaffold type - no need to display the picker + $scope.addNode($scope.scaffolds[0].contentTypeAlias); + return; + } + + $scope.overlayMenu.show = true; + }; + + $scope.closeNodeTypePicker = function () { + $scope.overlayMenu.show = false; + }; + + $scope.editNode = function (idx) { + if ($scope.currentNode && $scope.currentNode.key == $scope.nodes[idx].key) { + $scope.currentNode = undefined; + } else { + $scope.currentNode = $scope.nodes[idx]; + } + }; + + $scope.deleteNode = function (idx) { + if ($scope.nodes.length > $scope.model.config.minItems) { + if ($scope.model.config.confirmDeletes && $scope.model.config.confirmDeletes == 1) { + if (confirm("Are you sure you want to delete this item?")) { + $scope.nodes.splice(idx, 1); + $scope.setDirty(); + updateModel(); + } + } else { + $scope.nodes.splice(idx, 1); + $scope.setDirty(); + updateModel(); + } + } + }; + + $scope.getName = function (idx) { + + var name = "Item " + (idx + 1); + + if ($scope.model.value[idx]) { + + var contentType = $scope.getContentTypeConfig($scope.model.value[idx].ncContentTypeAlias); + + if (contentType != null && contentType.nameExp) { + // Run the expression against the stored dictionary value, NOT the node object + var item = $scope.model.value[idx]; + + // Add a temporary index property + item['$index'] = (idx + 1); + + var newName = contentType.nameExp(item); + if (newName && (newName = $.trim(newName))) { + name = newName; + } + + // Delete the index property as we don't want to persist it + delete item['$index']; + } + + } + + // Update the nodes actual name value + if ($scope.nodes[idx].name !== name) { + $scope.nodes[idx].name = name; + } + + + return name; + }; + + $scope.getIcon = function (idx) { + var scaffold = $scope.getScaffold($scope.model.value[idx].ncContentTypeAlias); + return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : "icon-folder"; + } + + $scope.sortableOptions = { + axis: 'y', + cursor: "move", + handle: ".nested-content__icon--move", + start: function (ev, ui) { + // Yea, yea, we shouldn't modify the dom, sue me + $("#nested-content--" + $scope.model.id + " .umb-rte textarea").each(function () { + tinymce.execCommand('mceRemoveEditor', false, $(this).attr('id')); + $(this).css("visibility", "hidden"); + }); + $scope.$apply(function () { + $scope.sorting = true; + }); + }, + update: function (ev, ui) { + $scope.setDirty(); + }, + stop: function (ev, ui) { + $("#nested-content--" + $scope.model.id + " .umb-rte textarea").each(function () { + tinymce.execCommand('mceAddEditor', true, $(this).attr('id')); + $(this).css("visibility", "visible"); + }); + $scope.$apply(function () { + $scope.sorting = false; + updateModel(); + }); + } + }; + + $scope.getScaffold = function (alias) { + return _.find($scope.scaffolds, function (scaffold) { + return scaffold.contentTypeAlias == alias; + }); + } + + $scope.getContentTypeConfig = function (alias) { + return _.find($scope.model.config.contentTypes, function (contentType) { + return contentType.ncAlias == alias; + }); + } + + // Initialize + var scaffoldsLoaded = 0; + $scope.scaffolds = []; + _.each($scope.model.config.contentTypes, function (contentType) { + contentResource.getScaffold(-20, contentType.ncAlias).then(function (scaffold) { + // remove all tabs except the specified tab + var tab = _.find(scaffold.tabs, function (tab) { + return tab.id != 0 && (tab.alias.toLowerCase() == contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias == ""); + }); + scaffold.tabs = []; + if (tab) { + scaffold.tabs.push(tab); + } + + // Store the scaffold object + $scope.scaffolds.push(scaffold); + + scaffoldsLoaded++; + initIfAllScaffoldsHaveLoaded(); + }, function (error) { + scaffoldsLoaded++; + initIfAllScaffoldsHaveLoaded(); + }); + }); + + var initIfAllScaffoldsHaveLoaded = function () { + // Initialize when all scaffolds have loaded + if ($scope.model.config.contentTypes.length == scaffoldsLoaded) { + // Because we're loading the scaffolds async one at a time, we need to + // sort them explicitly according to the sort order defined by the data type. + var contentTypeAliases = []; + _.each($scope.model.config.contentTypes, function (contentType) { + contentTypeAliases.push(contentType.ncAlias); + }); + $scope.scaffolds = $filter('orderBy')($scope.scaffolds, function (s) { + return contentTypeAliases.indexOf(s.contentTypeAlias); + }); + + // Convert stored nodes + if ($scope.model.value) { + for (var i = 0; i < $scope.model.value.length; i++) { + var item = $scope.model.value[i]; + var scaffold = $scope.getScaffold(item.ncContentTypeAlias); + if (scaffold == null) { + // No such scaffold - the content type might have been deleted. We need to skip it. + continue; + } + initNode(scaffold, item); + } + } + + // Enforce min items + if ($scope.nodes.length < $scope.model.config.minItems) { + for (var i = $scope.nodes.length; i < $scope.model.config.minItems; i++) { + $scope.addNode($scope.scaffolds[0].contentTypeAlias); + } + } + + // If there is only one item, set it as current node + if ($scope.singleMode || ($scope.nodes.length == 1 && $scope.maxItems == 1)) { + $scope.currentNode = $scope.nodes[0]; + } + + inited = true; + } + } + + var initNode = function (scaffold, item) { + var node = angular.copy(scaffold); + + node.key = guid(); + node.ncContentTypeAlias = scaffold.contentTypeAlias; + + for (var t = 0; t < node.tabs.length; t++) { + var tab = node.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + prop.propertyAlias = prop.alias; + prop.alias = $scope.model.alias + "___" + prop.alias; + // Force validation to occur server side as this is the + // only way we can have consistancy between mandatory and + // regex validation messages. Not ideal, but it works. + prop.validation = { + mandatory: false, + pattern: "" + }; + if (item) { + if (item[prop.propertyAlias]) { + prop.value = item[prop.propertyAlias]; + } + } + } + } + + $scope.nodes.push(node); + + return node; + } + + var updateModel = function () { + if ($scope.realCurrentNode) { + $scope.$broadcast("ncSyncVal", { key: $scope.realCurrentNode.key }); + } + if (inited) { + var newValues = []; + for (var i = 0; i < $scope.nodes.length; i++) { + var node = $scope.nodes[i]; + var newValue = { + key: node.key, + name: node.name, + ncContentTypeAlias: node.ncContentTypeAlias + }; + for (var t = 0; t < node.tabs.length; t++) { + var tab = node.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (typeof prop.value !== "function") { + newValue[prop.propertyAlias] = prop.value; + } + } + } + newValues.push(newValue); + } + $scope.model.value = newValues; + } + } + + $scope.$watch("currentNode", function (newVal) { + updateModel(); + $scope.realCurrentNode = newVal; + }); + + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + updateModel(); + }); + + $scope.$on('$destroy', function () { + unsubscribe(); + }); + + var guid = function () { + function _p8(s) { + var p = (Math.random().toString(16) + "000000000").substr(2, 8); + return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p; + } + return _p8() + _p8(true) + _p8(true) + _p8(); + }; + } + +]); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html new file mode 100644 index 0000000000..5a7d4edfdd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html @@ -0,0 +1,58 @@ +
+
+ + + + + + + + + + + + + + + + + +
+ + Document Type + + Tab + + Name Template + +
+ + + + + + + + Remove +
+
+ Add + +
+
+
+
+

+ Tab:
+ Select the tab who's properties should be displayed. If left blank, the first tab on the doc type will be used. +

+

+ Name template:
+ Enter an angular expression to evaluate against each item for its name. Use {{$index}} to display the item index +

+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html new file mode 100644 index 0000000000..b3d338feb3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html @@ -0,0 +1,9 @@ +
+ + + + + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html new file mode 100644 index 0000000000..4799167452 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -0,0 +1,62 @@ +
+ + +
+ +
+ +
+ +
+ + + +
+ +
+ +
+
+ +
+ +
+ +
+ + + +
+
+
+ +
+ +
+
+ +
+
diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 78d13ab600..f208ab649e 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -102,6 +102,9 @@ ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,svg,php,htaccess + + + Textstring diff --git a/src/Umbraco.Web.UI/umbraco_client/Editors/EditXslt.js b/src/Umbraco.Web.UI/umbraco_client/Editors/EditXslt.js index d17296a958..67f0259afb 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Editors/EditXslt.js +++ b/src/Umbraco.Web.UI/umbraco_client/Editors/EditXslt.js @@ -37,7 +37,7 @@ codeVal = UmbEditor.GetCode(); } umbraco.presentation.webservices.codeEditorSave.SaveXslt( - fileName, self._opts.originalFileName, codeVal, self._opts.skipTestingCheckBox.is(':checked'), + fileName, self._opts.originalFileName, codeVal, true, function (t) { self.submitSucces(t); }, function (t) { self.submitFailure(t); }); diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index 34ca2ceec0..391fd09dd1 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -6,6 +6,7 @@ using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.PropertyEditors; using Umbraco.Web.PropertyEditors.ValueConverters; @@ -13,12 +14,12 @@ namespace Umbraco.Web.Cache { /// /// A cache refresher to ensure member cache is updated when members change - /// + ///
public sealed class DataTypeCacheRefresher : JsonCacheRefresherBase { #region Static helpers - + /// /// Converts the json to a JsonPayload object /// @@ -29,7 +30,7 @@ namespace Umbraco.Web.Cache var serializer = new JavaScriptSerializer(); var jsonObject = serializer.Deserialize(json); return jsonObject; - } + } /// /// Creates the custom Json payload used to refresh cache amongst the servers @@ -43,7 +44,7 @@ namespace Umbraco.Web.Cache var json = serializer.Serialize(items); return json; } - + /// /// Converts a macro to a jsonPayload object /// @@ -58,7 +59,7 @@ namespace Umbraco.Web.Cache }; return payload; } - + #endregion #region Sub classes @@ -93,7 +94,7 @@ namespace Umbraco.Web.Cache //we need to clear the ContentType runtime cache since that is what caches the // db data type to store the value against and anytime a datatype changes, this also might change // we basically need to clear all sorts of runtime caches here because so many things depend upon a data type - + ClearAllIsolatedCacheByEntityType(); ClearAllIsolatedCacheByEntityType(); ClearAllIsolatedCacheByEntityType(); @@ -104,14 +105,15 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); var dataTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); - payloads.ForEach(payload => + foreach (var payload in payloads) { //clears the prevalue cache if (dataTypeCache) dataTypeCache.Result.ClearCacheByKeySearch(string.Format("{0}_{1}", CacheKeys.DataTypePreValuesCacheKey, payload.Id)); PublishedContentType.ClearDataType(payload.Id); - }); + NestedContentHelper.ClearCache(payload.Id); + } TagsValueConverter.ClearCaches(); MultipleMediaPickerPropertyConverter.ClearCaches(); diff --git a/src/Umbraco.Web/Cache/PageCacheRefresher.cs b/src/Umbraco.Web/Cache/PageCacheRefresher.cs index 83d38924ed..e884c9b3b8 100644 --- a/src/Umbraco.Web/Cache/PageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PageCacheRefresher.cs @@ -15,7 +15,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 - /// + /// public class PageCacheRefresher : TypedCacheRefresherBase { @@ -51,6 +51,7 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { content.Instance.RefreshContentFromDatabase(); + XmlPublishedContent.ClearRequest(); base.RefreshAll(); } @@ -62,6 +63,7 @@ namespace Umbraco.Web.Cache { ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); content.Instance.UpdateDocumentCache(id); + XmlPublishedContent.ClearRequest(); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); base.Refresh(id); @@ -75,6 +77,7 @@ namespace Umbraco.Web.Cache { ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); content.Instance.ClearDocumentCache(id, false); + XmlPublishedContent.ClearRequest(); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); ClearAllIsolatedCacheByEntityType(); diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 136f287c64..61278a1361 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -225,7 +225,7 @@ namespace Umbraco.Web.Editors if (identityUser != null) { var user = Services.UserService.GetByEmail(model.Email); - if (user != null && user.IsLockedOut == false) + if (user != null) { var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); var callbackUrl = ConstructCallbackUrl(identityUser.Id, code); @@ -324,9 +324,31 @@ namespace Umbraco.Web.Editors var result = await UserManager.ResetPasswordAsync(model.UserId, model.ResetCode, model.Password); if (result.Succeeded) { + var lockedOut = await UserManager.IsLockedOutAsync(model.UserId); + if (lockedOut) + { + Logger.Info( + "User {0} is currently locked out, unlocking and resetting AccessFailedCount", + () => model.UserId); + + //var user = await UserManager.FindByIdAsync(model.UserId); + var unlockResult = await UserManager.SetLockoutEndDateAsync(model.UserId, DateTimeOffset.Now); + if(unlockResult.Succeeded == false) + { + Logger.Warn("Could not unlock for user {0} - error {1}", + () => model.UserId, () => unlockResult.Errors.First()); + } + + var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(model.UserId); + if (resetAccessFailedCountResult.Succeeded == false) + { + Logger.Warn("Could not reset access failed count {0} - error {1}", + () => model.UserId, () => unlockResult.Errors.First()); + } + } + return Request.CreateResponse(HttpStatusCode.OK); } - return Request.CreateValidationErrorResponse( result.Errors.Any() ? result.Errors.First() : "Set password failed"); } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 728ebb3f1e..4a0c318d20 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -388,6 +388,10 @@ namespace Umbraco.Web.Editors { "disallowedUploadFiles", string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles) + }, + { + "allowedUploadFiles", + string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.AllowedUploadFiles) }, { "maxFileSize", diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 4cac919b1b..77be2e2c37 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -33,7 +33,8 @@ using Umbraco.Core.Configuration; using Umbraco.Web.UI; using Notification = Umbraco.Web.Models.ContentEditing.Notification; using Umbraco.Core.Persistence; - +using Umbraco.Core.Configuration.UmbracoSettings; + namespace Umbraco.Web.Editors { /// @@ -723,7 +724,7 @@ namespace Umbraco.Web.Editors var safeFileName = fileName.ToSafeFileName(); var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); - if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) + if (UmbracoConfig.For.UmbracoSettings().Content.IsFileAllowedForUpload(ext)) { var mediaType = Constants.Conventions.MediaTypes.File; diff --git a/src/Umbraco.Web/Extensions/UdiExtensions.cs b/src/Umbraco.Web/Extensions/UdiExtensions.cs index c814d63b8c..3302f5538a 100644 --- a/src/Umbraco.Web/Extensions/UdiExtensions.cs +++ b/src/Umbraco.Web/Extensions/UdiExtensions.cs @@ -1,23 +1,22 @@ -using Umbraco.Core; +using System; +using System.ComponentModel; +using Umbraco.Core; using Umbraco.Core.Models; namespace Umbraco.Web.Extensions { + [Obsolete("Use methods on UmbracoHelper instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static class UdiExtensions { - /// - /// An extension method to easily acquire a typed version of content, media or member item for a given Udi - /// - /// - /// An item if the item is a Document, Media or Member + [Obsolete("Use methods on UmbracoHelper instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static IPublishedContent ToPublishedContent(this Udi udi) { - Udi identifier; - if (Udi.TryParse(udi.ToString(), out identifier) == false) - return null; + var guidUdi = udi as GuidUdi; + if (guidUdi == null) return null; - var guidUdi = GuidUdi.Parse(udi.ToString()); - var umbracoType = Constants.UdiEntityType.ToUmbracoObjectType(identifier.EntityType); + var umbracoType = Constants.UdiEntityType.ToUmbracoObjectType(guidUdi.EntityType); var umbracoHelper = new UmbracoHelper(UmbracoContext.Current); var entityService = ApplicationContext.Current.Services.EntityService; @@ -36,7 +35,7 @@ namespace Umbraco.Web.Extensions return umbracoHelper.TypedMember(memberAttempt.Result); break; } - + return null; } } diff --git a/src/Umbraco.Web/ITypedPublishedContentQuery.cs b/src/Umbraco.Web/ITypedPublishedContentQuery.cs index 7a2be964f7..893c036958 100644 --- a/src/Umbraco.Web/ITypedPublishedContentQuery.cs +++ b/src/Umbraco.Web/ITypedPublishedContentQuery.cs @@ -11,8 +11,20 @@ namespace Umbraco.Web /// public interface ITypedPublishedContentQuery { + /// + /// Gets a content item from the cache + /// + /// + /// IPublishedContent TypedContent(int id); + + /// + /// Gets a content item from the cache + /// + /// + /// IPublishedContent TypedContent(Guid id); + IPublishedContent TypedContentSingleAtXPath(string xpath, params XPathVariable[] vars); IEnumerable TypedContent(IEnumerable ids); IEnumerable TypedContent(IEnumerable ids); @@ -20,8 +32,8 @@ namespace Umbraco.Web IEnumerable TypedContentAtXPath(XPathExpression xpath, params XPathVariable[] vars); IEnumerable TypedContentAtRoot(); - // note: we CANNOT implement TypedMedia by Guid in v7 without break-changing IPublishedCache, - // since we don't support XPath navigation of the media tree. + // TODO: we CANNOT implement TypedMedia by Guid in v7 without break-changing IPublishedCache, since we don't support XPath navigation of the media tree. + // surely there is a way we can support this without XPath, it's needed so we can query properly by UDI IPublishedContent TypedMedia(int id); //IPublishedContent TypedMedia(Guid id); diff --git a/src/Umbraco.Web/Models/DetachedPublishedContent.cs b/src/Umbraco.Web/Models/DetachedPublishedContent.cs new file mode 100644 index 0000000000..b088d9be3c --- /dev/null +++ b/src/Umbraco.Web/Models/DetachedPublishedContent.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.Models +{ + public class DetachedPublishedContent : PublishedContentWithKeyBase + { + private readonly Guid _key; + private readonly string _name; + private readonly PublishedContentType _contentType; + private readonly IEnumerable _properties; + private readonly int _sortOrder; + private readonly bool _isPreviewing; + private readonly IPublishedContent _containerNode; + + public DetachedPublishedContent( + Guid key, + string name, + PublishedContentType contentType, + IEnumerable properties, + IPublishedContent containerNode = null, + int sortOrder = 0, + bool isPreviewing = false) + { + _key = key; + _name = name; + _contentType = contentType; + _properties = properties; + _sortOrder = sortOrder; + _isPreviewing = isPreviewing; + _containerNode = containerNode; + } + + public override Guid Key + { + get { return _key; } + } + + public override int Id + { + get { return 0; } + } + + public override string Name + { + get { return _name; } + } + + public override bool IsDraft + { + get { return _isPreviewing; } + } + + public override PublishedItemType ItemType + { + get { return PublishedItemType.Content; } + } + + public override PublishedContentType ContentType + { + get { return _contentType; } + } + + public override string DocumentTypeAlias + { + get { return _contentType.Alias; } + } + + public override int DocumentTypeId + { + get { return _contentType.Id; } + } + + public override ICollection Properties + { + get { return _properties.ToArray(); } + } + + public override IPublishedProperty GetProperty(string alias) + { + return _properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); + } + + public override IPublishedProperty GetProperty(string alias, bool recurse) + { + if (recurse) + throw new NotSupportedException(); + + return GetProperty(alias); + } + + public override IPublishedContent Parent + { + get { return null; } + } + + public override IEnumerable Children + { + get { return Enumerable.Empty(); } + } + + public override int TemplateId + { + get { return 0; } + } + + public override int SortOrder + { + get { return _sortOrder; } + } + + public override string UrlName + { + get { return null; } + } + + public override string WriterName + { + get { return _containerNode != null ? _containerNode.WriterName : null; } + } + + public override string CreatorName + { + get { return _containerNode != null ? _containerNode.CreatorName : null; } + } + + public override int WriterId + { + get { return _containerNode != null ? _containerNode.WriterId : 0; } + } + + public override int CreatorId + { + get { return _containerNode != null ? _containerNode.CreatorId : 0; } + } + + public override string Path + { + get { return null; } + } + + public override DateTime CreateDate + { + get { return _containerNode != null ? _containerNode.CreateDate : DateTime.MinValue; } + } + + public override DateTime UpdateDate + { + get { return _containerNode != null ? _containerNode.UpdateDate : DateTime.MinValue; } + } + + public override Guid Version + { + get { return _containerNode != null ? _containerNode.Version : Guid.Empty; } + } + + public override int Level + { + get { return 0; } + } + } +} diff --git a/src/Umbraco.Web/Models/DetachedPublishedProperty.cs b/src/Umbraco.Web/Models/DetachedPublishedProperty.cs new file mode 100644 index 0000000000..4a76942dd6 --- /dev/null +++ b/src/Umbraco.Web/Models/DetachedPublishedProperty.cs @@ -0,0 +1,52 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.Models +{ + internal class DetachedPublishedProperty : IPublishedProperty + { + private readonly PublishedPropertyType _propertyType; + private readonly object _rawValue; + private readonly Lazy _sourceValue; + private readonly Lazy _objectValue; + private readonly Lazy _xpathValue; + private readonly bool _isPreview; + + public DetachedPublishedProperty(PublishedPropertyType propertyType, object value) + : this(propertyType, value, false) + { + } + + public DetachedPublishedProperty(PublishedPropertyType propertyType, object value, bool isPreview) + { + _propertyType = propertyType; + _isPreview = isPreview; + + _rawValue = value; + + _sourceValue = new Lazy(() => _propertyType.ConvertDataToSource(_rawValue, _isPreview)); + _objectValue = new Lazy(() => _propertyType.ConvertSourceToObject(_sourceValue.Value, _isPreview)); + _xpathValue = new Lazy(() => _propertyType.ConvertSourceToXPath(_sourceValue.Value, _isPreview)); + } + + public string PropertyTypeAlias + { + get + { + return _propertyType.PropertyTypeAlias; + } + } + + public bool HasValue + { + get { return DataValue != null && DataValue.ToString().Trim().Length > 0; } + } + + public object DataValue { get { return _rawValue; } } + + public object Value { get { return _objectValue.Value; } } + + public object XPathValue { get { return _xpathValue.Value; } } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentController.cs b/src/Umbraco.Web/PropertyEditors/NestedContentController.cs new file mode 100644 index 0000000000..3e8b0027fd --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/NestedContentController.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Web.Editors; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.PropertyEditors +{ + [PluginController("UmbracoApi")] + public class NestedContentController : UmbracoAuthorizedJsonController + { + [System.Web.Http.HttpGet] + public IEnumerable GetContentTypes() + { + return Services.ContentTypeService.GetAllContentTypes() + .OrderBy(x => x.SortOrder) + .Select(x => new + { + id = x.Id, + guid = x.Key, + name = x.Name, + alias = x.Alias, + icon = x.Icon, + tabs = x.CompositionPropertyGroups.Select(y => y.Name).Distinct() + }); + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentHelper.cs b/src/Umbraco.Web/PropertyEditors/NestedContentHelper.cs new file mode 100644 index 0000000000..079a2e33a4 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/NestedContentHelper.cs @@ -0,0 +1,131 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.PropertyEditors +{ + internal static class NestedContentHelper + { + private const string CacheKeyPrefix = "Umbraco.Web.PropertyEditors.NestedContent.GetPreValuesCollectionByDataTypeId_"; + + public static PreValueCollection GetPreValuesCollectionByDataTypeId(int dtdId) + { + var preValueCollection = (PreValueCollection)ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( + string.Concat(CacheKeyPrefix, dtdId), + () => ApplicationContext.Current.Services.DataTypeService.GetPreValuesCollectionByDataTypeId(dtdId)); + + return preValueCollection; + } + + public static void ClearCache(int id) + { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem( + string.Concat(CacheKeyPrefix, id)); + } + + public static string GetContentTypeAliasFromItem(JObject item) + { + var contentTypeAliasProperty = item[NestedContentPropertyEditor.ContentTypeAliasPropertyKey]; + if (contentTypeAliasProperty == null) + { + return null; + } + + return contentTypeAliasProperty.ToObject(); + } + + public static IContentType GetContentTypeFromItem(JObject item) + { + var contentTypeAlias = GetContentTypeAliasFromItem(item); + if (string.IsNullOrEmpty(contentTypeAlias)) + { + return null; + } + + return ApplicationContext.Current.Services.ContentTypeService.GetContentType(contentTypeAlias); + } + + #region Conversion from v0.1.1 data formats + + public static void ConvertItemValueFromV011(JObject item, int dtdId, ref PreValueCollection preValues) + { + var contentTypeAlias = GetContentTypeAliasFromItem(item); + if (contentTypeAlias != null) + { + // the item is already in >v0.1.1 format + return; + } + + // old style (v0.1.1) data, let's attempt a conversion + // - get the prevalues (if they're not loaded already) + preValues = preValues ?? GetPreValuesCollectionByDataTypeId(dtdId); + + // - convert the prevalues (if necessary) + ConvertPreValueCollectionFromV011(preValues); + + // - get the content types prevalue as JArray + var preValuesAsDictionary = preValues.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value); + if (!preValuesAsDictionary.ContainsKey(ContentTypesPreValueKey) || string.IsNullOrEmpty(preValuesAsDictionary[ContentTypesPreValueKey]) != false) + { + return; + } + + var preValueContentTypes = JArray.Parse(preValuesAsDictionary[ContentTypesPreValueKey]); + if (preValueContentTypes.Any()) + { + // the only thing we can really do is assume that the item is the first available content type + item[NestedContentPropertyEditor.ContentTypeAliasPropertyKey] = preValueContentTypes.First().Value("ncAlias"); + } + } + + public static void ConvertPreValueCollectionFromV011(PreValueCollection preValueCollection) + { + if (preValueCollection == null) + { + return; + } + + var persistedPreValuesAsDictionary = preValueCollection.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value); + + // do we have a "docTypeGuid" prevalue and no "contentTypes" prevalue? + if (persistedPreValuesAsDictionary.ContainsKey("docTypeGuid") == false || persistedPreValuesAsDictionary.ContainsKey(ContentTypesPreValueKey)) + { + // the prevalues are already in >v0.1.1 format + return; + } + + // attempt to parse the doc type guid + Guid guid; + if (Guid.TryParse(persistedPreValuesAsDictionary["docTypeGuid"], out guid) == false) + { + // this shouldn't happen... but just in case. + return; + } + + // find the content type + var contentType = ApplicationContext.Current.Services.ContentTypeService.GetAllContentTypes().FirstOrDefault(c => c.Key == guid); + if (contentType == null) + { + return; + } + + // add a prevalue in the format expected by the new (>0.1.1) content type picker/configurator + preValueCollection.PreValuesAsDictionary[ContentTypesPreValueKey] = new PreValue( + string.Format(@"[{{""ncAlias"": ""{0}"", ""ncTabAlias"": ""{1}"", ""nameTemplate"": ""{2}"", }}]", + contentType.Alias, + persistedPreValuesAsDictionary["tabAlias"], + persistedPreValuesAsDictionary["nameTemplate"] + ) + ); + } + + private static string ContentTypesPreValueKey + { + get { return NestedContentPropertyEditor.NestedContentPreValueEditor.ContentTypesPreValueKey; } + } + + #endregion + } +} diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs new file mode 100644 index 0000000000..38944d0a34 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -0,0 +1,413 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.NestedContentAlias, "Nested Content", "nestedcontent", ValueType = "JSON", Group = "lists", Icon = "icon-thumbnail-list")] + public class NestedContentPropertyEditor : PropertyEditor + { + internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; + + private IDictionary _defaultPreValues; + public override IDictionary DefaultPreValues + { + get { return _defaultPreValues; } + set { _defaultPreValues = value; } + } + + public NestedContentPropertyEditor() + { + // Setup default values + _defaultPreValues = new Dictionary + { + {NestedContentPreValueEditor.ContentTypesPreValueKey, ""}, + {"minItems", 0}, + {"maxItems", 0}, + {"confirmDeletes", "1"}, + {"showIcons", "1"} + }; + } + + #region Pre Value Editor + + protected override PreValueEditor CreatePreValueEditor() + { + return new NestedContentPreValueEditor(); + } + + internal class NestedContentPreValueEditor : PreValueEditor + { + internal const string ContentTypesPreValueKey = "contentTypes"; + + [PreValueField(ContentTypesPreValueKey, "Doc Types", "views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html", Description = "Select the doc types to use as the data blueprint.")] + public string[] ContentTypes { get; set; } + + [PreValueField("minItems", "Min Items", "number", Description = "Set the minimum number of items allowed.")] + public string MinItems { get; set; } + + [PreValueField("maxItems", "Max Items", "number", Description = "Set the maximum number of items allowed.")] + public string MaxItems { get; set; } + + [PreValueField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Set whether item deletions should require confirming.")] + public string ConfirmDeletes { get; set; } + + [PreValueField("showIcons", "Show Icons", "boolean", Description = "Set whether to show the items doc type icon in the list.")] + public string ShowIcons { get; set; } + + [PreValueField("hideLabel", "Hide Label", "boolean", Description = "Set whether to hide the editor label and have the list take up the full width of the editor window.")] + public string HideLabel { get; set; } + + public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) + { + // re-format old style (v0.1.1) pre values if necessary + NestedContentHelper.ConvertPreValueCollectionFromV011(persistedPreVals); + + return base.ConvertDbToEditor(defaultPreVals, persistedPreVals); + } + } + + #endregion + + #region Value Editor + + protected override PropertyValueEditor CreateValueEditor() + { + return new NestedContentPropertyValueEditor(base.CreateValueEditor()); + } + + internal class NestedContentPropertyValueEditor : PropertyValueEditorWrapper + { + public NestedContentPropertyValueEditor(PropertyValueEditor wrapped) + : base(wrapped) + { + Validators.Add(new NestedContentValidator()); + } + + internal ServiceContext Services + { + get { return ApplicationContext.Current.Services; } + } + + public override void ConfigureForDisplay(PreValueCollection preValues) + { + base.ConfigureForDisplay(preValues); + + var asDictionary = preValues.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value); + if (asDictionary.ContainsKey("hideLabel")) + { + var boolAttempt = asDictionary["hideLabel"].TryConvertTo(); + if (boolAttempt.Success) + { + HideLabel = boolAttempt.Result; + } + } + } + + #region DB to String + + public override string ConvertDbToString(Property property, PropertyType propertyType, IDataTypeService dataTypeService) + { + // Convert / validate value + if (property.Value == null || string.IsNullOrWhiteSpace(property.Value.ToString())) + return string.Empty; + + var value = JsonConvert.DeserializeObject>(property.Value.ToString()); + if (value == null) + return string.Empty; + + // Process value + PreValueCollection preValues = null; + for (var i = 0; i < value.Count; i++) + { + var o = value[i]; + var propValues = ((JObject)o); + + // convert from old style (v0.1.1) data format if necessary + NestedContentHelper.ConvertItemValueFromV011(propValues, propertyType.DataTypeDefinitionId, ref preValues); + + var contentType = NestedContentHelper.GetContentTypeFromItem(propValues); + if (contentType == null) + { + continue; + } + + var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray(); + + foreach (var propKey in propValueKeys) + { + var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey); + if (propType == null) + { + if (IsSystemPropertyKey(propKey) == false) + { + // Property missing so just delete the value + propValues[propKey] = null; + } + } + else + { + try + { + // Create a fake property using the property abd stored value + var prop = new Property(propType, propValues[propKey] == null ? null : propValues[propKey].ToString()); + + // Lookup the property editor + var propEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias); + + // Get the editor to do it's conversion, and store it back + propValues[propKey] = propEditor.ValueEditor.ConvertDbToString(prop, propType, dataTypeService); + } + catch (InvalidOperationException) + { + // https://github.com/umco/umbraco-nested-content/issues/111 + // Catch any invalid cast operations as likely means courier failed due to missing + // or trashed item so couldn't convert a guid back to an int + + propValues[propKey] = null; + } + } + + } + } + + // Update the value on the property + property.Value = JsonConvert.SerializeObject(value); + + // Pass the call down + return base.ConvertDbToString(property, propertyType, dataTypeService); + } + + #endregion + + #region DB to Editor + + public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService) + { + if (property.Value == null || string.IsNullOrWhiteSpace(property.Value.ToString())) + return string.Empty; + + var value = JsonConvert.DeserializeObject>(property.Value.ToString()); + if (value == null) + return string.Empty; + + // Process value + PreValueCollection preValues = null; + for (var i = 0; i < value.Count; i++) + { + var o = value[i]; + var propValues = ((JObject)o); + + // convert from old style (v0.1.1) data format if necessary + NestedContentHelper.ConvertItemValueFromV011(propValues, propertyType.DataTypeDefinitionId, ref preValues); + + var contentType = NestedContentHelper.GetContentTypeFromItem(propValues); + if (contentType == null) + { + continue; + } + + var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray(); + + foreach (var propKey in propValueKeys) + { + var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey); + if (propType == null) + { + if (IsSystemPropertyKey(propKey) == false) + { + // Property missing so just delete the value + propValues[propKey] = null; + } + } + else + { + try + { + // Create a fake property using the property and stored value + var prop = new Property(propType, propValues[propKey] == null ? null : propValues[propKey].ToString()); + + // Lookup the property editor + var propEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias); + + // Get the editor to do it's conversion + var newValue = propEditor.ValueEditor.ConvertDbToEditor(prop, propType, dataTypeService); + + // Store the value back + propValues[propKey] = (newValue == null) ? null : JToken.FromObject(newValue); + } + catch (InvalidOperationException) + { + // https://github.com/umco/umbraco-nested-content/issues/111 + // Catch any invalid cast operations as likely means courier failed due to missing + // or trashed item so couldn't convert a guid back to an int + + propValues[propKey] = null; + } + } + + } + } + + // Update the value on the property + property.Value = JsonConvert.SerializeObject(value); + + // Pass the call down + return base.ConvertDbToEditor(property, propertyType, dataTypeService); + } + + #endregion + + #region Editor to DB + + public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) + { + if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) + return null; + + var value = JsonConvert.DeserializeObject>(editorValue.Value.ToString()); + if (value == null) + return null; + + // Issue #38 - Keep recursive property lookups working + if (!value.Any()) + return null; + + // Process value + for (var i = 0; i < value.Count; i++) + { + var o = value[i]; + var propValues = ((JObject)o); + + var contentType = NestedContentHelper.GetContentTypeFromItem(propValues); + if (contentType == null) + { + continue; + } + + var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray(); + + foreach (var propKey in propValueKeys) + { + var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey); + if (propType == null) + { + if (IsSystemPropertyKey(propKey) == false) + { + // Property missing so just delete the value + propValues[propKey] = null; + } + } + else + { + // Fetch the property types prevalue + var propPreValues = Services.DataTypeService.GetPreValuesCollectionByDataTypeId( + propType.DataTypeDefinitionId); + + // Lookup the property editor + var propEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias); + + // Create a fake content property data object + var contentPropData = new ContentPropertyData( + propValues[propKey], propPreValues, + new Dictionary()); + + // Get the property editor to do it's conversion + var newValue = propEditor.ValueEditor.ConvertEditorToDb(contentPropData, propValues[propKey]); + + // Store the value back + propValues[propKey] = (newValue == null) ? null : JToken.FromObject(newValue); + } + + } + } + + return JsonConvert.SerializeObject(value); + } + + #endregion + } + + internal class NestedContentValidator : IPropertyValidator + { + public IEnumerable Validate(object rawValue, PreValueCollection preValues, PropertyEditor editor) + { + var value = JsonConvert.DeserializeObject>(rawValue.ToString()); + if (value == null) + yield break; + + IDataTypeService dataTypeService = ApplicationContext.Current.Services.DataTypeService; + for (var i = 0; i < value.Count; i++) + { + var o = value[i]; + var propValues = ((JObject)o); + + var contentType = NestedContentHelper.GetContentTypeFromItem(propValues); + if (contentType == null) + { + continue; + } + + var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray(); + + foreach (var propKey in propValueKeys) + { + var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey); + if (propType != null) + { + PreValueCollection propPrevalues = dataTypeService.GetPreValuesCollectionByDataTypeId(propType.DataTypeDefinitionId); + PropertyEditor propertyEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias); + + foreach (IPropertyValidator validator in propertyEditor.ValueEditor.Validators) + { + foreach (ValidationResult result in validator.Validate(propValues[propKey], propPrevalues, propertyEditor)) + { + result.ErrorMessage = "Item " + (i + 1) + " '" + propType.Name + "' " + result.ErrorMessage; + yield return result; + } + } + + // Check mandatory + if (propType.Mandatory) + { + if (propValues[propKey] == null) + yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be null", new[] { propKey }); + else if (propValues[propKey].ToString().IsNullOrWhiteSpace()) + yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be empty", new[] { propKey }); + } + + // Check regex + if (!propType.ValidationRegExp.IsNullOrWhiteSpace() + && propValues[propKey] != null && !propValues[propKey].ToString().IsNullOrWhiteSpace()) + { + var regex = new Regex(propType.ValidationRegExp); + if (!regex.IsMatch(propValues[propKey].ToString())) + { + yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' is invalid, it does not match the correct pattern", new[] { propKey }); + } + } + } + } + } + } + } + + #endregion + + private static bool IsSystemPropertyKey(string propKey) + { + return propKey == "name" || propKey == "key" || propKey == ContentTypeAliasPropertyKey; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs index 96a1211589..8a96c7ac3e 100644 --- a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs +++ b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs @@ -9,7 +9,8 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using umbraco; - +using Umbraco.Core.Configuration.UmbracoSettings; + namespace Umbraco.Web.PropertyEditors { internal class UploadFileTypeValidator : IPropertyValidator @@ -41,8 +42,9 @@ namespace Umbraco.Web.PropertyEditors internal static bool ValidateFileExtension(string fileName) { if (fileName.IndexOf('.') <= 0) return false; - var extension = Path.GetExtension(fileName).TrimStart("."); - return UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Any(x => StringExtensions.InvariantEquals(x, extension)) == false; + var extension = Path.GetExtension(fileName).TrimStart("."); + + return UmbracoConfig.For.UmbracoSettings().Content.IsFileAllowedForUpload(extension); } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerPropertyConverter.cs index ca147698ec..9876868af1 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerPropertyConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ContentPickerPropertyConverter.cs @@ -50,10 +50,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// public override bool IsConverter(PublishedPropertyType propertyType) { + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.ContentPicker2Alias)) + return true; + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) { - return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.ContentPickerAlias) - || propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.ContentPicker2Alias); + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.ContentPickerAlias); } return false; } @@ -106,25 +108,26 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return null; } - if (UmbracoContext.Current != null) + if (UmbracoContext.Current == null) return source; + + if ((propertyType.PropertyTypeAlias != null && PropertiesToExclude.Contains(propertyType.PropertyTypeAlias.ToLower(CultureInfo.InvariantCulture))) == false) { - if ((propertyType.PropertyTypeAlias != null && PropertiesToExclude.Contains(propertyType.PropertyTypeAlias.ToLower(CultureInfo.InvariantCulture))) == false) + IPublishedContent content; + if (source is int) { - IPublishedContent content; - if (source is int) - { - var sourceInt = (int)source; - content = UmbracoContext.Current.ContentCache.GetById(sourceInt); - if(content != null) - return content; - } - else - { - var sourceUdi = source as Udi; - content = sourceUdi.ToPublishedContent(); - if (content != null) - return content; - } + var sourceInt = (int)source; + content = UmbracoContext.Current.ContentCache.GetById(sourceInt); + if(content != null) + return content; + } + else + { + var sourceUdi = source as Udi; + if (sourceUdi == null) return null; + var umbHelper = new UmbracoHelper(UmbracoContext.Current); + content = umbHelper.TypedContent(sourceUdi); + if (content != null) + return content; } } return source; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/LegacyRelatedLinksEditorValueConvertor.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/LegacyRelatedLinksEditorValueConvertor.cs index ee64bf6c15..df4a4356b6 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/LegacyRelatedLinksEditorValueConvertor.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/LegacyRelatedLinksEditorValueConvertor.cs @@ -18,16 +18,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] public class LegacyRelatedLinksEditorValueConvertor : PropertyValueConverterBase { - private static readonly string[] MatchingEditors = { - Constants.PropertyEditors.RelatedLinksAlias, - Constants.PropertyEditors.RelatedLinks2Alias - }; public override bool IsConverter(PublishedPropertyType propertyType) { if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters == false) { - return MatchingEditors.Contains(propertyType.PropertyEditorAlias); + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.RelatedLinksAlias); } return false; } @@ -42,37 +38,38 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters try { var obj = JsonConvert.DeserializeObject(sourceString); + //update the internal links if we have a context - if (UmbracoContext.Current != null) + if (UmbracoContext.Current == null) return obj; + + var helper = new UmbracoHelper(UmbracoContext.Current); + foreach (var a in obj) { - var helper = new UmbracoHelper(UmbracoContext.Current); - foreach (var a in obj) + var type = a.Value("type"); + if (type.IsNullOrWhiteSpace() == false) { - var type = a.Value("type"); - if (type.IsNullOrWhiteSpace() == false) + if (type == "internal") { - if (type == "internal") + switch (propertyType.PropertyEditorAlias) { - switch (propertyType.PropertyEditorAlias) - { - case Constants.PropertyEditors.RelatedLinksAlias: - var intLinkId = a.Value("link"); - var intLink = helper.NiceUrl(intLinkId); - a["link"] = intLink; - break; - case Constants.PropertyEditors.RelatedLinks2Alias: - var strLinkId = a.Value("link"); - var udiAttempt = strLinkId.TryConvertTo(); - if (udiAttempt) - { - var content = udiAttempt.Result.ToPublishedContent(); - a["link"] = helper.NiceUrl(content.Id); - } - break; - } - } + case Constants.PropertyEditors.RelatedLinksAlias: + var intLinkId = a.Value("link"); + var intLink = helper.NiceUrl(intLinkId); + a["link"] = intLink; + break; + case Constants.PropertyEditors.RelatedLinks2Alias: + var strLinkId = a.Value("link"); + var udiAttempt = strLinkId.TryConvertTo(); + if (udiAttempt) + { + var content = helper.TypedContent(udiAttempt.Result); + if (content == null) break; + a["link"] = helper.NiceUrl(content.Id); + } + break; + } } - } + } } return obj; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerPropertyConverter.cs index 4b3db67025..5f54363975 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerPropertyConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerPropertyConverter.cs @@ -12,6 +12,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; @@ -122,7 +123,14 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// public override bool IsConverter(PublishedPropertyType propertyType) { - return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MediaPicker2Alias); + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MediaPicker2Alias)) + return true; + + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) + { + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MediaPickerAlias); + } + return false; } /// @@ -173,11 +181,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var udis = (Udi[])source; var mediaItems = new List(); + if (UmbracoContext.Current == null) return source; + var helper = new UmbracoHelper(UmbracoContext.Current); if (udis.Any()) { foreach (var udi in udis) { - var item = udi.ToPublishedContent(); + var item = helper.TypedMedia(udi); if (item != null) mediaItems.Add(item); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerPropertyConverter.cs index 4467c464f5..e89a5c1a09 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerPropertyConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MemberPickerPropertyConverter.cs @@ -17,10 +17,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { public override bool IsConverter(PublishedPropertyType propertyType) { + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MemberPicker2Alias)) + return true; + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) { - return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.MemberPickerAlias) - || propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.MemberPicker2Alias); + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MemberPickerAlias); } return false; } @@ -41,23 +43,22 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (source == null) return null; - if (UmbracoContext.Current != null) + if (UmbracoContext.Current == null) return source; + + var umbracoHelper = new UmbracoHelper(UmbracoContext.Current); + IPublishedContent member; + if (source is int) + { + member = umbracoHelper.TypedMember((int)source); + if (member != null) + return member; + } + else { - IPublishedContent member; - if (source is int) - { - var membershipHelper = new MembershipHelper(UmbracoContext.Current); - member = membershipHelper.GetById((int)source); - if (member != null) - return member; - } - else - { - var sourceUdi = source as Udi; - member = sourceUdi.ToPublishedContent(); - if (member != null) - return member; - } + var sourceUdi = source as Udi; + member = umbracoHelper.TypedMember(sourceUdi); + if (member != null) + return member; } return source; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs index c0c0321e91..249b7e0cac 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiNodeTreePickerPropertyConverter.cs @@ -52,10 +52,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// public override bool IsConverter(PublishedPropertyType propertyType) { + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePicker2Alias)) + return true; + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) { - return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePickerAlias) - || propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePicker2Alias); + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePickerAlias); } return false; } @@ -119,68 +121,67 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } //TODO: Inject an UmbracoHelper and create a GetUmbracoHelper method based on either injected or singleton - if (UmbracoContext.Current != null) + if (UmbracoContext.Current == null) return source; + + var umbHelper = new UmbracoHelper(UmbracoContext.Current); + + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePickerAlias)) { - if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePickerAlias)) + var nodeIds = (int[])source; + + if ((propertyType.PropertyTypeAlias != null && PropertiesToExclude.InvariantContains(propertyType.PropertyTypeAlias)) == false) { - var nodeIds = (int[])source; + var multiNodeTreePicker = new List(); - if ((propertyType.PropertyTypeAlias != null && PropertiesToExclude.InvariantContains(propertyType.PropertyTypeAlias)) == false) + var objectType = UmbracoObjectTypes.Unknown; + + foreach (var nodeId in nodeIds) { - var multiNodeTreePicker = new List(); + var multiNodeTreePickerItem = + GetPublishedContent(nodeId, ref objectType, UmbracoObjectTypes.Document, umbHelper.TypedContent) + ?? GetPublishedContent(nodeId, ref objectType, UmbracoObjectTypes.Media, umbHelper.TypedMedia) + ?? GetPublishedContent(nodeId, ref objectType, UmbracoObjectTypes.Member, umbHelper.TypedMember); - if (nodeIds.Length > 0) + if (multiNodeTreePickerItem != null) { - var umbHelper = new UmbracoHelper(UmbracoContext.Current); - var objectType = UmbracoObjectTypes.Unknown; - - foreach (var nodeId in nodeIds) - { - var multiNodeTreePickerItem = - GetPublishedContent(nodeId, ref objectType, UmbracoObjectTypes.Document, umbHelper.TypedContent) - ?? GetPublishedContent(nodeId, ref objectType, UmbracoObjectTypes.Media, umbHelper.TypedMedia) - ?? GetPublishedContent(nodeId, ref objectType, UmbracoObjectTypes.Member, umbHelper.TypedMember); - - if (multiNodeTreePickerItem != null) - { - multiNodeTreePicker.Add(multiNodeTreePickerItem); - } - } + multiNodeTreePicker.Add(multiNodeTreePickerItem); } - - return multiNodeTreePicker; } - // return the first nodeId as this is one of the excluded properties that expects a single id - return nodeIds.FirstOrDefault(); + return multiNodeTreePicker; } - if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePicker2Alias)) + // return the first nodeId as this is one of the excluded properties that expects a single id + return nodeIds.FirstOrDefault(); + } + + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiNodeTreePicker2Alias)) + { + var udis = (Udi[])source; + + if ((propertyType.PropertyTypeAlias != null && PropertiesToExclude.InvariantContains(propertyType.PropertyTypeAlias)) == false) { - var udis = (Udi[])source; + var multiNodeTreePicker = new List(); - if ((propertyType.PropertyTypeAlias != null && PropertiesToExclude.InvariantContains(propertyType.PropertyTypeAlias)) == false) + var objectType = UmbracoObjectTypes.Unknown; + + foreach (var udi in udis) { - var multiNodeTreePicker = new List(); - - if (udis.Length > 0) + var multiNodeTreePickerItem = + GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Document, umbHelper.TypedContent) + ?? GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Media, umbHelper.TypedMedia) + ?? GetPublishedContent(udi, ref objectType, UmbracoObjectTypes.Member, umbHelper.TypedMember); + if (multiNodeTreePickerItem != null) { - foreach (var udi in udis) - { - var item = udi.ToPublishedContent(); - if (item != null) - { - multiNodeTreePicker.Add(item); - } - } + multiNodeTreePicker.Add(multiNodeTreePickerItem); } - - return multiNodeTreePicker; } - // return the first nodeId as this is one of the excluded properties that expects a single id - return udis.FirstOrDefault(); + return multiNodeTreePicker; } + + // return the first nodeId as this is one of the excluded properties that expects a single id + return udis.FirstOrDefault(); } return source; } @@ -193,7 +194,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// The type of content expected/supported by /// A function to fetch content of type /// The requested content, or null if either it does not exist or does not match - private IPublishedContent GetPublishedContent(int nodeId, ref UmbracoObjectTypes actualType, UmbracoObjectTypes expectedType, Func contentFetcher) + private IPublishedContent GetPublishedContent(T nodeId, ref UmbracoObjectTypes actualType, UmbracoObjectTypes expectedType, Func contentFetcher) { // is the actual type supported by the content fetcher? if (actualType != UmbracoObjectTypes.Unknown && actualType != expectedType) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs new file mode 100644 index 0000000000..c504601d35 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + public class NestedContentManyValueConverter : PropertyValueConverterBase, IPropertyValueConverterMeta + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.IsNestedContentProperty() && !propertyType.IsSingleNestedContentProperty(); + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + try + { + return propertyType.ConvertPropertyToNestedContent(source, preview); + } + catch (Exception e) + { + LogHelper.Error("Error converting value", e); + } + + return null; + } + + public virtual Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(IEnumerable); + } + + public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue) + { + return PropertyCacheLevel.Content; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentPublishedPropertyTypeExtensions.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentPublishedPropertyTypeExtensions.cs new file mode 100644 index 0000000000..cb26ef6786 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentPublishedPropertyTypeExtensions.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + internal static class NestedContentPublishedPropertyTypeExtensions + { + public static bool IsNestedContentProperty(this PublishedPropertyType publishedProperty) + { + return publishedProperty.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.NestedContentAlias); + } + + public static bool IsSingleNestedContentProperty(this PublishedPropertyType publishedProperty) + { + if (!publishedProperty.IsNestedContentProperty()) + { + return false; + } + + var preValueCollection = NestedContentHelper.GetPreValuesCollectionByDataTypeId(publishedProperty.DataTypeId); + var preValueDictionary = preValueCollection.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value); + + int minItems, maxItems; + return preValueDictionary.ContainsKey("minItems") && + int.TryParse(preValueDictionary["minItems"], out minItems) && minItems == 1 + && preValueDictionary.ContainsKey("maxItems") && + int.TryParse(preValueDictionary["maxItems"], out maxItems) && maxItems == 1; + } + + public static object ConvertPropertyToNestedContent(this PublishedPropertyType propertyType, object source, bool preview) + { + using (DisposableTimer.DebugDuration(string.Format("ConvertPropertyToNestedContent ({0})", propertyType.DataTypeId))) + { + if (source != null && !source.ToString().IsNullOrWhiteSpace()) + { + var rawValue = JsonConvert.DeserializeObject>(source.ToString()); + var processedValue = new List(); + + var preValueCollection = NestedContentHelper.GetPreValuesCollectionByDataTypeId(propertyType.DataTypeId); + var preValueDictionary = preValueCollection.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value); + + for (var i = 0; i < rawValue.Count; i++) + { + var item = (JObject)rawValue[i]; + + // Convert from old style (v.0.1.1) data format if necessary + // - Please note: This call has virtually no impact on rendering performance for new style (>v0.1.1). + // Even so, this should be removed eventually, when it's safe to assume that there is + // no longer any need for conversion. + NestedContentHelper.ConvertItemValueFromV011(item, propertyType.DataTypeId, ref preValueCollection); + + var contentTypeAlias = NestedContentHelper.GetContentTypeAliasFromItem(item); + if (string.IsNullOrEmpty(contentTypeAlias)) + { + continue; + } + + var publishedContentType = PublishedContentType.Get(PublishedItemType.Content, contentTypeAlias); + if (publishedContentType == null) + { + continue; + } + + var propValues = item.ToObject>(); + var properties = new List(); + + foreach (var jProp in propValues) + { + var propType = publishedContentType.GetPropertyType(jProp.Key); + if (propType != null) + { + properties.Add(new DetachedPublishedProperty(propType, jProp.Value, preview)); + } + } + + // Parse out the name manually + object nameObj = null; + if (propValues.TryGetValue("name", out nameObj)) + { + // Do nothing, we just want to parse out the name if we can + } + + object keyObj; + var key = Guid.Empty; + if (propValues.TryGetValue("key", out keyObj)) + { + key = Guid.Parse(keyObj.ToString()); + } + + // Get the current request node we are embedded in + var pcr = UmbracoContext.Current == null ? null : UmbracoContext.Current.PublishedContentRequest; + var containerNode = pcr != null && pcr.HasPublishedContent ? pcr.PublishedContent : null; + + // Create the model based on our implementation of IPublishedContent + IPublishedContent content = new DetachedPublishedContent( + key, + nameObj == null ? null : nameObj.ToString(), + publishedContentType, + properties.ToArray(), + containerNode, + i, + preview); + + if (PublishedContentModelFactoryResolver.HasCurrent) + { + // Let the current model factory create a typed model to wrap our model + content = PublishedContentModelFactoryResolver.Current.Factory.CreateModel(content); + } + + // Add the (typed) model as a result + processedValue.Add(content); + } + + if (propertyType.IsSingleNestedContentProperty()) + { + return processedValue.FirstOrDefault(); + } + + return processedValue; + } + } + + return null; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs new file mode 100644 index 0000000000..a7af2fd8ae --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs @@ -0,0 +1,40 @@ +using System; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + public class NestedContentSingleValueConverter : PropertyValueConverterBase, IPropertyValueConverterMeta + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.IsSingleNestedContentProperty(); + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + try + { + return propertyType.ConvertPropertyToNestedContent(source, preview); + } + catch (Exception e) + { + LogHelper.Error("Error converting value", e); + } + + return null; + } + + public virtual Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return typeof(IPublishedContent); + } + + public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue) + { + return PropertyCacheLevel.Content; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs index b95644d9ca..67ff40fed4 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksEditorValueConvertor.cs @@ -53,10 +53,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// public override bool IsConverter(PublishedPropertyType propertyType) { + if (propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.RelatedLinks2Alias)) + return true; + if (UmbracoConfig.For.UmbracoSettings().Content.EnablePropertyValueConverters) { - return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.RelatedLinksAlias) - || propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.RelatedLinks2Alias); + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.RelatedLinksAlias); } return false; } @@ -88,6 +90,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var relatedLinksData = JsonConvert.DeserializeObject>(sourceString); var relatedLinks = new List(); + if (UmbracoContext.Current == null) return source; + + var helper = new UmbracoHelper(UmbracoContext.Current); + foreach (var linkData in relatedLinksData) { var relatedLink = new RelatedLink @@ -111,7 +117,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var udiAttempt = strLinkId.TryConvertTo(); if (udiAttempt.Success) { - var content = udiAttempt.Result.ToPublishedContent(); + var content = helper.TypedContent(udiAttempt.Result); if (content != null) { relatedLink.Id = content.Id; diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs index b5c3d850d2..8decd0100a 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs @@ -12,7 +12,11 @@ namespace Umbraco.Web.PublishedCache /// Provides access to cached contents in a specified context. /// public abstract class ContextualPublishedCache - { + { + //TODO: We need to add: + //* GetById(Guid contentId) + //* GetById(UDI contentId) + protected readonly UmbracoContext UmbracoContext; /// @@ -35,6 +39,18 @@ namespace Umbraco.Web.PublishedCache return GetById(UmbracoContext.InPreviewMode, contentId); } + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on context. + public IPublishedContent GetById(Guid contentId) + { + var intId = UmbracoContext.Application.Services.EntityService.GetIdForKey(contentId, UmbracoObjectTypes.Document); + return GetById(intId.Success ? intId.Result : -1); + } + /// /// Gets a content identified by its unique identifier. /// diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 36d1306ab6..8ce0d5c2db 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -24,6 +24,12 @@ namespace Umbraco.Web public static Guid GetKey(this IPublishedContent content) { + var wrapped = content as PublishedContentWrapped; + while (wrapped != null) + { + content = wrapped.Unwrap(); + wrapped = content as PublishedContentWrapped; + } var contentWithKey = content as IPublishedContentWithKey; return contentWithKey == null ? Guid.Empty : contentWithKey.Key; } diff --git a/src/Umbraco.Web/PublishedContentQueryExtensions.cs b/src/Umbraco.Web/PublishedContentQueryExtensions.cs new file mode 100644 index 0000000000..c40daf5e82 --- /dev/null +++ b/src/Umbraco.Web/PublishedContentQueryExtensions.cs @@ -0,0 +1,23 @@ +using System; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web +{ + public static class PublishedContentQueryExtensions + { + /// + /// Gets a content item from the cache + /// + /// + /// + /// + public static IPublishedContent TypedContent(this ITypedPublishedContentQuery contentQuery, Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi == null) + throw new InvalidOperationException("UDIs for content items must be " + typeof(GuidUdi)); + return contentQuery.TypedContent(guidUdi.Guid); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs index 05b6b0e468..afbfe1c4f6 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs @@ -17,13 +17,14 @@ using umbraco; using umbraco.cms.businesslogic.web; using umbraco.cms.businesslogic.language; using umbraco.cms.businesslogic.member; +using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Security; using RenderingEngine = Umbraco.Core.RenderingEngine; namespace Umbraco.Web.Routing { - internal class PublishedContentRequestEngine + internal class PublishedContentRequestEngine { private readonly PublishedContentRequest _pcr; private readonly RoutingContext _routingContext; @@ -518,51 +519,69 @@ namespace Umbraco.Web.Routing const string tracePrefix = "FollowInternalRedirects: "; if (_pcr.PublishedContent == null) - throw new InvalidOperationException("There is no PublishedContent."); + throw new InvalidOperationException("There is no PublishedContent."); + + // don't try to find a redirect if the property doesn't exist + if (_pcr.PublishedContent.HasProperty(Constants.Conventions.Content.InternalRedirectId) == false) + return false; + + var redirect = false; + IPublishedContent internalRedirectNode = null; + var internalRedirectId = + _pcr.PublishedContent.GetPropertyValue(Constants.Conventions.Content.InternalRedirectId, -1); + var valueValid = false; + if (internalRedirectId > 0) + { + valueValid = true; + // Try and get the redirect node from a legacy integer ID + internalRedirectNode = _routingContext.UmbracoContext.ContentCache.GetById(internalRedirectId); + } + else + { + var udiInternalRedirectId = + _pcr.PublishedContent.GetPropertyValue(Constants.Conventions.Content.InternalRedirectId); + if (udiInternalRedirectId != null) + { + valueValid = true; + // Try and get the redirect node from a UDI Guid + internalRedirectNode = + _routingContext.UmbracoContext.ContentCache.GetById(udiInternalRedirectId.Guid); + } + } + + if (valueValid == false) + { + // bad redirect - log and display the current page (legacy behavior) + ProfilingLogger + .Logger.Debug( + "{0}Failed to redirect, value of '{1}' is not an int nor a GuidUdi", + () => tracePrefix, () => Constants.Conventions.Content.InternalRedirectId); + } - bool redirect = false; - var internalRedirect = _pcr.PublishedContent.GetPropertyValue(Constants.Conventions.Content.InternalRedirectId); + if (internalRedirectNode == null) + { + ProfilingLogger.Logger.Debug( + "{0}Failed to redirect, value of '{1}' does not lead to a published document", () => tracePrefix, + () => Constants.Conventions.Content.InternalRedirectId); + } + else if (internalRedirectNode.Id == _pcr.PublishedContent.Id) + { + // redirect to self + ProfilingLogger.Logger.Debug("{0}Redirecting to self, ignore", + () => tracePrefix); + } + else + { + // Redirect to another page + _pcr.SetInternalRedirectPublishedContent(internalRedirectNode); + redirect = true; + ProfilingLogger.Logger.Debug("{0}Redirecting to id={1}", () => tracePrefix, + () => internalRedirectNode.Id); + } - if (string.IsNullOrWhiteSpace(internalRedirect) == false) - { - ProfilingLogger.Logger.Debug("{0}Found umbracoInternalRedirectId={1}", () => tracePrefix, () => internalRedirect); - - int internalRedirectId; - if (int.TryParse(internalRedirect, out internalRedirectId) == false) - internalRedirectId = -1; - - if (internalRedirectId <= 0) - { - // bad redirect - log and display the current page (legacy behavior) - //_pcr.Document = null; // no! that would be to force a 404 - ProfilingLogger.Logger.Debug("{0}Failed to redirect to id={1}: invalid value", () => tracePrefix, () => internalRedirect); - } - else if (internalRedirectId == _pcr.PublishedContent.Id) - { - // redirect to self - ProfilingLogger.Logger.Debug("{0}Redirecting to self, ignore", () => tracePrefix); - } - else - { - // redirect to another page - var node = _routingContext.UmbracoContext.ContentCache.GetById(internalRedirectId); - - if (node != null) - { - _pcr.SetInternalRedirectPublishedContent(node); // don't use .PublishedContent here - redirect = true; - ProfilingLogger.Logger.Debug("{0}Redirecting to id={1}", () => tracePrefix, () => internalRedirectId); - } - else - { - ProfilingLogger.Logger.Debug("{0}Failed to redirect to id={1}: no such published document", () => tracePrefix, () => internalRedirectId); - } - } - } - - return redirect; + return redirect; } - + /// /// Ensures that access to current node is permitted. /// @@ -719,16 +738,31 @@ namespace Umbraco.Web.Routing /// As per legacy, if the redirect does not work, we just ignore it. private void FollowExternalRedirect() { - if (_pcr.HasPublishedContent == false) return; + if (_pcr.HasPublishedContent == false) return; + + // don't try to find a redirect if the property doesn't exist + if (_pcr.PublishedContent.HasProperty(Constants.Conventions.Content.Redirect) == false) + return; + + var redirectId = _pcr.PublishedContent.GetPropertyValue(Constants.Conventions.Content.Redirect, -1); - var redirectId = _pcr.PublishedContent.GetPropertyValue(Constants.Conventions.Content.Redirect, -1); var redirectUrl = "#"; if (redirectId > 0) - redirectUrl = _routingContext.UrlProvider.GetUrl(redirectId); + { + redirectUrl = _routingContext.UrlProvider.GetUrl(redirectId); + } + else + { + // might be a UDI instead of an int Id + var redirectUdi = _pcr.PublishedContent.GetPropertyValue(Constants.Conventions.Content.Redirect); + if (redirectUdi != null) + redirectUrl = _routingContext.UrlProvider.GetUrl(redirectUdi.Guid); + } if (redirectUrl != "#") + { _pcr.SetRedirect(redirectUrl); + } } - #endregion } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index bf49e335f6..24359d2b50 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -88,6 +88,22 @@ namespace Umbraco.Web.Scheduling } var result = await wc.SendAsync(request, token); + var content = await result.Content.ReadAsStringAsync(); + + if (result.IsSuccessStatusCode) + { + LogHelper.Debug( + () => string.Format( + "Request successfully sent to url = \"{0}\". ", url)); + } + else + { + var msg = string.Format( + "Request failed with status code \"{0}\". Request content = \"{1}\".", + result.StatusCode, content); + var ex = new HttpRequestException(msg); + LogHelper.Error(msg, ex); + } } } catch (Exception e) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 3831fa4b06..ae585cbb57 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -304,8 +304,31 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -368,6 +391,8 @@ + + @@ -406,6 +431,9 @@ + + + @@ -416,6 +444,10 @@ + + + + @@ -485,27 +517,6 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 5befbda7cf..fec4e144fb 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -17,7 +17,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Web.Mvc; -using System.Web.Routing; using Umbraco.Core.Cache; namespace Umbraco.Web @@ -38,6 +37,7 @@ namespace Umbraco.Web private MembershipHelper _membershipHelper; private TagQuery _tag; private IDataTypeService _dataTypeService; + private IEntityService _entityService; private UrlProvider _urlProvider; private ICultureDictionary _cultureDictionary; @@ -113,6 +113,14 @@ namespace Umbraco.Web get { return _dataTypeService ?? (_dataTypeService = UmbracoContext.Application.Services.DataTypeService); } } + /// + /// Lazy instantiates the IEntityService + /// + private IEntityService EntityService + { + get { return _entityService ?? (_entityService = UmbracoContext.Application.Services.EntityService); } + } + /// /// Lazy instantiates the IUmbracoComponentRenderer if not specified in the constructor /// @@ -527,11 +535,23 @@ namespace Umbraco.Web public string UrlAbsolute(int contentId) { return UrlProvider.GetUrl(contentId, true); + } + + #endregion + + #region Members + + public IPublishedContent TypedMember(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi == null) return null; + return TypedMember(guidUdi.Guid); } - #endregion - - #region Members + public IPublishedContent TypedMember(Guid id) + { + return MembershipHelper.GetByProviderKey(id); + } public IPublishedContent TypedMember(object id) { @@ -593,6 +613,9 @@ namespace Umbraco.Web Guid guidId; if (ConvertIdObjectToGuid(id, out guidId)) return ContentQuery.TypedContent(guidId); + Udi udiId; + if (ConvertIdObjectToUdi(id, out udiId)) + return ContentQuery.TypedContent(udiId); return null; } @@ -615,6 +638,16 @@ namespace Umbraco.Web { return ContentQuery.TypedContent(id); } + + /// + /// Gets a content item from the cache + /// + /// + /// + public IPublishedContent TypedContent(Udi id) + { + return ContentQuery.TypedContent(id); + } /// /// Gets a content item from the cache. @@ -907,6 +940,22 @@ namespace Umbraco.Web return false; } + private static bool ConvertIdObjectToUdi(object id, out Udi guidId) + { + var s = id as string; + if (s != null) + { + return Udi.TryParse(s, out guidId); + } + if (id is Udi) + { + guidId = (Udi)id; + return true; + } + guidId = null; + return false; + } + private static bool ConvertIdsObjectToInts(IEnumerable ids, out IEnumerable intIds) { var list = new List(); @@ -937,29 +986,59 @@ namespace Umbraco.Web } guidIds = list; return true; + } + + #endregion + + #region Media + + public IPublishedContent TypedMedia(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi == null) return null; + return TypedMedia(guidUdi.Guid); } - #endregion + public IPublishedContent TypedMedia(Guid id) + { + //TODO: This is horrible but until the media cache properly supports GUIDs we have no choice here and + // currently there won't be any way to add this method correctly to `ITypedPublishedContentQuery` without breaking an interface and adding GUID support for media + + var entityService = UmbracoContext.Application.Services.EntityService; + var mediaAttempt = entityService.GetIdForKey(id, UmbracoObjectTypes.Media); + return mediaAttempt.Success ? ContentQuery.TypedMedia(mediaAttempt.Result) : null; + } + + /// + /// Overloaded method accepting an 'object' type + /// + /// + /// + /// + /// We accept an object type because GetPropertyValue now returns an 'object', we still want to allow people to pass + /// this result in to this method. + /// This method will throw an exception if the value is not of type int or string. + /// + public IPublishedContent TypedMedia(object id) + { + return TypedMediaForObject(id); + } - #region Media - - /// - /// Overloaded method accepting an 'object' type - /// - /// - /// - /// - /// We accept an object type because GetPropertyValue now returns an 'object', we still want to allow people to pass - /// this result in to this method. - /// This method will throw an exception if the value is not of type int or string. - /// - public IPublishedContent TypedMedia(object id) - { + private IPublishedContent TypedMediaForObject(object id) + { int intId; - return ConvertIdObjectToInt(id, out intId) ? ContentQuery.TypedMedia(intId) : null; - } - - public IPublishedContent TypedMedia(int id) + if (ConvertIdObjectToInt(id, out intId)) + return ContentQuery.TypedMedia(intId); + Guid guidId; + if (ConvertIdObjectToGuid(id, out guidId)) + return TypedMedia(guidId); + Udi udiId; + if (ConvertIdObjectToUdi(id, out udiId)) + return TypedMedia(udiId); + return null; + } + + public IPublishedContent TypedMedia(int id) { return ContentQuery.TypedMedia(id); } @@ -1489,5 +1568,10 @@ namespace Umbraco.Web return surfaceRouteParams.EncryptWithMachineKey(); } + public int GetIdForUdi(Udi udi) + { + var udiToIdAttempt = EntityService.GetIdForUdi(udi); + return udiToIdAttempt.Success ? udiToIdAttempt.Result : -1; + } } } diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index a96863ea09..54bb1ac2f1 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -511,7 +511,7 @@ namespace umbraco private void ClearContextCache() { var items = HttpContextItems; - if (items == null || items.Contains(XmlContextContentItemKey)) return; + if (items == null || items.Contains(XmlContextContentItemKey) == false) return; items.Remove(XmlContextContentItemKey); } diff --git a/src/Umbraco.Web/umbraco.presentation/helper.cs b/src/Umbraco.Web/umbraco.presentation/helper.cs index 2bee9d1e3a..ffb6151887 100644 --- a/src/Umbraco.Web/umbraco.presentation/helper.cs +++ b/src/Umbraco.Web/umbraco.presentation/helper.cs @@ -137,12 +137,16 @@ namespace umbraco attributeValue = StateHelper.GetCookieValue(keyName); break; case "#": + if (pageElements == null) + pageElements = GetPageElements(); if (pageElements[keyName] != null) attributeValue = pageElements[keyName].ToString(); else attributeValue = ""; break; case "$": + if (pageElements == null) + pageElements = GetPageElements(); if (pageElements[keyName] != null && pageElements[keyName].ToString() != string.Empty) { attributeValue = pageElements[keyName].ToString(); @@ -190,6 +194,14 @@ namespace umbraco return attributeValue; } + private static IDictionary GetPageElements() + { + IDictionary pageElements = null; + if (HttpContext.Current.Items["pageElements"] != null) + pageElements = (IDictionary)HttpContext.Current.Items["pageElements"]; + return pageElements; + } + [UmbracoWillObsolete("We should really obsolete that one.")] public static string SpaceCamelCasing(string text) { diff --git a/src/umbraco.businesslogic/UmbracoSettings.cs b/src/umbraco.businesslogic/UmbracoSettings.cs index d73ea22844..dea58ab3ae 100644 --- a/src/umbraco.businesslogic/UmbracoSettings.cs +++ b/src/umbraco.businesslogic/UmbracoSettings.cs @@ -270,6 +270,14 @@ namespace umbraco public static IEnumerable DisallowedUploadFiles { get { return UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles; } + } + + /// + /// File types that will be allowed to be uploaded via the content/media upload control + /// + public static IEnumerable AllowedUploadFiles + { + get { return UmbracoConfig.For.UmbracoSettings().Content.AllowedUploadFiles; } } /// diff --git a/src/umbraco.cms/businesslogic/ContentType.cs b/src/umbraco.cms/businesslogic/ContentType.cs index 7f1f3db742..8ffd207cd5 100644 --- a/src/umbraco.cms/businesslogic/ContentType.cs +++ b/src/umbraco.cms/businesslogic/ContentType.cs @@ -611,21 +611,19 @@ namespace umbraco.cms.businesslogic //its own + inherited property types, which is wrong. Once we are able to fully switch to the new api //this should no longer be a problem as the composition always contains a correct list of property types. var result = new Dictionary(); - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = sqlHelper.ExecuteReader( - "select id from cmsPropertyType where contentTypeId = @ctId order by sortOrder", - sqlHelper.CreateParameter("@ctId", Id))) - { - while (dr.Read()) - { - int id = dr.GetInt("id"); - PropertyType pt = PropertyType.GetPropertyType(id); - if (pt != null) - result.Add(pt.Id, pt); - } - } - // Get Property Types from the master content type + var ids = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "select id from cmsPropertyType where contentTypeId = @ctId order by sortOrder", + new {ctId = Id}); + + foreach (var id in ids) + { + var pt = PropertyType.GetPropertyType(id); + if (pt != null) + result.Add(pt.Id, pt); + } + + // Get Property Types from the master content type if (MasterContentTypes.Count > 0) { foreach (var mct in MasterContentTypes) diff --git a/src/umbraco.editorControls/uploadfield/uploadField.cs b/src/umbraco.editorControls/uploadfield/uploadField.cs index 6f795e1ad9..8a62fb3073 100644 --- a/src/umbraco.editorControls/uploadfield/uploadField.cs +++ b/src/umbraco.editorControls/uploadfield/uploadField.cs @@ -9,7 +9,8 @@ using System.Web.UI.WebControls; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using umbraco.interfaces; -using Umbraco.Core; +using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; using Content = umbraco.cms.businesslogic.Content; using Umbraco.Core; @@ -90,8 +91,8 @@ namespace umbraco.editorControls //now check the file type var extension = Path.GetExtension(postedFile.FileName).TrimStart("."); - - return UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Any(x => x.InvariantEquals(extension)) == false; + + return UmbracoConfig.For.UmbracoSettings().Content.IsFileAllowedForUpload(extension); } public string Text