Merge pull request #11568 from umbraco/v9/feature/merge_v8_03-11-2021

V9: Merge v8: 03-11-2021
This commit is contained in:
Bjarke Berg
2021-11-03 12:56:23 +01:00
committed by GitHub
15 changed files with 332 additions and 44 deletions

View File

@@ -17,6 +17,7 @@
"CMS": {
//#if (HasNoNodesViewPath || UseHttpsRedirect)
"Global": {
"SanitizeTinyMce": true,
//#if (!HasNoNodesViewPath && UseHttpsRedirect)
"UseHttps": true
//#elseif (UseHttpsRedirect)
@@ -25,6 +26,7 @@
//#if (HasNoNodesViewPath)
"NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE"
//#endif
},
//#endif
"Hosting": {

View File

@@ -28,6 +28,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
internal const bool StaticDisableElectionForSingleServer = false;
internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml";
internal const string StaticSqlWriteLockTimeOut = "00:00:05";
internal const bool StaticSanitizeTinyMce = false;
/// <summary>
/// Gets or sets a value for the reserved URLs.
@@ -157,6 +158,12 @@ namespace Umbraco.Cms.Core.Configuration.Models
/// </summary>
public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host);
/// <summary>
/// Gets a value indicating whether TinyMCE scripting sanitization should be applied
/// </summary>
[DefaultValue(StaticSanitizeTinyMce)]
public bool SanitizeTinyMce => StaticSanitizeTinyMce;
/// <summary>
/// An int value representing the time in milliseconds to lock the database for a write operation
/// </summary>

View File

@@ -79,7 +79,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
var success = false;
// Access the site home page and check for the click-jack protection header or meta tag
Uri url = _hostingEnvironment.ApplicationMainUrl;
var url = _hostingEnvironment.ApplicationMainUrl.GetLeftPart(UriPartial.Authority);
try
{

View File

@@ -23,28 +23,28 @@ namespace Umbraco.Cms.Infrastructure.Search
}
/// <inheritdoc />
public IEnumerable<string> GetBackOfficeFields() => _backOfficeFields;
public virtual IEnumerable<string> GetBackOfficeFields() => _backOfficeFields;
/// <inheritdoc />
public IEnumerable<string> GetBackOfficeMembersFields() => _backOfficeMembersFields;
public virtual IEnumerable<string> GetBackOfficeMembersFields() => _backOfficeMembersFields;
/// <inheritdoc />
public IEnumerable<string> GetBackOfficeMediaFields() => _backOfficeMediaFields;
public virtual IEnumerable<string> GetBackOfficeMediaFields() => _backOfficeMediaFields;
/// <inheritdoc />
public IEnumerable<string> GetBackOfficeDocumentFields() => Enumerable.Empty<string>();
public virtual IEnumerable<string> GetBackOfficeDocumentFields() => Enumerable.Empty<string>();
/// <inheritdoc />
public ISet<string> GetBackOfficeFieldsToLoad() => _backOfficeFieldsToLoad;
public virtual ISet<string> GetBackOfficeFieldsToLoad() => _backOfficeFieldsToLoad;
/// <inheritdoc />
public ISet<string> GetBackOfficeMembersFieldsToLoad() => _backOfficeMembersFieldsToLoad;
public virtual ISet<string> GetBackOfficeMembersFieldsToLoad() => _backOfficeMembersFieldsToLoad;
/// <inheritdoc />
public ISet<string> GetBackOfficeMediaFieldsToLoad() => _backOfficeMediaFieldsToLoad;
public virtual ISet<string> GetBackOfficeMediaFieldsToLoad() => _backOfficeMediaFieldsToLoad;
/// <inheritdoc />
public ISet<string> GetBackOfficeDocumentFieldsToLoad()
public virtual ISet<string> GetBackOfficeDocumentFieldsToLoad()
{
var fields = _backOfficeDocumentFieldsToLoad;

View File

@@ -413,6 +413,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{"showAllowSegmentationForDocumentTypes", false},
{"minimumPasswordLength", _memberPasswordConfigurationSettings.RequiredLength},
{"minimumPasswordNonAlphaNum", _memberPasswordConfigurationSettings.GetMinNonAlphaNumericChars()},
{"sanitizeTinyMce", _globalSettings.SanitizeTinyMce}
}
},
{

View File

@@ -590,36 +590,44 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
var tempPath = Path.Combine(root,fileName);
using (var stream = System.IO.File.Create(tempPath))
if (Path.GetFullPath(tempPath).StartsWith(Path.GetFullPath(root)))
{
formFile.CopyToAsync(stream).GetAwaiter().GetResult();
}
if (ext.InvariantEquals("udt"))
{
model.TempFileName = Path.Combine(root, fileName);
var xd = new XmlDocument
using (var stream = System.IO.File.Create(tempPath))
{
XmlResolver = null
};
xd.Load(model.TempFileName);
formFile.CopyToAsync(stream).GetAwaiter().GetResult();
}
model.Alias = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Alias")?.FirstChild.Value;
model.Name = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Name")?.FirstChild.Value;
if (ext.InvariantEquals("udt"))
{
model.TempFileName = Path.Combine(root, fileName);
var xd = new XmlDocument
{
XmlResolver = null
};
xd.Load(model.TempFileName);
model.Alias = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Alias")?.FirstChild.Value;
model.Name = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Name")?.FirstChild.Value;
}
else
{
model.Notifications.Add(new BackOfficeNotification(
_localizedTextService.Localize("speechBubbles","operationFailedHeader"),
_localizedTextService.Localize("media","disallowedFileType"),
NotificationStyle.Warning));
}
}
else
{
model.Notifications.Add(new BackOfficeNotification(
_localizedTextService.Localize("speechBubbles","operationFailedHeader"),
_localizedTextService.Localize("media","disallowedFileType"),
_localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
_localizedTextService.Localize("media", "invalidFileName"),
NotificationStyle.Warning));
}
}
return model;
}

View File

@@ -12,6 +12,58 @@
var currentOverlay = null;
/**
* @ngdoc method
* @name umbraco.services.overlayService#open
* @methodOf umbraco.services.overlayService
*
* @description
* Opens a new overlay.
*
* @param {object} overlay The rendering options for the overlay.
* @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/default/default.html` if nothing is specified.
* @param {string=} overlay.position The alias of the position of the overlay. Defaults to `center`.
*
* Custom positions can be added by adding a CSS rule for the the underlying CSS rule. Eg. for the position `center`, the corresponding `umb-overlay-center` CSS rule is defined as:
*
* <pre>
* .umb-overlay.umb-overlay-center {
* position: absolute;
* width: 600px;
* height: auto;
* top: 50%;
* left: 50%;
* transform: translate(-50%,-50%);
* border-radius: 3px;
* }
* </pre>
* @param {string=} overlay.size Sets an alias for the size of the overlay to be opened. If set to `small` (default), an `umb-overlay--small` class name will be appended the the class list of the main overlay element in the DOM.
*
* Umbraco does not support any more sizes by default, but if you wish to introduce a `medium` size, you could do so by adding a CSS rule simlar to:
*
* <pre>
* .umb-overlay-center.umb-overlay--medium {
* width: 800px;
* }
* </pre>
* @param {booean=} overlay.disableBackdropClick A boolean value indicating whether the click event on the backdrop should be disabled.
* @param {string=} overlay.title The overall title of the overlay. The title will be omitted if not specified.
* @param {string=} overlay.subtitle The sub title of the overlay. The sub title will be omitted if not specified.
* @param {object=} overlay.itemDetails An item that will replace the header of the overlay.
* @param {string=} overlay.itemDetails.icon The icon of the item - eg. `icon-book`.
* @param {string=} overlay.itemDetails.title The title of the item.
* @param {string=} overlay.itemDetails.description Sets the description of the item. *
* @param {string=} overlay.submitButtonLabel The label of the submit button. To support localized values, it's recommended to use the `submitButtonLabelKey` instead.
* @param {string=} overlay.submitButtonLabelKey The key to be used for the submit button label. Defaults to `general_submit` if not specified.
* @param {string=} overlay.submitButtonState The state of the submit button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `init`, `busy", `success`, `error`.
* @param {string=} overlay.submitButtonStyle The styling of the submit button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. Defaults to `success` if not specified specified.
* @param {string=} overlay.hideSubmitButton A boolean value indicating whether the submit button should be hidden. Default is `false`.
* @param {string=} overlay.disableSubmitButton A boolean value indicating whether the submit button should be disabled, preventing the user from submitting the overlay. Default is `false`.
* @param {string=} overlay.closeButtonLabel The label of the close button. To support localized values, it's recommended to use the `closeButtonLabelKey` instead.
* @param {string=} overlay.closeButtonLabelKey The key to be used for the close button label. Defaults to `general_close` if not specified.
* @param {string=} overlay.submit A callback function that is invoked when the user submits the overlay.
* @param {string=} overlay.close A callback function that is invoked when the user closes the overlay.
*/
function open(newOverlay) {
// prevent two open overlays at the same time
@@ -49,6 +101,14 @@
eventsService.emit("appState.overlay", overlay);
}
/**
* @ngdoc method
* @name umbraco.services.overlayService#close
* @methodOf umbraco.services.overlayService
*
* @description
* Closes the current overlay.
*/
function close() {
focusLockService.removeInertAttribute();
@@ -61,6 +121,16 @@
eventsService.emit("appState.overlay", null);
}
/**
* @ngdoc method
* @name umbraco.services.overlayService#ysod
* @methodOf umbraco.services.overlayService
*
* @description
* Opens a new overlay with an error message.
*
* @param {object} error The error to be shown.
*/
function ysod(error) {
const overlay = {
view: "views/common/overlays/ysod/ysod.html",
@@ -72,6 +142,36 @@
open(overlay);
}
/**
* @ngdoc method
* @name umbraco.services.overlayService#confirm
* @methodOf umbraco.services.overlayService
*
* @description
* Opens a new overlay prompting the user to confirm the overlay.
*
* @param {object} overlay The options for the overlay.
* @param {string=} overlay.confirmType The type of the confirm dialog, which helps define standard styling and labels of the overlay. Supported values are `delete` and `remove`.
* @param {string=} overlay.closeButtonLabelKey The key to be used for the cancel button label. Defaults to `general_cancel` if not specified.
* @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/confirm/confirm.html` if nothing is specified.
* @param {string=} overlay.confirmMessageStyle The styling of the confirm message. If `overlay.confirmType` is `delete`, the fallback value is `danger` - otherwise a message style isn't explicitly specified.
* @param {string=} overlay.submitButtonStyle The styling of the confirm button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`.
*
* If not specified, the fallback value depends on the value specified for the `overlay.confirmType` parameter:
*
* - `delete`: fallback key is `danger`
* - `remove`: fallback key is `primary`
* - anything else: no fallback AKA default button style
* @param {string=} overlay.submitButtonLabelKey The key to be used for the confirm button label.
*
* If not specified, the fallback value depends on the value specified for the `overlay.confirmType` parameter:
*
* - `delete`: fallback key is `actions_delete`
* - `remove`: fallback key is `actions_remove`
* - anything else: fallback is `general_confirm`
* @param {function=} overlay.close A callback function that is invoked when the user closes the overlay.
* @param {function=} overlay.submit A callback function that is invoked when the user confirms the overlay.
*/
function confirm(overlay) {
if (!overlay.closeButtonLabelKey) overlay.closeButtonLabelKey = "general_cancel";
@@ -99,11 +199,45 @@
open(overlay);
}
/**
* @ngdoc method
* @name umbraco.services.overlayService#confirmDelete
* @methodOf umbraco.services.overlayService
*
* @description
* Opens a new overlay prompting the user to confirm the overlay. The overlay will have styling and labels useful for when the user needs to confirm a delete action.
*
* @param {object} overlay The options for the overlay.
* @param {string=} overlay.closeButtonLabelKey The key to be used for the cancel button label. Defaults to `general_cancel` if not specified.
* @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/confirm/confirm.html` if nothing is specified.
* @param {string=} overlay.confirmMessageStyle The styling of the confirm message. Defaults to `delete` if not specified specified.
* @param {string=} overlay.submitButtonStyle The styling of the confirm button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. Defaults to `danger` if not specified specified.
* @param {string=} overlay.submitButtonLabelKey The key to be used for the confirm button label. Defaults to `actions_delete` if not specified.
* @param {function=} overlay.close A callback function that is invoked when the user closes the overlay.
* @param {function=} overlay.submit A callback function that is invoked when the user confirms the overlay.
*/
function confirmDelete(overlay) {
overlay.confirmType = "delete";
confirm(overlay);
}
/**
* @ngdoc method
* @name umbraco.services.overlayService#confirmRemove
* @methodOf umbraco.services.overlayService
*
* @description
* Opens a new overlay prompting the user to confirm the overlay. The overlay will have styling and labels useful for when the user needs to confirm a remove action.
*
* @param {object} overlay The options for the overlay.
* @param {string=} overlay.closeButtonLabelKey The key to be used for the cancel button label. Defaults to `general_cancel` if not specified.
* @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/confirm/confirm.html` if nothing is specified.
* @param {string=} overlay.confirmMessageStyle The styling of the confirm message - eg. `danger`.
* @param {string=} overlay.submitButtonStyle The styling of the confirm button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. Defaults to `primary` if not specified specified.
* @param {string=} overlay.submitButtonLabelKey The key to be used for the confirm button label. Defaults to `actions_remove` if not specified.
* @param {function=} overlay.close A callback function that is invoked when the user closes the overlay.
* @param {function=} overlay.submit A callback function that is invoked when the user confirms the overlay.
*/
function confirmRemove(overlay) {
overlay.confirmType = "remove";
confirm(overlay);

View File

@@ -1502,6 +1502,19 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
});
}
if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){
/** prevent injecting arbitrary JavaScript execution in on-attributes. */
const allNodes = Array.prototype.slice.call(args.editor.dom.doc.getElementsByTagName("*"));
allNodes.forEach(node => {
for (var i = 0; i < node.attributes.length; i++) {
if(node.attributes[i].name.indexOf("on") === 0) {
node.removeAttribute(node.attributes[i].name)
}
}
});
}
});
args.editor.on('init', function (e) {
@@ -1513,6 +1526,60 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
//enable browser based spell checking
args.editor.getBody().setAttribute('spellcheck', true);
/** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes:
* https://github.com/advisories/GHSA-w7jx-j77m-wp65
* https://github.com/advisories/GHSA-5vm8-hhgr-jcjp
*/
const uriAttributesToSanitize = ['src', 'href', 'data', 'background', 'action', 'formaction', 'poster', 'xlink:href'];
const parseUri = function() {
// Encapsulated JS logic.
const safeSvgDataUrlElements = [ 'img', 'video' ];
const scriptUriRegExp = /((java|vb)script|mhtml):/i;
const trimRegExp = /[\s\u0000-\u001F]+/g;
const isInvalidUri = (uri, tagName) => {
if (/^data:image\//i.test(uri)) {
return safeSvgDataUrlElements.indexOf(tagName) !== -1 && /^data:image\/svg\+xml/i.test(uri);
} else {
return /^data:/i.test(uri);
}
};
return function parseUri(uri, tagName) {
uri = uri.replace(trimRegExp, '');
try {
// Might throw malformed URI sequence
uri = decodeURIComponent(uri);
} catch (ex) {
// Fallback to non UTF-8 decoder
uri = unescape(uri);
}
if (scriptUriRegExp.test(uri)) {
return;
}
if (isInvalidUri(uri, tagName)) {
return;
}
return uri;
}
}();
if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){
args.editor.serializer.addAttributeFilter(uriAttributesToSanitize, function (nodes) {
nodes.forEach(function(node) {
node.attributes.forEach(function(attr) {
const attrName = attr.name.toLowerCase();
if(uriAttributesToSanitize.indexOf(attrName) !== -1) {
attr.value = parseUri(attr.value, node.name);
}
});
});
});
}
//start watching the value
startWatch();
});

View File

@@ -55,6 +55,11 @@ input.umb-table__input {
color: @ui-disabled-type;
}
.umb-table-head__icon {
position: relative;
top: 2px;
}
.umb-table-head__link {
background: transparent;
border: 0 none;
@@ -111,7 +116,7 @@ input.umb-table__input {
.umb-table-body .umb-table-row.-selectable {
cursor: pointer;
}
.umb-table-row.-selected,
.umb-table-row.-selected,
.umb-table-body .umb-table-row.-selectable:hover {
&::before {
content: "";
@@ -226,7 +231,7 @@ input.umb-table__input {
&.umb-table-body__checkicon {
display: inline-block;
}
}
}
}
// Table Row Styles
@@ -309,8 +314,8 @@ input.umb-table__input {
.umb-table__loading-overlay {
position: absolute;
width: 100%;
height: 100%;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
z-index: 1;
}
@@ -330,7 +335,7 @@ input.umb-table__input {
}
.umb-table--condensed {
.umb-table-cell:first-of-type:not(.not-fixed) {
padding-top: 10px;
padding-bottom: 10px;

View File

@@ -60,20 +60,20 @@
ng-click="vm.selectAll($event)"
ng-checked="vm.isSelectedAll()">
</div>
<div class="umb-table-cell umb-table__name">
<div class="umb-table-cell umb-table__name">
<button type="button"
class="umb-table-head__link sortable"
ng-click="setSort('name')">
<localize key="general_name">Name</localize>
<i class="umb-table-head__icon icon" aria-hidden="true" ng-class="{'icon-navigation-up': isSortDirection('name', 'asc'), 'icon-navigation-down': isSortDirection('name', 'desc')}"></i>
<umb-icon ng-attr-icon="{{isSortDirection('name', 'asc') && 'icon-navigation-up' || isSortDirection('name', 'desc') && 'icon-navigation-down'}}" class="umb-table-head__icon"></umb-icon>
</button>
</div>
<div class="umb-table-cell">
<button type="button"
class="umb-table-head__link sortable"
ng-click="setSort('updateDate')">
Last Updated
<i class="umb-table-head__icon icon" aria-hidden="true" ng-class="{'icon-navigation-up': isSortDirection('updateDate', 'asc'), 'icon-navigation-down': isSortDirection('updateDate', 'desc')}"></i>
<localize key="general_lastUpdated">Last Updated</localize>
<umb-icon ng-attr-icon="{{isSortDirection('updateDate', 'asc') && 'icon-navigation-up' || isSortDirection('updateDate', 'desc') && 'icon-navigation-down'}}" class="umb-table-head__icon"></umb-icon>
</button>
</div>
@@ -90,7 +90,7 @@
ng-show="item.isFolder"
ng-class="{'-locked': item.selected || !item.file || !item.thumbnail}"
ng-click="clickItemName(item, $event, $index)">
</umb-icon>
</umb-icon>
<span data-src="{{item.value.src}}" class="item-name">{{item.name}}</span>
</div>
<div class="umb-table-cell">
@@ -101,4 +101,3 @@
</div>
</div>
</div>

View File

@@ -22,7 +22,7 @@
<umb-icon icon="icon-navigation" class="handle"></umb-icon>
</td>
<td>
{{ph = placeholder(config);""}}
{{ph = placeholder(config);hasTabsOrFirstRender = (elemTypeTabs[config.ncAlias].length || config.ncAlias=='');""}}
<button type="button" class="btn-reset umb-nested-content__placeholder" ng-class="{'umb-nested-content__placeholder--selected':ph}" ng-click="openElemTypeModal($event, config)">
<umb-node-preview ng-if="ph" icon="ph.icon" name="ph.name"></umb-node-preview>
<localize key="content_nestedContentAddElementType" ng-if="!ph">Add element type</localize>
@@ -30,9 +30,14 @@
</td>
<td>
<select id="{{model.alias}}_tab_select"
<select ng-show="hasTabsOrFirstRender" id="{{model.alias}}_tab_select"
ng-options="t for t in elemTypeTabs[config.ncAlias]"
ng-model="config.ncTabAlias" required></select>
<span ng-show="!hasTabsOrFirstRender" class="red">
<localize key="content_nestedContentNoGroups">
The selected element type does not contain any supported groups (tabs are not supported by this editor, either change them to groups or use the Block List editor).
</localize>
</span>
</td>
<td>
<input type="text" ng-model="config.nameTemplate" />

View File

@@ -34,7 +34,8 @@ a:hover {
color: rgba(0, 0, 0, .8);
}
.content p code {
.content p code,
.content li code {
font-size: 85%;
font-family: inherit;
background-color: #f7f7f9;

View File

@@ -282,6 +282,7 @@
name. Use
</key>
<key alias="nestedContentTemplateHelpTextPart2">to display the item index</key>
<key alias="nestedContentNoGroups">The selected element type does not contain any supported groups (tabs are not supported by this editor, either change them to groups or use the Block List editor).</key>
<key alias="addTextBox">Add another text box</key>
<key alias="removeTextBox">Remove this text box</key>
<key alias="contentRoot">Content root</key>
@@ -325,6 +326,7 @@
<key alias="clickToUpload">Click to upload</key>
<key alias="orClickHereToUpload">or click here to choose files</key>
<key alias="disallowedFileType">Cannot upload this file, it does not have an approved file type</key>
<key alias="invalidFileName">Cannot upload this file, it does not have a valid file name</key>
<key alias="maxFileSize">Max file size is</key>
<key alias="mediaRoot">Media root</key>
<key alias="createFolderFailed">Failed to create a folder under parent id %0%</key>
@@ -848,6 +850,7 @@
<key alias="avatar">Avatar for</key>
<key alias="header">Header</key>
<key alias="systemField">system field</key>
<key alias="lastUpdated">Last Updated</key>
</area>
<area alias="colors">
<key alias="blue">Blue</key>
@@ -1168,7 +1171,6 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
%6%
Have a nice day!
Cheers from the Umbraco robot
]]></key>
<key alias="mailBodyVariantSummary">The following languages have been modified %0%</key>
@@ -1912,7 +1914,6 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
http://%3%
Have a nice day!
Cheers from the Umbraco robot
]]></key>
<key alias="noTranslators">No translator users found. Please create a translator user before you start sending

View File

@@ -286,6 +286,7 @@
name. Use
</key>
<key alias="nestedContentTemplateHelpTextPart2">to display the item index</key>
<key alias="nestedContentNoGroups">The selected element type does not contain any supported groups (tabs are not supported by this editor, either change them to groups or use the Block List editor).</key>
<key alias="addTextBox">Add another text box</key>
<key alias="removeTextBox">Remove this text box</key>
<key alias="contentRoot">Content root</key>
@@ -329,6 +330,7 @@
<key alias="clickToUpload">Click to upload</key>
<key alias="orClickHereToUpload">or click here to choose files</key>
<key alias="disallowedFileType">Cannot upload this file, it does not have an approved file type</key>
<key alias="invalidFileName">Cannot upload this file, it does not have a valid file name</key>
<key alias="maxFileSize">Max file size is</key>
<key alias="mediaRoot">Media root</key>
<key alias="moveToSameFolderFailed">Parent and destination folders cannot be the same</key>
@@ -869,6 +871,7 @@
<key alias="avatar">Avatar for</key>
<key alias="header">Header</key>
<key alias="systemField">system field</key>
<key alias="lastUpdated">Last Updated</key>
</area>
<area alias="colors">
<key alias="blue">Blue</key>

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using NUnit.Framework;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configurations
{
[TestFixture]
public class LanguageXmlTests
{
[Test]
[Platform("Win")] //TODO figure out why Path.GetFullPath("/mnt/c/...") is not considered an absolute path on linux + mac
public void Can_Load_Language_Xml_Files()
{
var languageDirectoryPath = GetLanguageDirectory();
var readFilesCount = 0;
var xmlDocument = new XmlDocument();
var directoryInfo = new DirectoryInfo(languageDirectoryPath);
foreach (var languageFile in directoryInfo.GetFiles("*.xml", SearchOption.TopDirectoryOnly))
{
// Load will throw an exception if the XML isn't valid.
xmlDocument.Load(languageFile.FullName);
readFilesCount++;
}
// Ensure that at least one file was read.
Assert.AreNotEqual(0, readFilesCount);
}
private static string GetLanguageDirectory()
{
var testDirectoryPathParts = Path.GetDirectoryName(TestContext.CurrentContext.TestDirectory)
.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
var solutionDirectoryPathParts = testDirectoryPathParts
.Take(Array.IndexOf(testDirectoryPathParts, "tests"));
var languageFolderPathParts = new List<string>(solutionDirectoryPathParts);
var additionalPathParts = new[] { "Umbraco.Web.UI", "umbraco", "config", "lang" };
languageFolderPathParts.AddRange(additionalPathParts);
// Hack for build-server - when this path is generated in that envrionment it's missing the "src" folder.
// Not sure why, but if it's missing we'll add it in the right place.
if (!languageFolderPathParts.Contains("src"))
{
languageFolderPathParts.Insert(languageFolderPathParts.Count - additionalPathParts.Length, "src");
}
return string.Join(Path.DirectorySeparatorChar.ToString(), languageFolderPathParts);
}
}
}