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;
}
}
}