using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// /// Represents a repository for doing CRUD operations for /// public class MediaRepository : ContentRepositoryBase, IMediaRepository { private readonly AppCaches _cache; private readonly IMediaTypeRepository _mediaTypeRepository; private readonly ITagRepository _tagRepository; private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; private readonly IJsonSerializer _serializer; private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; public MediaRepository( IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILoggerFactory loggerFactory, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, Lazy propertyEditorCollection, MediaUrlGeneratorCollection mediaUrlGenerators, DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, IJsonSerializer serializer, IEventAggregator eventAggregator) : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories, dataTypeService, eventAggregator) { _cache = cache; _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); _mediaUrlGenerators = mediaUrlGenerators; _serializer = serializer; _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, loggerFactory.CreateLogger()); } protected override MediaRepository This => this; #region Repository Base protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Media; protected override IMedia PerformGet(int id) { var sql = GetBaseQuery(QueryType.Single) .Where(x => x.NodeId == id) .SelectTop(1); var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : MapDtoToContent(dto); } protected override IEnumerable PerformGetAll(params int[] ids) { var sql = GetBaseQuery(QueryType.Many); if (ids.Any()) sql.WhereIn(x => x.NodeId, ids); return MapDtosToContent(Database.Fetch(sql)); } protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(QueryType.Many); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); sql .OrderBy(x => x.Level) .OrderBy(x => x.SortOrder); return MapDtosToContent(Database.Fetch(sql)); } protected override Sql GetBaseQuery(QueryType queryType) { return GetBaseQuery(queryType); } protected virtual Sql GetBaseQuery(QueryType queryType, bool current = true, bool joinMediaVersion = false) { var sql = SqlContext.Sql(); switch (queryType) { case QueryType.Count: sql = sql.SelectCount(); break; case QueryType.Ids: sql = sql.Select(x => x.NodeId); break; case QueryType.Single: case QueryType.Many: sql = sql.Select(r => r.Select(x => x.NodeDto) .Select(x => x.ContentVersionDto)) // ContentRepositoryBase expects a variantName field to order by name // for now, just return the plain invariant node name .AndSelect(x => Alias(x.Text, "variantName")); break; } sql .From() .InnerJoin().On(left => left.NodeId, right => right.NodeId) .InnerJoin().On(left => left.NodeId, right => right.NodeId); if (joinMediaVersion) sql.InnerJoin().On((left, right) => left.Id == right.Id); sql.Where(x => x.NodeObjectType == NodeObjectTypeId); if (current) sql.Where(x => x.Current); // always get the current version return sql; } protected override Sql GetBaseQuery(bool isCount) { return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); } // ah maybe not, that what's used for eg Exists in base repo protected override string GetBaseWhereClause() { return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; } protected override IEnumerable GetDeleteClauses() { var list = new List { "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup + " SET startContentId = NULL WHERE startContentId = @id", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion + " WHERE id IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" }; return list; } #endregion #region Versions public override IEnumerable GetAllVersions(int nodeId) { var sql = GetBaseQuery(QueryType.Many, current: false) .Where(x => x.NodeId == nodeId) .OrderByDescending(x => x.Current) .AndByDescending(x => x.VersionDate); return MapDtosToContent(Database.Fetch(sql), true); } public override IMedia GetVersion(int versionId) { var sql = GetBaseQuery(QueryType.Single) .Where(x => x.Id == versionId); var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : MapDtoToContent(dto); } public IMedia GetMediaByPath(string mediaPath) { var umbracoFileValue = mediaPath; const string pattern = ".*[_][0-9]+[x][0-9]+[.].*"; var isResized = Regex.IsMatch(mediaPath, pattern); // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" URL. if (isResized) { var underscoreIndex = mediaPath.LastIndexOf('_'); var dotIndex = mediaPath.LastIndexOf('.'); umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); } var sql = GetBaseQuery(QueryType.Single, joinMediaVersion: true) .Where(x => x.Path == umbracoFileValue) .SelectTop(1); var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : MapDtoToContent(dto); } protected override void PerformDeleteVersion(int id, int versionId) { Database.Delete("WHERE versionId = @versionId", new { versionId }); Database.Delete("WHERE versionId = @versionId", new { versionId }); } #endregion #region Persist protected override void PersistNewItem(IMedia entity) { entity.AddingEntity(); // ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? entity.SanitizeEntityPropertiesForXmlStorage(); // create the dto var dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); // derive path and level from parent var parent = GetParentNodeDto(entity.ParentId); var level = parent.Level + 1; // get sort order var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); // persist the node dto var nodeDto = dto.ContentDto.NodeDto; nodeDto.Path = parent.Path; nodeDto.Level = Convert.ToInt16(level); nodeDto.SortOrder = sortOrder; // see if there's a reserved identifier for this unique id // and then either update or insert the node dto var id = GetReservedId(nodeDto.UniqueId); if (id > 0) nodeDto.NodeId = id; else Database.Insert(nodeDto); nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); nodeDto.ValidatePathWithException(); Database.Update(nodeDto); // update entity entity.Id = nodeDto.NodeId; entity.Path = nodeDto.Path; entity.SortOrder = sortOrder; entity.Level = level; // persist the content dto var contentDto = dto.ContentDto; contentDto.NodeId = nodeDto.NodeId; Database.Insert(contentDto); // persist the content version dto // assumes a new version id and version date (modified date) has been set var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = true; Database.Insert(contentVersionDto); entity.VersionId = contentVersionDto.Id; // persist the media version dto var mediaVersionDto = dto.MediaVersionDto; mediaVersionDto.Id = entity.VersionId; Database.Insert(mediaVersionDto); // persist the property data InsertPropertyValues(entity, 0, out _, out _); // set tags SetEntityTags(entity, _tagRepository, _serializer); PersistRelations(entity); OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); entity.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IMedia entity) { // update entity.UpdatingEntity(); // Check if this entity is being moved as a descendant as part of a bulk moving operations. // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. var isMoving = entity.IsMoving(); if (!isMoving) { // ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? entity.SanitizeEntityPropertiesForXmlStorage(); // if parent has changed, get path, level and sort order if (entity.IsPropertyDirty(nameof(entity.ParentId))) { var parent = GetParentNodeDto(entity.ParentId); entity.Path = string.Concat(parent.Path, ",", entity.Id); entity.Level = parent.Level + 1; entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); } } // create the dto var dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); // update the node dto var nodeDto = dto.ContentDto.NodeDto; nodeDto.ValidatePathWithException(); Database.Update(nodeDto); if (!isMoving) { // update the content dto Database.Update(dto.ContentDto); // update the content & media version dtos var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; var mediaVersionDto = dto.MediaVersionDto; contentVersionDto.Current = true; Database.Update(contentVersionDto); Database.Update(mediaVersionDto); // replace the property data ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); SetEntityTags(entity, _tagRepository, _serializer); PersistRelations(entity); } OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); entity.ResetDirtyProperties(); } protected override void PersistDeletedItem(IMedia entity) { // Raise event first else potential FK issues OnUowRemovingEntity(entity); base.PersistDeletedItem(entity); } #endregion #region Recycle Bin public override int RecycleBinId => Cms.Core.Constants.System.RecycleBinMedia; public bool RecycleBinSmells() { var cache = _cache.RuntimeCache; var cacheKey = CacheKeys.MediaRecycleBinCacheKey; // always cache either true or false return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); } #endregion #region Read Repository implementation for Guid keys public IMedia Get(Guid id) { return _mediaByGuidReadRepository.Get(id); } IEnumerable IReadRepository.GetMany(params Guid[] ids) { return _mediaByGuidReadRepository.GetMany(ids); } public bool Exists(Guid id) { return _mediaByGuidReadRepository.Exists(id); } // A reading repository purely for looking up by GUID // TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this private class MediaByGuidReadRepository : EntityRepositoryBase { private readonly MediaRepository _outerRepo; public MediaByGuidReadRepository(MediaRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) { _outerRepo = outerRepo; } protected override IMedia PerformGet(Guid id) { var sql = _outerRepo.GetBaseQuery(QueryType.Single) .Where(x => x.UniqueId == id); var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); if (dto == null) return null; var content = _outerRepo.MapDtoToContent(dto); return content; } protected override IEnumerable PerformGetAll(params Guid[] ids) { var sql = _outerRepo.GetBaseQuery(QueryType.Many); if (ids.Length > 0) sql.WhereIn(x => x.UniqueId, ids); return _outerRepo.MapDtosToContent(Database.Fetch(sql)); } protected override IEnumerable PerformGetByQuery(IQuery query) { throw new InvalidOperationException("This method won't be implemented."); } protected override IEnumerable GetDeleteClauses() { throw new InvalidOperationException("This method won't be implemented."); } protected override void PersistNewItem(IMedia entity) { throw new InvalidOperationException("This method won't be implemented."); } protected override void PersistUpdatedItem(IMedia entity) { throw new InvalidOperationException("This method won't be implemented."); } protected override Sql GetBaseQuery(bool isCount) { throw new InvalidOperationException("This method won't be implemented."); } protected override string GetBaseWhereClause() { throw new InvalidOperationException("This method won't be implemented."); } } #endregion /// public override IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { Sql filterSql = null; if (filter != null) { filterSql = Sql(); foreach (var clause in filter.GetWhereClauses()) filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); } return GetPage(query, pageIndex, pageSize, out totalRecords, x => MapDtosToContent(x), filterSql, ordering); } private IEnumerable MapDtosToContent(List dtos, bool withCache = false) { var temps = new List>(); var contentTypes = new Dictionary(); var content = new Core.Models.Media[dtos.Count]; for (var i = 0; i < dtos.Count; i++) { var dto = dtos[i]; if (withCache) { // if the cache contains the (proper version of the) item, use it var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { content[i] = (Core.Models.Media)cached; continue; } } // else, need to build it // get the content type - the repository is full cache *but* still deep-clones // whatever comes out of it, so use our own local index here to avoid this var contentTypeId = dto.ContentTypeId; if (contentTypes.TryGetValue(contentTypeId, out IMediaType contentType) == false) contentTypes[contentTypeId] = contentType = _mediaTypeRepository.Get(contentTypeId); var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); // need properties var versionId = dto.ContentVersionDto.Id; temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); } // load all properties for all documents from database in 1 query - indexed by version id var properties = GetPropertyCollections(temps); // assign properties foreach (var temp in temps) { temp.Content.Properties = properties[temp.VersionId]; // reset dirty initial properties (U4-1946) temp.Content.ResetDirtyProperties(false); } return content; } private IMedia MapDtoToContent(ContentDto dto) { var contentType = _mediaTypeRepository.Get(dto.ContentTypeId); var media = ContentBaseFactory.BuildEntity(dto, contentType); // get properties - indexed by version id var versionId = dto.ContentVersionDto.Id; var temp = new TempContent(dto.NodeId, versionId, 0, contentType); var properties = GetPropertyCollections(new List> { temp }); media.Properties = properties[versionId]; // reset dirty initial properties (U4-1946) media.ResetDirtyProperties(false); return media; } } }