Files
Umbraco-CMS/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs

769 lines
29 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Extensions.Options;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Extensions;
using File = System.IO.File;
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
{
/// <inheritdoc />
public class CreatedPackageSchemaRepository : ICreatedPackagesRepository
{
private readonly PackageDefinitionXmlParser _xmlParser;
private readonly IUmbracoDatabase? _umbracoDatabase;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly FileSystems _fileSystems;
private readonly IEntityXmlSerializer _serializer;
private readonly IDataTypeService _dataTypeService;
private readonly ILocalizationService _localizationService;
private readonly IFileService _fileService;
private readonly IMediaService _mediaService;
private readonly IMediaTypeService _mediaTypeService;
private readonly IContentService _contentService;
private readonly MediaFileManager _mediaFileManager;
private readonly IMacroService _macroService;
private readonly IContentTypeService _contentTypeService;
private readonly string _tempFolderPath;
private readonly string _mediaFolderPath;
/// <summary>
/// Initializes a new instance of the <see cref="CreatedPackageSchemaRepository"/> class.
/// </summary>
public CreatedPackageSchemaRepository(
2022-01-23 15:26:17 +01:00
IUmbracoDatabaseFactory umbracoDatabaseFactory,
IHostingEnvironment hostingEnvironment,
IOptions<GlobalSettings> globalSettings,
FileSystems fileSystems,
IEntityXmlSerializer serializer,
IDataTypeService dataTypeService,
ILocalizationService localizationService,
IFileService fileService,
IMediaService mediaService,
IMediaTypeService mediaTypeService,
IContentService contentService,
MediaFileManager mediaFileManager,
IMacroService macroService,
IContentTypeService contentTypeService,
string? mediaFolderPath = null,
string? tempFolderPath = null)
{
2022-01-23 15:26:17 +01:00
_umbracoDatabase = umbracoDatabaseFactory.CreateDatabase();
_hostingEnvironment = hostingEnvironment;
_fileSystems = fileSystems;
_serializer = serializer;
_dataTypeService = dataTypeService;
_localizationService = localizationService;
_fileService = fileService;
_mediaService = mediaService;
_mediaTypeService = mediaTypeService;
_contentService = contentService;
_mediaFileManager = mediaFileManager;
_macroService = macroService;
_contentTypeService = contentTypeService;
_xmlParser = new PackageDefinitionXmlParser();
_mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages);
_tempFolderPath =
tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles";
}
public IEnumerable<PackageDefinition> GetAll()
{
Sql<ISqlContext> query = new Sql<ISqlContext>(_umbracoDatabase!.SqlContext)
.Select<CreatedPackageSchemaDto>()
.From<CreatedPackageSchemaDto>()
.OrderBy<CreatedPackageSchemaDto>(x => x.Id);
var packageDefinitions = new List<PackageDefinition>();
List<CreatedPackageSchemaDto> xmlSchemas = _umbracoDatabase.Fetch<CreatedPackageSchemaDto>(query);
foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas)
{
var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value));
if (packageDefinition is not null)
{
packageDefinition.Id = packageSchema.Id;
packageDefinition.Name = packageSchema.Name;
packageDefinition.PackageId = packageSchema.PackageId;
packageDefinitions.Add(packageDefinition);
}
}
return packageDefinitions;
}
public PackageDefinition? GetById(int id)
{
Sql<ISqlContext> query = new Sql<ISqlContext>(_umbracoDatabase!.SqlContext)
.Select<CreatedPackageSchemaDto>()
.From<CreatedPackageSchemaDto>()
.Where<CreatedPackageSchemaDto>(x => x.Id == id);
List<CreatedPackageSchemaDto> schemaDtos = _umbracoDatabase.Fetch<CreatedPackageSchemaDto>(query);
if (schemaDtos.IsCollectionEmpty())
{
return null;
}
var packageSchema = schemaDtos.First();
var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value));
if (packageDefinition is not null)
{
packageDefinition.Id = packageSchema.Id;
packageDefinition.Name = packageSchema.Name;
packageDefinition.PackageId = packageSchema.PackageId;
}
return packageDefinition;
}
public void Delete(int id)
{
// Delete package snapshot
var packageDef = GetById(id);
if (File.Exists(packageDef?.PackagePath))
{
File.Delete(packageDef.PackagePath);
}
Sql<ISqlContext> query = new Sql<ISqlContext>(_umbracoDatabase!.SqlContext)
.Delete<CreatedPackageSchemaDto>()
.Where<CreatedPackageSchemaDto>(x => x.Id == id);
_umbracoDatabase.Delete<CreatedPackageSchemaDto>(query);
}
public bool SavePackage(PackageDefinition definition)
{
if (definition == null)
{
throw new NullReferenceException("PackageDefinition cannot be null when saving");
}
if (definition.Name == null || string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null)
{
return false;
}
// Ensure it's valid
ValidatePackage(definition);
if (definition.Id == default)
{
// Create dto from definition
var dto = new CreatedPackageSchemaDto()
{
Name = definition.Name,
Value = _xmlParser.ToXml(definition).ToString(),
UpdateDate = DateTime.Now,
PackageId = Guid.NewGuid()
};
// Set the ids, we have to save in database first to get the Id
definition.PackageId = dto.PackageId;
var result = _umbracoDatabase!.Insert(dto);
var decimalResult = result.SafeCast<decimal>();
definition.Id = decimal.ToInt32(decimalResult);
}
// Save snapshot locally, we do this to the updated packagePath
ExportPackage(definition);
// Create dto from definition
var updatedDto = new CreatedPackageSchemaDto()
{
Name = definition.Name,
Value = _xmlParser.ToXml(definition).ToString(),
Id = definition.Id,
PackageId = definition.PackageId,
UpdateDate = DateTime.Now
};
_umbracoDatabase?.Update(updatedDto);
return true;
}
public string ExportPackage(PackageDefinition definition)
{
// Ensure it's valid
ValidatePackage(definition);
// Create a folder for building this package
var temporaryPath =
_hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid());
if (Directory.Exists(temporaryPath) == false)
{
Directory.CreateDirectory(temporaryPath);
}
try
{
// Init package file
XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root);
// Info section
root.Add(GetPackageInfoXml(definition));
PackageDocumentsAndTags(definition, root);
PackageDocumentTypes(definition, root);
PackageMediaTypes(definition, root);
PackageTemplates(definition, root);
PackageStylesheets(definition, root);
PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem!);
PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View",
_fileSystems.PartialViewsFileSystem!);
PackageMacros(definition, root);
PackageDictionaryItems(definition, root);
PackageLanguages(definition, root);
PackageDataTypes(definition, root);
Dictionary<string, Stream> mediaFiles = PackageMedia(definition, root);
string fileName;
string tempPackagePath;
if (mediaFiles.Count > 0)
{
fileName = "package.zip";
tempPackagePath = Path.Combine(temporaryPath, fileName);
using (FileStream fileStream = File.OpenWrite(tempPackagePath))
using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true))
{
ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml");
using (Stream entryStream = packageXmlEntry.Open())
{
compiledPackageXml.Save(entryStream);
}
foreach (KeyValuePair<string, Stream> mediaFile in mediaFiles)
{
var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}";
ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath);
using (Stream entryStream = mediaEntry.Open())
using (mediaFile.Value)
{
mediaFile.Value.Seek(0, SeekOrigin.Begin);
mediaFile.Value.CopyTo(entryStream);
}
}
}
}
else
{
fileName = "package.xml";
tempPackagePath = Path.Combine(temporaryPath, fileName);
using (FileStream fileStream = File.OpenWrite(tempPackagePath))
{
compiledPackageXml.Save(fileStream);
}
}
var directoryName =
_hostingEnvironment.MapPathWebRoot(
Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_')));
if (Directory.Exists(directoryName) == false)
{
Directory.CreateDirectory(directoryName);
}
var finalPackagePath = Path.Combine(directoryName, fileName);
if (File.Exists(finalPackagePath))
{
File.Delete(finalPackagePath);
}
if (File.Exists(finalPackagePath.Replace("zip", "xml")))
{
File.Delete(finalPackagePath.Replace("zip", "xml"));
}
File.Move(tempPackagePath, finalPackagePath);
definition.PackagePath = finalPackagePath;
return finalPackagePath;
}
finally
{
// Clean up
Directory.Delete(temporaryPath, true);
}
}
private XDocument CreateCompiledPackageXml(out XElement root)
{
root = new XElement("umbPackage");
var compiledPackageXml = new XDocument(root);
return compiledPackageXml;
}
private void ValidatePackage(PackageDefinition definition)
{
// Ensure it's valid
var context = new ValidationContext(definition, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(definition, context, results);
if (!isValid)
{
throw new InvalidOperationException("Validation failed, there is invalid data on the model: " +
string.Join(", ", results.Select(x => x.ErrorMessage)));
}
}
private void PackageDataTypes(PackageDefinition definition, XContainer root)
{
var dataTypes = new XElement("DataTypes");
foreach (var dtId in definition.DataTypes)
{
if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
{
continue;
}
IDataType? dataType = _dataTypeService.GetDataType(outInt);
if (dataType == null)
{
continue;
}
dataTypes.Add(_serializer.Serialize(dataType));
}
root.Add(dataTypes);
}
private void PackageLanguages(PackageDefinition definition, XContainer root)
{
var languages = new XElement("Languages");
foreach (var langId in definition.Languages)
{
if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
{
continue;
}
ILanguage? lang = _localizationService.GetLanguageById(outInt);
if (lang == null)
{
continue;
}
languages.Add(_serializer.Serialize(lang));
}
root.Add(languages);
}
private void PackageDictionaryItems(PackageDefinition definition, XContainer root)
{
var rootDictionaryItems = new XElement("DictionaryItems");
var items = new Dictionary<Guid, (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue)>();
foreach (var dictionaryId in definition.DictionaryItems)
{
if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
{
continue;
}
IDictionaryItem? di = _localizationService.GetDictionaryItemById(outInt);
if (di == null)
{
continue;
}
items[di.Key] = (di, _serializer.Serialize(di, false));
}
// organize them in hierarchy ...
var itemCount = items.Count;
var processed = new Dictionary<Guid, XElement>();
while (processed.Count < itemCount)
{
foreach (Guid key in items.Keys.ToList())
{
(IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key];
if (!dictionaryItem.ParentId.HasValue)
{
// if it has no parent, its definitely just at the root
AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue);
}
else
{
if (processed.ContainsKey(dictionaryItem.ParentId.Value))
{
// we've processed this parent element already so we can just append this xml child to it
AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key,
serializedDictionaryValue);
}
else if (items.ContainsKey(dictionaryItem.ParentId.Value))
{
// we know the parent exists in the dictionary but
// we haven't processed it yet so we'll leave it for the next loop
continue;
}
else
{
// in this case, the parent of this item doesn't exist in our collection, we have no
// choice but to add it to the root.
AppendDictionaryElement(rootDictionaryItems, items, processed, key,
serializedDictionaryValue);
}
}
}
}
root.Add(rootDictionaryItems);
static void AppendDictionaryElement(XElement rootDictionaryItems,
Dictionary<Guid, (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue)> items,
Dictionary<Guid, XElement> processed, Guid key, XElement serializedDictionaryValue)
{
// track it
processed.Add(key, serializedDictionaryValue);
// append it
rootDictionaryItems.Add(serializedDictionaryValue);
// remove it so its not re-processed
items.Remove(key);
}
}
private void PackageMacros(PackageDefinition definition, XContainer root)
{
var packagedMacros = new List<IMacro>();
var macros = new XElement("Macros");
foreach (var macroId in definition.Macros)
{
if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outInt))
{
continue;
}
XElement? macroXml = GetMacroXml(outInt, out IMacro? macro);
if (macroXml is null)
{
continue;
}
macros.Add(macroXml);
packagedMacros.Add(macro!);
}
root.Add(macros);
// Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views)
IEnumerable<string> views = packagedMacros
.Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials))
.Select(x =>
x.MacroSource.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\'));
PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem!);
}
private void PackageStylesheets(PackageDefinition definition, XContainer root)
{
var stylesheetsXml = new XElement("Stylesheets");
foreach (var stylesheet in definition.Stylesheets)
{
if (stylesheet.IsNullOrWhiteSpace())
{
continue;
}
XElement? xml = GetStylesheetXml(stylesheet, true);
if (xml != null)
{
stylesheetsXml.Add(xml);
}
}
root.Add(stylesheetsXml);
}
private void PackageStaticFiles(
IEnumerable<string> filePaths,
XContainer root,
string containerName,
string elementName,
IFileSystem fileSystem)
{
var scriptsXml = new XElement(containerName);
foreach (var file in filePaths)
{
if (file.IsNullOrWhiteSpace())
{
continue;
}
if (!fileSystem.FileExists(file))
{
throw new InvalidOperationException("No file found with path " + file);
}
using Stream? stream = fileSystem.OpenFile(file);
if (stream is not null)
{
using (var reader = new StreamReader(stream))
{
var fileContents = reader.ReadToEnd();
scriptsXml.Add(
new XElement(
elementName,
new XAttribute("path", file),
new XCData(fileContents)));
}
}
}
root.Add(scriptsXml);
}
private void PackageTemplates(PackageDefinition definition, XContainer root)
{
var templatesXml = new XElement("Templates");
foreach (var templateId in definition.Templates)
{
if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
{
continue;
}
ITemplate? template = _fileService.GetTemplate(outInt);
if (template == null)
{
continue;
}
templatesXml.Add(_serializer.Serialize(template));
}
root.Add(templatesXml);
}
private void PackageDocumentTypes(PackageDefinition definition, XContainer root)
{
var contentTypes = new HashSet<IContentType>();
var docTypesXml = new XElement("DocumentTypes");
foreach (var dtId in definition.DocumentTypes)
{
if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
{
continue;
}
IContentType? contentType = _contentTypeService.Get(outInt);
if (contentType == null)
{
continue;
}
AddDocumentType(contentType, contentTypes);
}
foreach (IContentType contentType in contentTypes)
{
docTypesXml.Add(_serializer.Serialize(contentType));
}
root.Add(docTypesXml);
}
private void PackageMediaTypes(PackageDefinition definition, XContainer root)
{
var mediaTypes = new HashSet<IMediaType>();
var mediaTypesXml = new XElement("MediaTypes");
foreach (var mediaTypeId in definition.MediaTypes)
{
if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt))
{
continue;
}
IMediaType? mediaType = _mediaTypeService.Get(outInt);
if (mediaType == null)
{
continue;
}
AddMediaType(mediaType, mediaTypes);
}
foreach (IMediaType mediaType in mediaTypes)
{
mediaTypesXml.Add(_serializer.Serialize(mediaType));
}
root.Add(mediaTypesXml);
}
private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root)
{
// Documents and tags
if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId,
NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId))
{
if (contentNodeId > 0)
{
// load content from umbraco.
IContent? content = _contentService.GetById(contentNodeId);
if (content != null)
{
var contentXml = definition.ContentLoadChildNodes
? content.ToDeepXml(_serializer)
: content.ToXml(_serializer);
// Create the Documents/DocumentSet node
root.Add(
new XElement(
"Documents",
new XElement(
"DocumentSet",
new XAttribute("importMode", "root"),
contentXml)));
}
}
}
}
private Dictionary<string, Stream> PackageMedia(PackageDefinition definition, XElement root)
{
var mediaStreams = new Dictionary<string, Stream>();
// callback that occurs on each serialized media item
void OnSerializedMedia(IMedia media, XElement xmlMedia)
{
// get the media file path and store that separately in the XML.
// the media file path is different from the URL and is specifically
// extracted using the property editor for this media file and the current media file system.
Stream? mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath);
if (mediaStream != null)
{
xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath!));
// add the stream to our outgoing stream
mediaStreams.Add(mediaFilePath!, mediaStream);
}
}
IEnumerable<IMedia> medias = _mediaService.GetByIds(definition.MediaUdis);
var mediaXml = new XElement(
"MediaItems",
medias.Select(media =>
{
XElement serializedMedia = _serializer.Serialize(
media,
definition.MediaLoadChildNodes,
OnSerializedMedia);
return new XElement("MediaSet", serializedMedia);
}));
root.Add(mediaXml);
return mediaStreams;
}
/// <summary>
/// Gets a macros xml node
/// </summary>
private XElement? GetMacroXml(int macroId, out IMacro? macro)
{
macro = _macroService.GetById(macroId);
if (macro == null)
{
return null;
}
XElement xml = _serializer.Serialize(macro);
return xml;
}
/// <summary>
/// Converts a umbraco stylesheet to a package xml node
/// </summary>
/// <param name="path">The path of the stylesheet.</param>
/// <param name="includeProperties">if set to <c>true</c> [include properties].</param>
private XElement? GetStylesheetXml(string path, bool includeProperties)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
}
IStylesheet? stylesheet = _fileService.GetStylesheet(path);
if (stylesheet == null)
{
return null;
}
return _serializer.Serialize(stylesheet, includeProperties);
}
private void AddDocumentType(IContentType dt, HashSet<IContentType> dtl)
{
if (dt.ParentId > 0)
{
IContentType? parent = _contentTypeService.Get(dt.ParentId);
if (parent != null)
{
AddDocumentType(parent, dtl);
}
}
if (!dtl.Contains(dt))
{
dtl.Add(dt);
}
}
private void AddMediaType(IMediaType mediaType, HashSet<IMediaType> mediaTypes)
{
if (mediaType.ParentId > 0)
{
IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId);
if (parent != null)
{
AddMediaType(parent, mediaTypes);
}
}
if (!mediaTypes.Contains(mediaType))
{
mediaTypes.Add(mediaType);
}
}
private static XElement GetPackageInfoXml(PackageDefinition definition)
{
var info = new XElement("info");
// Package info
var package = new XElement("package");
package.Add(new XElement("name", definition.Name));
info.Add(package);
return info;
}
}
}