* Fix broken selectable state for list view items (#13148) * Add sync rendering extensions for block grid and async ones for block list (#13168) * Re-add IsPackable to Umbraco.Tests.Integration * Fix for potential race condition in packages search (#13153) * search on input allowing to wait for copy/paste etc * invoke resourcePromise() with correct parameters * return the xhrStatus allowing the caller to check if the request was aborted * fix: send in canceler.promise to allow the timeout to work * catch any errors and ignore aborts if they happen * move the logic to handle cancellations outside Angulars $scope.$apply * remove file accidentally committed * Fix for potential race condition in packages search (#13153) * search on input allowing to wait for copy/paste etc * invoke resourcePromise() with correct parameters * return the xhrStatus allowing the caller to check if the request was aborted * fix: send in canceler.promise to allow the timeout to work * catch any errors and ignore aborts if they happen * move the logic to handle cancellations outside Angulars $scope.$apply * remove file accidentally committed (cherry picked from commit4a412bb432) * V10: Fix request accessor memory leak (#13152) * Dispose OnChange event registration when disposing the notification handler * Ensure that the ApplicationUrl is only initialized once Since notifications handlers are transient,_hasAppUrl and _isInit defaults to false on every request causing it to always be called. * Make notification handler and EnsureApplicationUrl internal * Add missing ForceLeft and ForceRight (#13190) * V10: Fix request accessor memory leak (#13152) * Dispose OnChange event registration when disposing the notification handler * Ensure that the ApplicationUrl is only initialized once Since notifications handlers are transient,_hasAppUrl and _isInit defaults to false on every request causing it to always be called. * Make notification handler and EnsureApplicationUrl internal * Add missing ForceLeft and ForceRight (#13190) * Pass the node property to umb-property & umb-property-editor (#13151) Co-authored-by: Zeegaan <nge@umbraco.dk> * V10: 13099 fix validation error (#13170) * Add validation error message to Viewpicker * Add help-inline class to make validation-text red Co-authored-by: Zeegaan <nge@umbraco.dk> * move clear:both; to the flexbox example (#13194) * remove pointer-events from Image, to make drag n' drop work on firefox. (#13193) * area permission min-max inputs width increase (#13195) * Fix tags with CSV storage type (#13188) * Fixing null check as default(NRT) is null => default(configuration?.Delimiter) is also null and we were counting on it being the same as default(char) * Adding tests to check cases with multiple tags (or tag made of comma separated values) * Fix tags with CSV storage type (#13188) * Fixing null check as default(NRT) is null => default(configuration?.Delimiter) is also null and we were counting on it being the same as default(char) * Adding tests to check cases with multiple tags (or tag made of comma separated values) * Add documentation for default block grid partial views in the rendering extension methods (#13184) * Add data-element to umb property so we can find it (#13199) Co-authored-by: Zeegaan <nge@umbraco.dk> * Add data-element to umb property so we can find it (#13199) Co-authored-by: Zeegaan <nge@umbraco.dk> * V10/bugfix/create simple package test (#13162) * Fixed assert to hopefully find the package each time so it isnt flaky anymore * Updated so it retries 5 times instead of 2 * Dont submit html-report * Dont have output defined in npm run * Copy over playwright trace.zip files before publishing * Updated assert so it looks after the package in the table * updated so we get the first fail as the trace file * Bumped version for testhelpers * Updated so the test checks if the package actually exists. Added a wait that checks if the created packages button is visible * Updated package lock * Fixed so it now calls the correct testhelper Co-authored-by: Zeegaan <nge@umbraco.dk> * Merge BjarneF fix into 10.3 (#13220) Co-authored-by: Bjarne Fyrstenborg <bjarne_fyrstenborg@hotmail.com> * make Area fit within block row (#13221) * 10.3.0-RC: Change grid area input to number + change generic label (#13203) Co-authored-by: Bjarne Fyrstenborg <bjarne_fyrstenborg@hotmail.com> * move below center, to make room (#13222) * highlight areas in dragging-mode for modern browsers (#13224) * Collect new .xml language files from different sources (#13212) * Collecting new language files from different sources * Apply suggestions from review * Adding TODO for merging the language files locations to one when packages are not concerned * Collect new .xml language files from different sources (#13212) * Collecting new language files from different sources * Apply suggestions from review * Adding TODO for merging the language files locations to one when packages are not concerned * Resync editors if content model changed (#13230) * Disable BlockGridEditor (#13229) * Disable BlockGridEditor * Fix typeloader test * Update src/Umbraco.Core/Models/Blocks/BlockGridItem.cs Co-authored-by: Kenn Jacobsen <kja@umbraco.dk> * Apply suggestions from code review Co-authored-by: Kenn Jacobsen <kja@umbraco.dk> Co-authored-by: Kenn Jacobsen <kja@umbraco.dk> * V10.4: Re-enable block grid editor (#13231) * Revert "Disable BlockGridEditor (#13229)" This reverts commit4e9aa8dac2. * Re-do xml comments fix * Fix nullable reference error * Fix acceptance test package.json and package-lock.json * Re-add wait-on Co-authored-by: Kenn Jacobsen <kja@umbraco.dk> Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Co-authored-by: Zeegaan <nge@umbraco.dk> Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com> Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Co-authored-by: Bjarne Fyrstenborg <bjarne_fyrstenborg@hotmail.com> Co-authored-by: Matt Darby <matt@darby.digital>
329 lines
13 KiB
C#
329 lines
13 KiB
C#
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
using System.Xml;
|
|
using System.Xml.Linq;
|
|
using Microsoft.Extensions.FileProviders;
|
|
using Microsoft.Extensions.FileProviders.Internal;
|
|
using Microsoft.Extensions.Logging;
|
|
using Umbraco.Cms.Core.Cache;
|
|
using Umbraco.Extensions;
|
|
|
|
namespace Umbraco.Cms.Core.Services;
|
|
|
|
/// <summary>
|
|
/// Exposes the XDocument sources from files for the default localization text service and ensure caching is taken care
|
|
/// of
|
|
/// </summary>
|
|
public class LocalizedTextServiceFileSources
|
|
{
|
|
private readonly IAppPolicyCache _cache;
|
|
private readonly IDirectoryContents _directoryContents;
|
|
private readonly DirectoryInfo? _fileSourceFolder;
|
|
private readonly ILogger<LocalizedTextServiceFileSources> _logger;
|
|
private readonly IEnumerable<LocalizedTextServiceSupplementaryFileSource>? _supplementFileSources;
|
|
|
|
// TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
|
|
private readonly Dictionary<string, CultureInfo> _twoLetterCultureConverter = new();
|
|
|
|
private readonly Lazy<Dictionary<CultureInfo, Lazy<XDocument>>> _xmlSources;
|
|
|
|
[Obsolete("Use ctor with all params. This will be removed in Umbraco 12")]
|
|
public LocalizedTextServiceFileSources(
|
|
ILogger<LocalizedTextServiceFileSources> logger,
|
|
AppCaches appCaches,
|
|
DirectoryInfo fileSourceFolder,
|
|
IEnumerable<LocalizedTextServiceSupplementaryFileSource> supplementFileSources)
|
|
: this(
|
|
logger,
|
|
appCaches,
|
|
fileSourceFolder,
|
|
supplementFileSources,
|
|
new NotFoundDirectoryContents())
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is used to configure the file sources with the main file sources shipped with Umbraco and also including
|
|
/// supplemental/plugin based
|
|
/// localization files. The supplemental files will be loaded in and merged in after the primary files.
|
|
/// The supplemental files must be named with the 4 letter culture name with a hyphen such as : en-AU.xml
|
|
/// </summary>
|
|
public LocalizedTextServiceFileSources(
|
|
ILogger<LocalizedTextServiceFileSources> logger,
|
|
AppCaches appCaches,
|
|
DirectoryInfo fileSourceFolder,
|
|
IEnumerable<LocalizedTextServiceSupplementaryFileSource> supplementFileSources,
|
|
IDirectoryContents directoryContents)
|
|
{
|
|
if (appCaches == null)
|
|
{
|
|
throw new ArgumentNullException("appCaches");
|
|
}
|
|
|
|
_logger = logger ?? throw new ArgumentNullException("logger");
|
|
_directoryContents = directoryContents;
|
|
_cache = appCaches.RuntimeCache;
|
|
_fileSourceFolder = fileSourceFolder ?? throw new ArgumentNullException("fileSourceFolder");
|
|
_supplementFileSources = supplementFileSources;
|
|
|
|
// Create the lazy source for the _xmlSources
|
|
_xmlSources = new Lazy<Dictionary<CultureInfo, Lazy<XDocument>>>(() =>
|
|
{
|
|
var result = new Dictionary<CultureInfo, Lazy<XDocument>>();
|
|
|
|
IEnumerable<IFileInfo> files = GetLanguageFiles();
|
|
|
|
if (!files.Any())
|
|
{
|
|
return result;
|
|
}
|
|
|
|
foreach (IFileInfo fileInfo in files)
|
|
{
|
|
IFileInfo localCopy = fileInfo;
|
|
var filename = Path.GetFileNameWithoutExtension(localCopy.Name).Replace("_", "-");
|
|
|
|
// TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct
|
|
// names instead of storing them as 2 letters but actually having a 4 letter culture. So now, we
|
|
// need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that
|
|
// if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea)
|
|
// that any 4 letter file is named with the actual culture that it is!
|
|
CultureInfo? culture = null;
|
|
if (filename.Length == 2)
|
|
{
|
|
// we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't
|
|
// want to load in the entire doc into mem just to read a single value
|
|
using (Stream fs = fileInfo.CreateReadStream())
|
|
using (var reader = XmlReader.Create(fs))
|
|
{
|
|
if (reader.IsStartElement())
|
|
{
|
|
if (reader.Name == "language")
|
|
{
|
|
if (reader.MoveToAttribute("culture"))
|
|
{
|
|
var cultureVal = reader.Value;
|
|
try
|
|
{
|
|
culture = CultureInfo.GetCultureInfo(cultureVal);
|
|
|
|
// add to the tracked dictionary
|
|
_twoLetterCultureConverter[filename] = culture;
|
|
}
|
|
catch (CultureNotFoundException)
|
|
{
|
|
_logger.LogWarning(
|
|
"The culture {CultureValue} found in the file {CultureFile} is not a valid culture",
|
|
cultureVal,
|
|
fileInfo.Name);
|
|
|
|
// If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise
|
|
// an exception will be thrown.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (culture == null)
|
|
{
|
|
culture = CultureInfo.GetCultureInfo(filename);
|
|
}
|
|
|
|
// get the lazy value from cache
|
|
result[culture] = new Lazy<XDocument>(
|
|
() => _cache.GetCacheItem(
|
|
string.Format("{0}-{1}", typeof(LocalizedTextServiceFileSources).Name, culture.Name),
|
|
() =>
|
|
{
|
|
XDocument xdoc;
|
|
|
|
// load in primary
|
|
using (Stream fs = localCopy.CreateReadStream())
|
|
{
|
|
xdoc = XDocument.Load(fs);
|
|
}
|
|
|
|
// load in supplementary
|
|
MergeSupplementaryFiles(culture, xdoc);
|
|
|
|
return xdoc;
|
|
},
|
|
isSliding: true,
|
|
timeout: TimeSpan.FromMinutes(10))!);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public LocalizedTextServiceFileSources(ILogger<LocalizedTextServiceFileSources> logger, AppCaches appCaches, DirectoryInfo fileSourceFolder)
|
|
: this(logger, appCaches, fileSourceFolder, Enumerable.Empty<LocalizedTextServiceSupplementaryFileSource>())
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all xml sources for all culture files found in the folder.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public IDictionary<CultureInfo, Lazy<XDocument>> GetXmlSources() => _xmlSources.Value;
|
|
|
|
private IEnumerable<IFileInfo> GetLanguageFiles()
|
|
{
|
|
var result = new List<IFileInfo>();
|
|
|
|
if (_fileSourceFolder is not null && _fileSourceFolder.Exists)
|
|
{
|
|
result.AddRange(
|
|
new PhysicalDirectoryContents(_fileSourceFolder.FullName)
|
|
.Where(x => !x.IsDirectory && !x.Name.Contains("user") && x.Name.EndsWith(".xml"))); // Filter out *.user.xml
|
|
}
|
|
|
|
if (_supplementFileSources is not null)
|
|
{
|
|
// Get only the .xml files and filter out the user defined language files (*.user.xml) that overwrite the default
|
|
result.AddRange(_supplementFileSources
|
|
.Where(x => !x.FileInfo.Name.Contains("user") && x.FileInfo.Name.EndsWith(".xml"))
|
|
.Select(x => x.FileInfo));
|
|
}
|
|
|
|
if (_directoryContents.Exists)
|
|
{
|
|
result.AddRange(
|
|
_directoryContents
|
|
.Where(x => !x.IsDirectory && x.Name.EndsWith(".xml")));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
|
|
public Attempt<CultureInfo?> TryConvert2LetterCultureTo4Letter(string twoLetterCulture)
|
|
{
|
|
if (twoLetterCulture.Length != 2)
|
|
{
|
|
return Attempt<CultureInfo?>.Fail();
|
|
}
|
|
|
|
// This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
|
|
Dictionary<CultureInfo, Lazy<XDocument>> resolved = _xmlSources.Value;
|
|
|
|
return _twoLetterCultureConverter.ContainsKey(twoLetterCulture)
|
|
? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture])
|
|
: Attempt<CultureInfo?>.Fail();
|
|
}
|
|
|
|
// TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :(
|
|
public Attempt<string?> TryConvert4LetterCultureTo2Letter(CultureInfo culture)
|
|
{
|
|
if (culture == null)
|
|
{
|
|
throw new ArgumentNullException("culture");
|
|
}
|
|
|
|
// This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
|
|
Dictionary<CultureInfo, Lazy<XDocument>> resolved = _xmlSources.Value;
|
|
|
|
return _twoLetterCultureConverter.Values.Contains(culture)
|
|
? Attempt.Succeed(culture.Name.Substring(0, 2))
|
|
: Attempt<string?>.Fail();
|
|
}
|
|
|
|
private void MergeSupplementaryFiles(CultureInfo culture, XDocument xMasterDoc)
|
|
{
|
|
if (xMasterDoc.Root == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_supplementFileSources != null)
|
|
{
|
|
// now load in supplementary
|
|
IEnumerable<LocalizedTextServiceSupplementaryFileSource> found = _supplementFileSources.Where(x =>
|
|
{
|
|
var extension = Path.GetExtension(x.FileInfo.Name);
|
|
var fileCultureName = Path.GetFileNameWithoutExtension(x.FileInfo.Name).Replace("_", "-")
|
|
.Replace(".user", string.Empty);
|
|
return extension.InvariantEquals(".xml") && (
|
|
fileCultureName.InvariantEquals(culture.Name)
|
|
|| fileCultureName.InvariantEquals(culture.TwoLetterISOLanguageName));
|
|
});
|
|
|
|
foreach (LocalizedTextServiceSupplementaryFileSource supplementaryFile in found)
|
|
{
|
|
using (Stream stream = supplementaryFile.FileInfo.CreateReadStream())
|
|
{
|
|
XDocument xChildDoc;
|
|
try
|
|
{
|
|
xChildDoc = XDocument.Load(stream);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.FileInfo.Name);
|
|
continue;
|
|
}
|
|
|
|
if (xChildDoc.Root == null || xChildDoc.Root.Name != "language")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (XElement xArea in xChildDoc.Root.Elements("area")
|
|
.Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
|
|
{
|
|
var areaAlias = (string)xArea.Attribute("alias")!;
|
|
|
|
XElement? areaFound = xMasterDoc.Root.Elements("area").FirstOrDefault(x => (string)x.Attribute("alias")! == areaAlias);
|
|
if (areaFound == null)
|
|
{
|
|
// add the whole thing
|
|
xMasterDoc.Root.Add(xArea);
|
|
}
|
|
else
|
|
{
|
|
MergeChildKeys(xArea, areaFound, supplementaryFile.OverwriteCoreKeys);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void MergeChildKeys(XElement source, XElement destination, bool overwrite)
|
|
{
|
|
if (destination == null)
|
|
{
|
|
throw new ArgumentNullException("destination");
|
|
}
|
|
|
|
if (source == null)
|
|
{
|
|
throw new ArgumentNullException("source");
|
|
}
|
|
|
|
// merge in the child elements
|
|
foreach (XElement key in source.Elements("key")
|
|
.Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
|
|
{
|
|
var keyAlias = (string)key.Attribute("alias")!;
|
|
XElement? keyFound = destination.Elements("key")
|
|
.FirstOrDefault(x => (string)x.Attribute("alias")! == keyAlias);
|
|
if (keyFound == null)
|
|
{
|
|
// append, it doesn't exist
|
|
destination.Add(key);
|
|
}
|
|
else if (overwrite)
|
|
{
|
|
// overwrite
|
|
keyFound.Value = key.Value;
|
|
}
|
|
}
|
|
}
|
|
}
|