using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Xml.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { /// /// Represents the Media Service, which is an easy access to operations involving /// public class MediaService : ScopeRepositoryService, IMediaService, IMediaServiceOperations { //Support recursive locks because some of the methods that require locking call other methods that require locking. //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; private readonly MediaFileSystem _mediaFileSystem = FileSystemProviderManager.Current.MediaFileSystem; public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService) : base(provider, repositoryFactory, logger, eventMessagesFactory) { if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); if (userService == null) throw new ArgumentNullException("userService"); _dataTypeService = dataTypeService; _userService = userService; } /// /// Creates an object using the alias of the /// that this Media should based on. /// /// /// Note that using this method will simply return a new IMedia without any identity /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects /// that does not invoke a save operation against the database. /// /// Name of the Media object /// Id of Parent for the new Media item /// Alias of the /// Optional id of the user creating the media item /// public IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0) { var mediaType = FindMediaTypeByAlias(mediaTypeAlias); var media = new Models.Media(name, parentId, mediaType); var parent = GetById(media.ParentId); media.Path = string.Concat(parent.IfNotNull(x => x.Path, media.ParentId.ToString()), ",", media.Id); using (var uow = UowProvider.GetUnitOfWork()) { if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(media, mediaTypeAlias, parentId))) { uow.Commit(); media.WasCancelled = true; return media; } media.CreatorId = userId; uow.Events.Dispatch(Created, this, new NewEventArgs(media, false, mediaTypeAlias, parentId)); Audit(uow, AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); uow.Commit(); return media; } } /// /// Creates an object using the alias of the /// that this Media should based on. /// /// /// Note that using this method will simply return a new IMedia without any identity /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects /// that does not invoke a save operation against the database. /// /// Name of the Media object /// Parent for the new Media item /// Alias of the /// Optional id of the user creating the media item /// public IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0) { if (parent == null) throw new ArgumentNullException("parent"); var mediaType = FindMediaTypeByAlias(mediaTypeAlias); var media = new Models.Media(name, parent, mediaType); media.Path = string.Concat(parent.Path, ",", media.Id); using (var uow = UowProvider.GetUnitOfWork()) { if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(media, mediaTypeAlias, parent))) { uow.Commit(); media.WasCancelled = true; return media; } media.CreatorId = userId; uow.Events.Dispatch(Created, this, new NewEventArgs(media, false, mediaTypeAlias, parent)); Audit(uow, AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); uow.Commit(); return media; } } /// /// Creates an object using the alias of the /// that this Media should based on. /// /// /// This method returns an object that has been persisted to the database /// and therefor has an identity. /// /// Name of the Media object /// Id of Parent for the new Media item /// Alias of the /// Optional id of the user creating the media item /// public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0) { var mediaType = FindMediaTypeByAlias(mediaTypeAlias); var media = new Models.Media(name, parentId, mediaType); using (var uow = UowProvider.GetUnitOfWork()) { //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(media, mediaTypeAlias, parentId))) { uow.Commit(); media.WasCancelled = true; return media; } if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(media))) { uow.Commit(); media.WasCancelled = true; return media; } var repository = RepositoryFactory.CreateMediaRepository(uow); media.CreatorId = userId; repository.AddOrUpdate(media); repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) { repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); } uow.Events.Dispatch(Saved, this, new SaveEventArgs(media, false)); uow.Events.Dispatch(Created, this, new NewEventArgs(media, false, mediaTypeAlias, parentId)); Audit(uow, AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); uow.Commit(); } return media; } /// /// Creates an object using the alias of the /// that this Media should based on. /// /// /// This method returns an object that has been persisted to the database /// and therefor has an identity. /// /// Name of the Media object /// Parent for the new Media item /// Alias of the /// Optional id of the user creating the media item /// public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0) { if (parent == null) throw new ArgumentNullException("parent"); var mediaType = FindMediaTypeByAlias(mediaTypeAlias); var media = new Models.Media(name, parent, mediaType); using (var uow = UowProvider.GetUnitOfWork()) { //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(media, mediaTypeAlias, parent))) { uow.Commit(); media.WasCancelled = true; return media; } if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(media))) { uow.Commit(); media.WasCancelled = true; return media; } var repository = RepositoryFactory.CreateMediaRepository(uow); media.CreatorId = userId; repository.AddOrUpdate(media); repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); // generate preview for blame history? if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) { repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); } uow.Events.Dispatch(Saved, this, new SaveEventArgs(media, false)); uow.Events.Dispatch(Created, this, new NewEventArgs(media, false, mediaTypeAlias, parent)); Audit(uow, AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); uow.Commit(); } return media; } /// /// Gets an object by Id /// /// Id of the Content to retrieve /// public IMedia GetById(int id) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); return repository.Get(id); } } public int CountNotTrashed(string contentTypeAlias = null) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateMediaRepository(uow)) using (var mediaTypeRepository = RepositoryFactory.CreateMediaTypeRepository(uow)) { var mediaTypeId = 0; if (contentTypeAlias.IsNullOrWhiteSpace() == false) { var mediaType = mediaTypeRepository.Get(contentTypeAlias); if (mediaType == null) return 0; mediaTypeId = mediaType.Id; } var query = Query.Builder.Where(media => media.Trashed == false); if (mediaTypeId > 0) { query.Where(media => media.ContentTypeId == mediaTypeId); } return repository.Count(query); } } public int Count(string contentTypeAlias = null) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); return repository.Count(contentTypeAlias); } } public int CountChildren(int parentId, string contentTypeAlias = null) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); return repository.CountChildren(parentId, contentTypeAlias); } } public int CountDescendants(int parentId, string contentTypeAlias = null) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); return repository.CountDescendants(parentId, contentTypeAlias); } } /// /// Gets an object by Id /// /// Ids of the Media to retrieve /// public IEnumerable GetByIds(IEnumerable ids) { if (ids.Any() == false) return Enumerable.Empty(); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); return repository.GetAll(ids.ToArray()); } } /// /// Gets an object by its 'UniqueId' /// /// Guid key of the Media to retrieve /// public IMedia GetById(Guid key) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var query = Query.Builder.Where(x => x.Key == key); return repository.GetByQuery(query).SingleOrDefault(); } } /// /// Gets a collection of objects by Level /// /// The level to retrieve Media from /// An Enumerable list of objects public IEnumerable GetByLevel(int level) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var query = Query.Builder.Where(x => x.Level == level && x.Path.StartsWith("-21") == false); return repository.GetByQuery(query); } } /// /// Gets a specific version of an item. /// /// Id of the version to retrieve /// An item public IMedia GetByVersion(Guid versionId) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); return repository.GetByVersion(versionId); } } /// /// Gets a collection of an objects versions by Id /// /// /// An Enumerable list of objects public IEnumerable GetVersions(int id) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); return repository.GetAllVersions(id); } } /// /// Gets a collection of objects, which are ancestors of the current media. /// /// Id of the to retrieve ancestors for /// An Enumerable list of objects public IEnumerable GetAncestors(int id) { var media = GetById(id); return GetAncestors(media); } /// /// Gets a collection of objects, which are ancestors of the current media. /// /// to retrieve ancestors for /// An Enumerable list of objects public IEnumerable GetAncestors(IMedia media) { var ids = media.Path.Split(',').Where(x => x != "-1" && x != media.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); if (ids.Any() == false) return new List(); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); return repository.GetAll(ids); } } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Children from /// An Enumerable list of objects public IEnumerable GetChildren(int id) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var query = Query.Builder.Where(x => x.ParentId == id); return repository.GetByQuery(query); } } [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy, Direction orderDirection, string filter = "") { long total; var result = GetPagedChildren(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); totalChildren = Convert.ToInt32(total); return result; } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Children from /// Page index (zero based) /// Page size /// Total records query would return without paging /// Field to order by /// Direction to order by /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, string filter = "") { return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Children from /// Page index (zero based) /// Page size /// Total records query would return without paging /// Field to order by /// Direction to order by /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter, null); } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Children from /// Page number /// Page size /// Total records query would return without paging /// Field to order by /// Direction to order by /// Flag to indicate when ordering by system field /// Search text filter /// A list of content type Ids to filter the list by /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter, int[] contentTypeFilter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var query = Query.Builder; // always check for a parent - else it will also get decendants (and then you should use the GetPagedDescendants method) query.Where(x => x.ParentId == id); if (contentTypeFilter != null && contentTypeFilter.Length > 0) { query.Where(x => contentTypeFilter.Contains(x.ContentTypeId)); } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) { filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); } return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); } } [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { long total; var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); totalChildren = Convert.ToInt32(total); return result; } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Descendants from /// Page number /// Page size /// Total records query would return without paging /// Field to order by /// Direction to order by /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Descendants from /// Page number /// Page size /// Total records query would return without paging /// Field to order by /// Direction to order by /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var query = Query.Builder; //if the id is System Root, then just get all if (id != Constants.System.Root) { query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) { filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); } return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); } } /// /// Gets descendants of a object by its Id /// /// Id of the Parent to retrieve descendants from /// An Enumerable flat list of objects public IEnumerable GetDescendants(int id) { var media = GetById(id); if (media == null) { return Enumerable.Empty(); } return GetDescendants(media); } /// /// Gets descendants of a object by its Id /// /// The Parent object to retrieve descendants from /// An Enumerable flat list of objects public IEnumerable GetDescendants(IMedia media) { //This is a check to ensure that the path is correct for this entity to avoid problems like: http://issues.umbraco.org/issue/U4-9336 due to data corruption if (media.ValidatePath() == false) throw new InvalidDataException(string.Format("The content item {0} has an invalid path: {1} with parentID: {2}", media.Id, media.Path, media.ParentId)); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var pathMatch = media.Path + ","; var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != media.Id); return repository.GetByQuery(query); } } /// /// Gets the parent of the current media as an item. /// /// Id of the to retrieve the parent from /// Parent object public IMedia GetParent(int id) { var media = GetById(id); return GetParent(media); } /// /// Gets the parent of the current media as an item. /// /// to retrieve the parent from /// Parent object public IMedia GetParent(IMedia media) { if (media.ParentId == -1 || media.ParentId == -21) return null; return GetById(media.ParentId); } /// /// Gets a collection of objects by the Id of the /// /// Id of the /// An Enumerable list of objects public IEnumerable GetMediaOfMediaType(int id) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var query = Query.Builder.Where(x => x.ContentTypeId == id); return repository.GetByQuery(query); } } /// /// Gets a collection of objects, which reside at the first level / root /// /// An Enumerable list of objects public IEnumerable GetRootMedia() { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var query = Query.Builder.Where(x => x.ParentId == -1); return repository.GetByQuery(query); } } /// /// Gets a collection of an objects, which resides in the Recycle Bin /// /// An Enumerable list of objects public IEnumerable GetMediaInRecycleBin() { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var query = Query.Builder.Where(x => x.Path.Contains("-21")); return repository.GetByQuery(query); } } /// /// Gets an object from the path stored in the 'umbracoFile' property. /// /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) /// 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)); } Func createSql = url => new Sql().Select("*") .From() .InnerJoin() .On(left => left.PropertyTypeId, right => right.Id) .Where(x => x.Alias == "umbracoFile") .Where(x => x.VarChar == url); var sql = createSql(umbracoFileValue); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); // If the stripped-down url returns null, we try again with the original url. // Previously, the function would fail on e.g. "my_x_image.jpg" if (propertyDataDto == null) { sql = createSql(mediaPath); propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); } // If no reults far, try getting from a json value stored in the ntext column query if (propertyDataDto == null) { var ntextQuery = new Sql().Select("*") .From() .InnerJoin() .On(left => left.PropertyTypeId, right => right.Id) .Where(x => x.Alias == "umbracoFile") .Where("dataNtext LIKE @0", "%" + umbracoFileValue + "%"); propertyDataDto = uow.Database.Fetch(ntextQuery).FirstOrDefault(); } // If still no results, try getting from a json value stored in the nvarchar column if (propertyDataDto == null) { var nvarcharQuery = new Sql().Select("*") .From() .InnerJoin() .On(left => left.PropertyTypeId, right => right.Id) .Where(x => x.Alias == "umbracoFile") .Where("dataNvarchar LIKE @0", "%" + umbracoFileValue + "%"); propertyDataDto = uow.Database.Fetch(nvarcharQuery).FirstOrDefault(); } return propertyDataDto == null ? null : GetById(propertyDataDto.NodeId); } } /// /// Checks whether an item has any children /// /// Id of the /// True if the media has any children otherwise False public bool HasChildren(int id) { using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); var query = Query.Builder.Where(x => x.ParentId == id); int count = repository.Count(query); return count > 0; } } /// /// Moves an object to a new location /// /// The to move /// Id of the Media's new Parent /// Id of the User moving the Media public void Move(IMedia media, int parentId, int userId = 0) { //TODO: This all needs to be on the repo layer in one transaction! if (media == null) throw new ArgumentNullException("media"); using (new WriteLock(Locker)) { //This ensures that the correct method is called if this method is used to Move to recycle bin. if (parentId == -21) { MoveToRecycleBin(media, userId); return; } using (var uow = UowProvider.GetUnitOfWork()) { var originalPath = media.Path; if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(new MoveEventInfo(media, originalPath, parentId)), "Moving")) { uow.Commit(); return; } media.ParentId = parentId; if (media.Trashed) { media.ChangeTrashedState(false, parentId); } Save(media, userId, false); //no events! //used to track all the moved entities to be given to the event var moveInfo = new List> { new MoveEventInfo(media, originalPath, parentId) }; //Ensure that relevant properties are updated on children var children = GetChildren(media.Id).ToArray(); if (children.Any()) { var parentPath = media.Path; var parentLevel = media.Level; var parentTrashed = media.Trashed; var updatedDescendants = UpdatePropertiesOnChildren(children, parentPath, parentLevel, parentTrashed, moveInfo); Save(updatedDescendants, userId, false); //no events! } uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, moveInfo.ToArray()), "Moved"); Audit(uow, AuditType.Move, "Move Media performed by user", userId, media.Id); uow.Commit(); } } } /// /// Deletes an object by moving it to the Recycle Bin /// /// The to delete /// Id of the User deleting the Media public void MoveToRecycleBin(IMedia media, int userId = 0) { ((IMediaServiceOperations)this).MoveToRecycleBin(media, userId); } /// /// Permanently deletes an object /// /// /// Please note that this method will completely remove the Media from the database, /// but current not from the file system. /// /// The to delete /// Id of the User deleting the Media Attempt IMediaServiceOperations.Delete(IMedia media, int userId) { //TODO: IT would be much nicer to mass delete all in one trans in the repo level! var evtMsgs = EventMessagesFactory.Get(); using (var uow = UowProvider.GetUnitOfWork()) { var ret = DeleteUow(uow, media, userId, evtMsgs); uow.Commit(); return ret; } } private Attempt DeleteUow(IScopeUnitOfWork uow, IMedia media, int userId, EventMessages evtMsgs) { if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(media, evtMsgs))) { return OperationStatus.Cancelled(evtMsgs); } // delete children first var children = GetChildren(media.Id); foreach (var child in children) { DeleteUow(uow, child, userId, evtMsgs); } var repository = RepositoryFactory.CreateMediaRepository(uow); repository.Delete(media); var args = new DeleteEventArgs(media, false, evtMsgs); uow.Events.Dispatch(Deleted, this, args); Audit(uow, AuditType.Delete, "Delete Media performed by user", userId, media.Id); return OperationStatus.Success(evtMsgs); } /// /// Saves a single object /// /// The to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. Attempt IMediaServiceOperations.Save(IMedia media, int userId, bool raiseEvents) { var evtMsgs = EventMessagesFactory.Get(); using (var uow = UowProvider.GetUnitOfWork()) { if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(media, evtMsgs))) { uow.Commit(); return OperationStatus.Cancelled(evtMsgs); } if (string.IsNullOrWhiteSpace(media.Name)) { throw new ArgumentException("Cannot save media with empty name."); } var repository = RepositoryFactory.CreateMediaRepository(uow); media.CreatorId = userId; repository.AddOrUpdate(media); repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); // generate preview for blame history? if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) { repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); } if (raiseEvents) uow.Events.Dispatch(Saved, this, new SaveEventArgs(media, false, evtMsgs)); Audit(uow, AuditType.Save, "Save Media performed by user", userId, media.Id); uow.Commit(); } return OperationStatus.Success(evtMsgs); } /// /// Saves a collection of objects /// /// Collection of to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. Attempt IMediaServiceOperations.Save(IEnumerable medias, int userId, bool raiseEvents) { var asArray = medias.ToArray(); var evtMsgs = EventMessagesFactory.Get(); using (var uow = UowProvider.GetUnitOfWork()) { if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(asArray, evtMsgs))) { uow.Commit(); return OperationStatus.Cancelled(evtMsgs); } var repository = RepositoryFactory.CreateMediaRepository(uow); foreach (var media in asArray) { media.CreatorId = userId; repository.AddOrUpdate(media); repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); // generate preview for blame history? if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) { repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); } } if (raiseEvents) uow.Events.Dispatch(Saved, this, new SaveEventArgs(asArray, false, evtMsgs)); Audit(uow, AuditType.Save, "Save Media items performed by user", userId, -1); uow.Commit(); } return OperationStatus.Success(evtMsgs); } /// /// Empties the Recycle Bin by deleting all that resides in the bin /// public void EmptyRecycleBin() { using (new WriteLock(Locker)) { var nodeObjectType = new Guid(Constants.ObjectTypes.Media); using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateMediaRepository(uow); var entities = repository.GetEntitiesInRecycleBin().ToDictionary(key => key.Id, val => (IEnumerable)val.Properties); var files = ((MediaRepository)repository).GetFilesInRecycleBinForUploadField(); if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files))) { uow.Commit(); return; } var success = repository.EmptyRecycleBin(); uow.Events.Dispatch(EmptiedRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files, success)); Audit(uow, AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, -21); uow.Commit(); } } } /// /// Deletes all content of the specified types. All Descendants of deleted content that is not of these types is moved to Recycle Bin. /// /// Id of the /// Optional Id of the user issueing the delete operation public void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = 0) { using (new WriteLock(Locker)) using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateMediaRepository(uow); //track the 'root' items of the collection of nodes discovered to delete, we need to use //these items to lookup descendants that are not of this doc type so they can be transfered //to the recycle bin IDictionary rootItems; var mediaToDelete = this.TrackDeletionsForDeleteContentOfTypes(mediaTypeIds, repository, out rootItems).ToArray(); if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(mediaToDelete), "Deleting")) { uow.Commit(); return; } //Determine the items that will need to be recycled (that are children of these content items but not of these content types) var mediaToRecycle = this.TrackTrashedForDeleteContentOfTypes(mediaTypeIds, rootItems, repository); // move each item to the bin starting with the deepest items // nesting uow - technically supported though not ideal foreach (var child in mediaToRecycle.OrderByDescending(x => x.Level)) { MoveToRecycleBinDo(child, userId, true); } foreach (var content in mediaToDelete) { Delete(content, userId); } Audit(uow, AuditType.Delete, string.Format("Delete Media of Types {0} performed by user", string.Join(",", mediaTypeIds)), userId, Constants.System.Root); uow.Commit(); } } /// /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. /// /// This needs extra care and attention as its potentially a dangerous and extensive operation /// Id of the /// Optional id of the user deleting the media public void DeleteMediaOfType(int mediaTypeId, int userId = 0) { DeleteMediaOfTypes(new[] {mediaTypeId}, userId); } /// /// Deletes an object by moving it to the Recycle Bin /// /// The to delete /// Id of the User deleting the Media Attempt IMediaServiceOperations.MoveToRecycleBin(IMedia media, int userId) { return MoveToRecycleBinDo(media, userId, false); } /// /// Deletes an object by moving it to the Recycle Bin /// /// The to delete /// Id of the User deleting the Media /// /// A boolean indicating to ignore this item's descendant list from also being moved to the recycle bin. This is required for the DeleteContentOfTypes method /// because it has already looked up all descendant nodes that will need to be recycled /// TODO: Fix all of this, it will require a reasonable refactor and most of this stuff should be done at the repo level instead of service sub operations /// private Attempt MoveToRecycleBinDo(IMedia media, int userId, bool ignoreDescendants) { if (media == null) throw new ArgumentNullException("media"); var evtMsgs = EventMessagesFactory.Get(); using (new WriteLock(Locker)) { using (var uow = UowProvider.GetUnitOfWork()) { //Hack: this ensures that the entity's path is valid and if not it fixes/persists it //see: http://issues.umbraco.org/issue/U4-9336 media.EnsureValidPath(Logger, entity => GetById(entity.ParentId), QuickUpdate); var originalPath = media.Path; if (uow.Events.DispatchCancelable(Trashing, this, new MoveEventArgs(new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia)), "Trashing")) { uow.Commit(); return OperationStatus.Cancelled(evtMsgs); } var moveInfo = new List> { new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia) }; //get descendents to process of the content item that is being moved to trash - must be done before changing the state below, //must be processed with shallowest levels first var descendants = ignoreDescendants ? Enumerable.Empty() : GetDescendants(media).OrderBy(x => x.Level); //Do the updates for this item var repository = RepositoryFactory.CreateMediaRepository(uow); repository.DeleteContentXml(media); media.ChangeTrashedState(true, Constants.System.RecycleBinMedia); repository.AddOrUpdate(media); //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId foreach (var descendant in descendants) { repository.DeleteContentXml(descendant); descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); } uow.Events.Dispatch(Trashed, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), "Trashed"); Audit(uow, AuditType.Move, "Move Media to Recycle Bin performed by user", userId, media.Id); uow.Commit(); } return OperationStatus.Success(evtMsgs); } } /// /// Permanently deletes an object as well as all of its Children. /// /// /// Please note that this method will completely remove the Media from the database, /// as well as associated media files from the file system. /// /// The to delete /// Id of the User deleting the Media public void Delete(IMedia media, int userId = 0) { ((IMediaServiceOperations)this).Delete(media, userId); } /// /// Permanently deletes versions from an object prior to a specific date. /// This method will never delete the latest version of a content item. /// /// Id of the object to delete versions from /// Latest version date /// Optional Id of the User deleting versions of a Content object public void DeleteVersions(int id, DateTime versionDate, int userId = 0) { using (var uow = UowProvider.GetUnitOfWork()) { DeleteVersions(uow, id, versionDate, userId); uow.Commit(); } } private void DeleteVersions(IScopeUnitOfWork uow, int id, DateTime versionDate, int userId = 0) { if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, dateToRetain: versionDate))) return; var repository = RepositoryFactory.CreateMediaRepository(uow); repository.DeleteVersions(id, versionDate); uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate)); Audit(uow, AuditType.Delete, "Delete Media by version date performed by user", userId, -1); } /// /// Permanently deletes specific version(s) from an object. /// This method will never delete the latest version of a content item. /// /// Id of the object to delete a version from /// Id of the version to delete /// Boolean indicating whether to delete versions prior to the versionId /// Optional Id of the User deleting versions of a Content object public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) { using (var uow = UowProvider.GetUnitOfWork()) { if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, specificVersion: versionId))) { uow.Commit(); return; } if (deletePriorVersions) { var content = GetByVersion(versionId); DeleteVersions(uow, id, content.UpdateDate, userId); } var repository = RepositoryFactory.CreateMediaRepository(uow); repository.DeleteVersion(versionId); uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, specificVersion: versionId)); Audit(uow, AuditType.Delete, "Delete Media by version performed by user", userId, -1); uow.Commit(); } } /// /// Saves a single object /// /// The to save /// Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. public void Save(IMedia media, int userId = 0, bool raiseEvents = true) { ((IMediaServiceOperations)this).Save(media, userId, raiseEvents); } /// /// Saves a collection of objects /// /// Collection of to save /// Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. public void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true) { ((IMediaServiceOperations)this).Save(medias, userId, raiseEvents); } /// /// Sorts a collection of objects by updating the SortOrder according /// to the ordering of items in the passed in . /// /// /// /// /// True if sorting succeeded, otherwise False public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) { var asArray = items.ToArray(); using (var uow = UowProvider.GetUnitOfWork()) { if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(asArray))) { uow.Commit(); return false; } var repository = RepositoryFactory.CreateMediaRepository(uow); int i = 0; foreach (var media in asArray) { //If the current sort order equals that of the media //we don't need to update it, so just increment the sort order //and continue. if (media.SortOrder == i) { i++; continue; } media.SortOrder = i; i++; repository.AddOrUpdate(media); repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); // generate preview for blame history? if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) { repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); } } if (raiseEvents) uow.Events.Dispatch(Saved, this, new SaveEventArgs(asArray, false)); Audit(uow, AuditType.Sort, "Sorting Media performed by user", userId, 0); uow.Commit(); } return true; } /// /// Gets paged media descendants as XML by path /// /// Path starts with /// Page number /// Page size /// Total records the query would return without paging /// A paged enumerable of XML entries of media items public IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaRepository(uow); return repository.GetPagedXmlEntriesByPath(path, pageIndex, pageSize, null, out totalRecords); } } /// /// Rebuilds all xml content in the cmsContentXml table for all media /// /// /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures /// for all media /// public void RebuildXmlStructures(params int[] contentTypeIds) { using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateMediaRepository(uow); repository.RebuildXmlStructures(media => _entitySerializer.Serialize(this, _dataTypeService, _userService, media), contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); Audit(uow, AuditType.Publish, "MediaService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); uow.Commit(); } } /// /// Updates the Path and Level on a collection of objects /// based on the Parent's Path and Level. Also change the trashed state if relevant. /// /// Collection of objects to update /// Path of the Parent media /// Level of the Parent media /// Indicates whether the Parent is trashed or not /// Used to track the objects to be used in the move event /// Collection of updated objects private IEnumerable UpdatePropertiesOnChildren(IEnumerable children, string parentPath, int parentLevel, bool parentTrashed, ICollection> eventInfo) { var list = new List(); foreach (var child in children) { var originalPath = child.Path; child.Path = string.Concat(parentPath, ",", child.Id); child.Level = parentLevel + 1; if (parentTrashed != child.Trashed) { child.ChangeTrashedState(parentTrashed, child.ParentId); } eventInfo.Add(new MoveEventInfo(child, originalPath, child.ParentId)); list.Add(child); var grandkids = GetChildren(child.Id).ToArray(); if (grandkids.Any()) { list.AddRange(UpdatePropertiesOnChildren(grandkids, child.Path, child.Level, child.Trashed, eventInfo)); } } return list; } //private void CreateAndSaveMediaXml(XElement xml, int id, UmbracoDatabase db) //{ // var poco = new ContentXmlDto { NodeId = id, Xml = xml.ToDataString() }; // var exists = db.FirstOrDefault("WHERE nodeId = @Id", new { Id = id }) != null; // int result = exists ? db.Update(poco) : Convert.ToInt32(db.Insert(poco)); //} private IMediaType FindMediaTypeByAlias(string mediaTypeAlias) { Mandate.ParameterNotNullOrEmpty(mediaTypeAlias, "mediaTypeAlias"); using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateMediaTypeRepository(uow); var query = Query.Builder.Where(x => x.Alias == mediaTypeAlias); var mediaTypes = repository.GetByQuery(query); if (mediaTypes.Any() == false) throw new Exception(string.Format("No MediaType matching the passed in Alias: '{0}' was found", mediaTypeAlias)); var mediaType = mediaTypes.First(); if (mediaType == null) throw new Exception(string.Format("MediaType matching the passed in Alias: '{0}' was null", mediaTypeAlias)); return mediaType; } } private void Audit(IScopeUnitOfWork uow, AuditType type, string message, int userId, int objectId) { var repository = RepositoryFactory.CreateAuditRepository(uow); repository.AddOrUpdate(new AuditItem(objectId, message, type, userId)); } /// /// Hack: This is used to fix some data if an entity's properties are invalid/corrupt /// /// private void QuickUpdate(IMedia media) { if (media == null) throw new ArgumentNullException("media"); if (media.HasIdentity == false) throw new InvalidOperationException("Cannot update an entity without an Identity"); using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateMediaRepository(uow); repository.AddOrUpdate(media); uow.Commit(); } } public Stream GetMediaFileContentStream(string filepath) { if (_mediaFileSystem.FileExists(filepath) == false) return null; try { return _mediaFileSystem.OpenFile(filepath); } catch { return null; // deal with race conds } } public void SetMediaFileContent(string filepath, Stream stream) { _mediaFileSystem.AddFile(filepath, stream, true); } public long GetMediaFileSize(string filepath) { return _mediaFileSystem.GetSize(filepath); } public void DeleteMediaFile(string filepath) { _mediaFileSystem.DeleteFile(filepath, true); } public void GenerateThumbnails(string filepath, PropertyType propertyType) { using (var filestream = _mediaFileSystem.OpenFile(filepath)) { _mediaFileSystem.GenerateThumbnails(filestream, filepath, propertyType); } } #region Event Handlers /// /// Occurs before Delete /// public static event TypedEventHandler DeletingVersions; /// /// Occurs after Delete /// public static event TypedEventHandler DeletedVersions; /// /// Occurs before Delete /// public static event TypedEventHandler> Deleting; /// /// Occurs after Delete /// public static event TypedEventHandler> Deleted; /// /// Occurs before Save /// public static event TypedEventHandler> Saving; /// /// Occurs after Save /// public static event TypedEventHandler> Saved; /// /// Occurs before Create /// [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] public static event TypedEventHandler> Creating; /// /// Occurs after Create /// /// /// Please note that the Media object has been created, but not saved /// so it does not have an identity yet (meaning no Id has been set). /// public static event TypedEventHandler> Created; /// /// Occurs before Content is moved to Recycle Bin /// public static event TypedEventHandler> Trashing; /// /// Occurs after Content is moved to Recycle Bin /// public static event TypedEventHandler> Trashed; /// /// Occurs before Move /// public static event TypedEventHandler> Moving; /// /// Occurs after Move /// public static event TypedEventHandler> Moved; /// /// Occurs before the Recycle Bin is emptied /// public static event TypedEventHandler EmptyingRecycleBin; /// /// Occurs after the Recycle Bin has been Emptied /// public static event TypedEventHandler EmptiedRecycleBin; #endregion } }