Merge remote-tracking branch 'origin/v8/dev' into v9/dev

# Conflicts:
#	build/NuSpecs/UmbracoCms.Web.nuspec
#	src/SolutionInfo.cs
#	src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
#	src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs
#	src/Umbraco.Examine/Umbraco.Examine.csproj
#	src/Umbraco.Tests/Umbraco.Tests.csproj
#	src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg
#	src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg
#	src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js
#	src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less
#	src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html
#	src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js
#	src/Umbraco.Web.UI.Client/src/views/packages/views/install-local.controller.js
#	src/Umbraco.Web.UI.Client/src/views/packages/views/install-local.html
#	src/Umbraco.Web/Editors/BackOfficeServerVariables.cs
#	src/Umbraco.Web/Umbraco.Web.csproj
This commit is contained in:
Bjarke Berg
2022-02-15 14:10:17 +01:00
15 changed files with 768 additions and 302 deletions

View File

@@ -156,6 +156,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
internal const bool StaticShowDeprecatedPropertyEditors = false;
internal const string StaticLoginBackgroundImage = "assets/img/login.jpg";
internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg";
internal const bool StaticHideBackOfficeLogo = false;
/// <summary>
/// Gets or sets a value for the content notification settings.
@@ -219,6 +220,12 @@ namespace Umbraco.Cms.Core.Configuration.Models
[DefaultValue(StaticLoginLogoImage)]
public string LoginLogoImage { get; set; } = StaticLoginLogoImage;
/// <summary>
/// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not.
/// </summary>
[DefaultValue(StaticHideBackOfficeLogo)]
public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo;
/// <summary>
/// Get or sets the model representing the global content version cleanup policy
/// </summary>

View File

@@ -100,7 +100,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var keepOnlyKeys = new Dictionary<string, string[]>
{
{"umbracoUrls", new[] {"authenticationApiBaseUrl", "serverVarsJs", "externalLoginsUrl", "currentUserApiBaseUrl", "previewHubUrl", "iconApiBaseUrl"}},
{"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum"}},
{"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum", "hideBackofficeLogo"}},
{"application", new[] {"applicationPath", "cacheBuster"}},
{"isDebuggingEnabled", new string[] { }},
{"features", new [] {"disabledFeatures"}}
@@ -408,6 +408,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{"allowPasswordReset", _securitySettings.AllowPasswordReset},
{"loginBackgroundImage", _contentSettings.LoginBackgroundImage},
{"loginLogoImage", _contentSettings.LoginLogoImage },
{"hideBackofficeLogo", _contentSettings.HideBackOfficeLogo },
{"showUserInvite", _emailSender.CanSendRequiredEmail()},
{"canSendRequiredEmail", _emailSender.CanSendRequiredEmail()},
{"showAllowSegmentationForDocumentTypes", false},

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Routing;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Attributes;
@@ -11,11 +13,16 @@ namespace Umbraco.Cms.Web.BackOffice.Services
public class ConflictingRouteService : IConflictingRouteService
{
private readonly TypeLoader _typeLoader;
private readonly IEnumerable<EndpointDataSource> _endpointDataSources;
/// <summary>
/// Initializes a new instance of the <see cref="ConflictingRouteService"/> class.
/// </summary>
public ConflictingRouteService(TypeLoader typeLoader) => _typeLoader = typeLoader;
public ConflictingRouteService(TypeLoader typeLoader, IEnumerable<EndpointDataSource> endpointDataSources)
{
_typeLoader = typeLoader;
_endpointDataSources = endpointDataSources;
}
/// <inheritdoc/>
public bool HasConflictingRoutes(out string controllerName)

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 315.89 315.89">
<path fill="#ffffff" d="M0,157.74A157.95,157.95,0,1,1,158,315.89,157.95,157.95,0,0,1,0,157.74Zm154.74,54.09a155.41,155.41,0,0,1-36.5-3.29,27.92,27.92,0,0,1-19.94-16q-5.35-12.34-5.21-38.1a243,243,0,0,1,1.69-26.84q1.55-13,3.09-21.46l1.07-5.59a2,2,0,0,0,0-.49,3.2,3.2,0,0,0-2.65-3.17L75.92,93.67h-.44a3.19,3.19,0,0,0-3.11,2.48c-.35,1.31-.56,2.27-1.17,5.38-1.16,6-2.24,11.85-3.43,20.38a264.17,264.17,0,0,0-2.3,27.94,145.24,145.24,0,0,0,0,19.57q.72,25.94,8.9,41.42t27.72,22.3q19.53,6.81,54.43,6.66h2.91q34.94.15,54.41-6.66t27.71-22.3q8.17-15.53,8.91-41.42a145.24,145.24,0,0,0,0-19.57,266.84,266.84,0,0,0-2.3-27.94c-1.2-8.44-2.27-14.26-3.44-20.38-.61-3.11-.81-4.07-1.16-5.38a3.21,3.21,0,0,0-3.12-2.48h-.52l-20.38,3.18a3.2,3.2,0,0,0-2.68,3.17,4,4,0,0,0,0,.49l1.08,5.59q1.55,8.48,3.12,21.46a245.68,245.68,0,0,1,1.65,26.84q.27,25.69-5.21,38.07a27.9,27.9,0,0,1-19.76,16.07,155.19,155.19,0,0,1-36.48,3.29Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 315.89 315.89">
<path fill="#ffffff" d="M0,157.74A157.95,157.95,0,1,1,158,315.89,157.95,157.95,0,0,1,0,157.74Zm154.74,54.09a155.41,155.41,0,0,1-36.5-3.29,27.92,27.92,0,0,1-19.94-16q-5.35-12.34-5.21-38.1a243,243,0,0,1,1.69-26.84q1.55-13,3.09-21.46l1.07-5.59a2,2,0,0,0,0-.49,3.2,3.2,0,0,0-2.65-3.17L75.92,93.67h-.44a3.19,3.19,0,0,0-3.11,2.48c-.35,1.31-.56,2.27-1.17,5.38-1.16,6-2.24,11.85-3.43,20.38a264.17,264.17,0,0,0-2.3,27.94,145.24,145.24,0,0,0,0,19.57q.72,25.94,8.9,41.42t27.72,22.3q19.53,6.81,54.43,6.66h2.91q34.94.15,54.41-6.66t27.71-22.3q8.17-15.53,8.91-41.42a145.24,145.24,0,0,0,0-19.57,266.84,266.84,0,0,0-2.3-27.94c-1.2-8.44-2.27-14.26-3.44-20.38-.61-3.11-.81-4.07-1.16-5.38a3.21,3.21,0,0,0-3.12-2.48h-.52l-20.38,3.18a3.2,3.2,0,0,0-2.68,3.17,4,4,0,0,0,0,.49l1.08,5.59q1.55,8.48,3.12,21.46a245.68,245.68,0,0,1,1.65,26.84q.27,25.69-5.21,38.07a27.9,27.9,0,0,1-19.76,16.07,155.19,155.19,0,0,1-36.48,3.29Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 976 B

After

Width:  |  Height:  |  Size: 979 B

View File

@@ -16,6 +16,7 @@
{ value: "assets/img/application/logo@2x.png" },
{ value: "assets/img/application/logo@3x.png" }
];
scope.hideBackofficeLogo = Umbraco.Sys.ServerVariables.umbracoSettings.hideBackofficeLogo;
// when a user logs out or timesout
evts.push(eventsService.on("app.notAuthenticated", function () {
@@ -104,15 +105,26 @@
$timeout.cancel(scope.logoModal.timer);
};
scope.hideLogoModal = function() {
$timeout.cancel(scope.logoModal.timer);
scope.logoModal.timer = $timeout(function () {
scope.logoModal.show = false;
}, 100);
if(scope.logoModal.show === true) {
$timeout.cancel(scope.logoModal.timer);
scope.logoModal.timer = $timeout(function () {
scope.logoModal.show = false;
}, 100);
}
};
scope.stopClickEvent = function($event) {
$event.stopPropagation();
};
scope.toggleLogoModal = function() {
if(scope.logoModal.show) {
$timeout.cancel(scope.logoModal.timer);
scope.logoModal.show = false;
} else {
scope.showLogoModal();
}
};
}
var directive = {

View File

@@ -0,0 +1,20 @@
/**
* @ngdoc filter
* @name umbraco.filters.simpleMarkdown
* @description
* Used when rendering a string as Markdown as HTML (i.e. with ng-bind-html). Allows use of **bold**, *italics*, ![images](url) and [links](url)
**/
angular.module("umbraco.filters").filter('simpleMarkdown', function () {
return function (text) {
if (!text) {
return '';
}
return text
.replace(/\*\*(.*)\*\*/gim, '<b>$1</b>')
.replace(/\*(.*)\*/gim, '<i>$1</i>')
.replace(/!\[(.*?)\]\((.*?)\)/gim, "<img alt='$1' src='$2' />")
.replace(/\[(.*?)\]\((.*?)\)/gim, "<a href='$2' target='_blank' rel='noopener' class='underline'>$1</a>")
.replace(/\n/g, '<br />').trim();
};
});

View File

@@ -30,9 +30,8 @@
for (var p = 0; p < tab.properties.length; p++) {
var prop = tab.properties[p];
if (dataModel[prop.alias]) {
prop.value = dataModel[prop.alias];
}
prop.value = dataModel[prop.alias];
}
}
@@ -53,9 +52,8 @@
for (var p = 0; p < tab.properties.length; p++) {
var prop = tab.properties[p];
if (prop.value) {
dataModel[prop.alias] = prop.value;
}
dataModel[prop.alias] = prop.value;
}
}

View File

@@ -259,8 +259,8 @@ function navigationService($routeParams, $location, $q, $injector, eventsService
var updated = false;
retainedQueryStrings.forEach(r => {
// if mculture is set to null in nextRouteParams, the value will be undefined and we will not retain any query string that has a value of "null"
if (currRouteParams[r] && nextRouteParams[r] !== undefined && !nextRouteParams[r]) {
// testing explicitly for undefined in nextRouteParams here, as it must be possible to "unset" e.g. mculture by specifying a null value
if (currRouteParams[r] && nextRouteParams[r] === undefined) {
toRetain[r] = currRouteParams[r];
updated = true;
}

View File

@@ -9,11 +9,18 @@
.umb-app-header__logo {
margin-right: 30px;
flex-shrink: 0;
button {
img {
height: 30px;
}
}
}
@media (max-width: 1279px) {
.umb-app-header__logo {
display: none;
}
}
.umb-app-header__logo-modal {

View File

@@ -9,44 +9,51 @@
color: @gray-5;
}
.umb-upload-local__dropzone {
position: relative;
width: 500px;
height: 300px;
border: 2px dashed @ui-action-border;
border-radius: 3px;
background: @white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 30px;
transition: 100ms box-shadow ease, 100ms border ease;
.umb-upload-local {
&.drag-over {
border-color: @ui-action-border-hover;
border-style: solid;
box-shadow: 0 3px 8px rgba(0,0,0, .1);
&__dropzone {
position: relative;
width: 500px;
height: 300px;
border: 2px dashed @ui-action-border;
border-radius: 3px;
background: @white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 30px;
transition: 100ms box-shadow ease, 100ms border ease;
&.drag-over {
border-color: @ui-action-border-hover;
border-style: solid;
box-shadow: 0 3px 8px rgba(0,0,0, .1);
transition: 100ms box-shadow ease, 100ms border ease;
}
.umb-icon {
display: block;
color: @ui-action-type;
font-size: 6.75rem;
line-height: 1;
margin: 0 auto;
}
.umb-info-local-item {
margin: 20px;
}
}
.umb-icon {
display: block;
&__select-file {
font-weight: bold;
color: @ui-action-type;
font-size: 6.75rem;
line-height: 1;
margin: 0 auto;
}
}
cursor: pointer;
.umb-upload-local__select-file {
font-weight: bold;
color: @ui-action-type;
cursor: pointer;
&:hover {
text-decoration: underline;
color: @ui-action-type-hover;
&:hover {
text-decoration: underline;
color: @ui-action-type-hover;
}
}
}
@@ -117,7 +124,3 @@
.umb-info-local-item {
margin-bottom: 20px;
}
.umb-upload-local__dropzone .umb-info-local-item {
margin:20px;
}

View File

@@ -1,7 +1,11 @@
<div>
<div class="umb-app-header">
<div class="umb-app-header__logo">
<button type="button" class="btn-reset" ng-click="showLogoModal()">
<div class="umb-app-header__logo" ng-if="hideBackofficeLogo !== true">
<button
type="button"
class="btn-reset"
ng-click="toggleLogoModal()"
>
<img
src="assets/img/application/umbraco_logomark_white.svg"
alt="Umbraco"

View File

@@ -1,261 +1,261 @@
(function () {
"use strict";
"use strict";
function ContentProtectController($scope, $q, publicAccessResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) {
function ContentProtectController($scope, $q, contentResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) {
var vm = this;
var id = $scope.currentNode.id;
var vm = this;
var id = $scope.currentNode.id;
vm.loading = false;
vm.buttonState = "init";
vm.isValid = isValid;
vm.next = next;
vm.save = save;
vm.close = close;
vm.toggle = toggle;
vm.pickLoginPage = pickLoginPage;
vm.pickErrorPage = pickErrorPage;
vm.pickGroup = pickGroup;
vm.removeGroup = removeGroup;
vm.pickMember = pickMember;
vm.removeMember = removeMember;
vm.removeProtection = removeProtection;
vm.removeProtectionConfirm = removeProtectionConfirm;
vm.type = null;
vm.step = null;
function onInit() {
vm.loading = true;
// get the current public access protection
publicAccessResource.getPublicAccess(id).then(function (publicAccess) {
vm.loading = false;
vm.buttonState = "init";
// init the current settings for public access (if any)
vm.loginPage = publicAccess.loginPage;
vm.errorPage = publicAccess.errorPage;
vm.groups = publicAccess.groups || [];
vm.members = publicAccess.members || [];
vm.canRemove = true;
vm.isValid = isValid;
vm.next = next;
vm.save = save;
vm.close = close;
vm.toggle = toggle;
vm.pickLoginPage = pickLoginPage;
vm.pickErrorPage = pickErrorPage;
vm.pickGroup = pickGroup;
vm.removeGroup = removeGroup;
vm.pickMember = pickMember;
vm.removeMember = removeMember;
vm.removeProtection = removeProtection;
vm.removeProtectionConfirm = removeProtectionConfirm;
if (vm.members.length) {
vm.type = "member";
next();
}
else if (vm.groups.length) {
vm.type = "group";
next();
}
else {
vm.canRemove = false;
}
});
}
vm.type = null;
vm.step = null;
function next() {
if (vm.type === "group") {
vm.loading = true;
// get all existing member groups for lookup upon selection
// NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore
memberGroupResource.getGroups().then(function (groups) {
vm.step = vm.type;
vm.allGroups = groups;
vm.hasGroups = groups.length > 0;
vm.loading = false;
});
}
else {
vm.step = vm.type;
}
}
function isValid() {
if (!vm.type) {
return false;
}
if (!vm.protectForm.$valid) {
return false;
}
if (!vm.loginPage || !vm.errorPage) {
return false;
}
if (vm.type === "group") {
return vm.groups && vm.groups.length > 0;
}
if (vm.type === "member") {
return vm.members && vm.members.length > 0;
}
return true;
}
function save() {
vm.buttonState = "busy";
var groups = _.map(vm.groups, function (group) { return group.name; });
var usernames = _.map(vm.members, function (member) { return member.username; });
publicAccessResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then(
function () {
localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) {
vm.success = {
message: value
};
});
navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true });
$scope.dialog.confirmDiscardChanges = true;
}, function (error) {
vm.error = error;
vm.buttonState = "error";
}
);
}
function close() {
// ensure that we haven't set a locked state on the dialog before closing it
navigationService.allowHideDialog(true);
navigationService.hideDialog();
}
function toggle(group) {
group.selected = !group.selected;
$scope.dialog.confirmDiscardChanges = true;
}
function pickGroup() {
navigationService.allowHideDialog(false);
editorService.memberGroupPicker({
multiPicker: true,
submit: function (model) {
var selectedGroupIds = model.selectedMemberGroups
? model.selectedMemberGroups
: [model.selectedMemberGroup];
_.each(selectedGroupIds,
function (groupId) {
// find the group in the lookup list and add it if it isn't already
var group = _.find(vm.allGroups, function (g) { return g.id === parseInt(groupId); });
if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) {
vm.groups.push(group);
}
});
editorService.close();
navigationService.allowHideDialog(true);
$scope.dialog.confirmDiscardChanges = true;
},
close: function () {
editorService.close();
navigationService.allowHideDialog(true);
}
});
}
function removeGroup(group) {
vm.groups = _.reject(vm.groups, function (g) { return g.id === group.id });
$scope.dialog.confirmDiscardChanges = true;
}
function pickMember() {
navigationService.allowHideDialog(false);
// TODO: once editorService has a memberPicker method, use that instead
editorService.treePicker({
multiPicker: true,
entityType: "Member",
section: "member",
treeAlias: "member",
filter: function (i) {
return i.metaData.isContainer;
},
filterCssClass: "not-allowed",
submit: function (model) {
if (model.selection && model.selection.length) {
var promises = [];
// get the selected member usernames
_.each(model.selection,
function (member) {
// TODO:
// as-is we need to fetch all the picked members one at a time to get their usernames.
// when editorService has a memberPicker method, see if this can't be avoided - otherwise
// add a memberResource.getByKeys() method to do all this in one request
promises.push(
memberResource.getByKey(member.key).then(function (newMember) {
if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) {
vm.members.push(newMember);
}
})
);
});
editorService.close();
navigationService.allowHideDialog(true);
// wait for all the member lookups to complete
function onInit() {
vm.loading = true;
$q.all(promises).then(function () {
vm.loading = false;
// get the current public access protection
contentResource.getPublicAccess(id).then(function (publicAccess) {
vm.loading = false;
// init the current settings for public access (if any)
vm.loginPage = publicAccess.loginPage;
vm.errorPage = publicAccess.errorPage;
vm.groups = publicAccess.groups || [];
vm.members = publicAccess.members || [];
vm.canRemove = true;
if (vm.members.length) {
vm.type = "member";
next();
}
else if (vm.groups.length) {
vm.type = "group";
next();
}
else {
vm.canRemove = false;
}
});
}
function next() {
if (vm.type === "group") {
vm.loading = true;
// get all existing member groups for lookup upon selection
// NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore
memberGroupResource.getGroups().then(function (groups) {
vm.step = vm.type;
vm.allGroups = groups;
vm.hasGroups = groups.length > 0;
vm.loading = false;
});
}
else {
vm.step = vm.type;
}
}
function isValid() {
if (!vm.type) {
return false;
}
if (!vm.protectForm.$valid) {
return false;
}
if (!vm.loginPage || !vm.errorPage) {
return false;
}
if (vm.type === "group") {
return vm.groups && vm.groups.length > 0;
}
if (vm.type === "member") {
return vm.members && vm.members.length > 0;
}
return true;
}
function save() {
vm.buttonState = "busy";
var groups = _.map(vm.groups, function (group) { return encodeURIComponent(group.name); });
var usernames = _.map(vm.members, function (member) { return member.username; });
contentResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then(
function () {
localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) {
vm.success = {
message: value
};
});
navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true });
$scope.dialog.confirmDiscardChanges = true;
}, function (error) {
vm.error = error;
vm.buttonState = "error";
}
);
}
function close() {
// ensure that we haven't set a locked state on the dialog before closing it
navigationService.allowHideDialog(true);
navigationService.hideDialog();
}
function toggle(group) {
group.selected = !group.selected;
$scope.dialog.confirmDiscardChanges = true;
}
},
close: function () {
editorService.close();
navigationService.allowHideDialog(true);
}
});
}
function removeMember(member) {
vm.members = _.without(vm.members, member);
}
function pickLoginPage() {
pickPage(vm.loginPage);
}
function pickErrorPage() {
pickPage(vm.errorPage);
}
function pickPage(page) {
navigationService.allowHideDialog(false);
editorService.contentPicker({
submit: function (model) {
if (page === vm.loginPage) {
vm.loginPage = model.selection[0];
}
else {
vm.errorPage = model.selection[0];
}
editorService.close();
navigationService.allowHideDialog(true);
$scope.dialog.confirmDiscardChanges = true;
},
close: function () {
editorService.close();
navigationService.allowHideDialog(true);
function pickGroup() {
navigationService.allowHideDialog(false);
editorService.memberGroupPicker({
multiPicker: true,
submit: function(model) {
var selectedGroupIds = model.selectedMemberGroups
? model.selectedMemberGroups
: [model.selectedMemberGroup];
_.each(selectedGroupIds,
function (groupId) {
// find the group in the lookup list and add it if it isn't already
var group = _.find(vm.allGroups, function(g) { return g.id === parseInt(groupId); });
if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) {
vm.groups.push(group);
}
});
editorService.close();
navigationService.allowHideDialog(true);
$scope.dialog.confirmDiscardChanges = true;
},
close: function() {
editorService.close();
navigationService.allowHideDialog(true);
}
});
}
});
}
function removeProtection() {
vm.removing = true;
}
function removeProtectionConfirm() {
vm.buttonState = "busy";
publicAccessResource.removePublicAccess(id).then(
function () {
localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function (value) {
vm.success = {
message: value
};
});
navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true });
}, function (error) {
vm.error = error;
vm.buttonState = "error";
function removeGroup(group) {
vm.groups = _.reject(vm.groups, function(g) { return g.id === group.id });
$scope.dialog.confirmDiscardChanges = true;
}
);
function pickMember() {
navigationService.allowHideDialog(false);
// TODO: once editorService has a memberPicker method, use that instead
editorService.treePicker({
multiPicker: true,
entityType: "Member",
section: "member",
treeAlias: "member",
filter: function (i) {
return i.metaData.isContainer;
},
filterCssClass: "not-allowed",
submit: function (model) {
if (model.selection && model.selection.length) {
var promises = [];
// get the selected member usernames
_.each(model.selection,
function (member) {
// TODO:
// as-is we need to fetch all the picked members one at a time to get their usernames.
// when editorService has a memberPicker method, see if this can't be avoided - otherwise
// add a memberResource.getByKeys() method to do all this in one request
promises.push(
memberResource.getByKey(member.key).then(function(newMember) {
if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) {
vm.members.push(newMember);
}
})
);
});
editorService.close();
navigationService.allowHideDialog(true);
// wait for all the member lookups to complete
vm.loading = true;
$q.all(promises).then(function() {
vm.loading = false;
});
$scope.dialog.confirmDiscardChanges = true;
}
},
close: function () {
editorService.close();
navigationService.allowHideDialog(true);
}
});
}
function removeMember(member) {
vm.members = _.without(vm.members, member);
}
function pickLoginPage() {
pickPage(vm.loginPage);
}
function pickErrorPage() {
pickPage(vm.errorPage);
}
function pickPage(page) {
navigationService.allowHideDialog(false);
editorService.contentPicker({
submit: function (model) {
if (page === vm.loginPage) {
vm.loginPage = model.selection[0];
}
else {
vm.errorPage = model.selection[0];
}
editorService.close();
navigationService.allowHideDialog(true);
$scope.dialog.confirmDiscardChanges = true;
},
close: function () {
editorService.close();
navigationService.allowHideDialog(true);
}
});
}
function removeProtection() {
vm.removing = true;
}
function removeProtectionConfirm() {
vm.buttonState = "busy";
contentResource.removePublicAccess(id).then(
function () {
localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function(value) {
vm.success = {
message: value
};
});
navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true });
}, function (error) {
vm.error = error;
vm.buttonState = "error";
}
);
}
onInit();
}
onInit();
}
angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController);
angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController);
})();

View File

@@ -0,0 +1,212 @@
(function () {
"use strict";
function PackagesInstallLocalController($scope, Upload, umbRequestHelper, packageResource, localStorageService, $timeout, $window, localizationService, $q) {
var vm = this;
vm.state = "upload";
vm.localPackage = {};
vm.installPackage = installPackage;
vm.installState = {
status: "",
progress: 0
};
vm.installCompleted = false;
vm.zipFile = {
uploadStatus: "idle",
uploadProgress: 0,
serverErrorMessage: null
};
$scope.handleFiles = function (files, event, invalidFiles) {
if (files) {
for (var i = 0; i < files.length; i++) {
upload(files[i]);
}
}
};
var labels = {};
var labelKeys = [
"packager_installStateImporting",
"packager_installStateInstalling",
"packager_installStateRestarting",
"packager_installStateComplete",
"packager_installStateCompleted"
];
localizationService.localizeMany(labelKeys).then(function (values) {
labels.installStateImporting = values[0];
labels.installStateInstalling = values[1];
labels.installStateRestarting = values[2];
labels.installStateComplete = values[3];
labels.installStateCompleted = values[4];
});
function upload(file) {
Upload.upload({
url: umbRequestHelper.getApiUrl("packageInstallApiBaseUrl", "UploadLocalPackage"),
fields: {},
file: file
}).progress(function (evt) {
// hack: in some browsers the progress event is called after success
// this prevents the UI from going back to a uploading state
if (vm.zipFile.uploadStatus !== "done" && vm.zipFile.uploadStatus !== "error") {
// set view state to uploading
vm.state = 'uploading';
// calculate progress in percentage
var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10);
// set percentage property on file
vm.zipFile.uploadProgress = progressPercentage;
// set uploading status on file
vm.zipFile.uploadStatus = "uploading";
}
}).success(function (data, status, headers, config) {
if (data.notifications && data.notifications.length > 0) {
// set error status on file
vm.zipFile.uploadStatus = "error";
// Throw message back to user with the cause of the error
vm.zipFile.serverErrorMessage = data.notifications[0].message;
} else {
// set done status on file
vm.zipFile.uploadStatus = "done";
loadPackage();
vm.zipFile.uploadProgress = 100;
vm.localPackage = data;
}
}).error(function (evt, status, headers, config) {
// set status done
vm.zipFile.uploadStatus = "error";
// If file not found, server will return a 404 and display this message
if (status === 404) {
vm.zipFile.serverErrorMessage = "File not found";
}
else if (status == 400) {
//it's a validation error
vm.zipFile.serverErrorMessage = evt.message;
}
else {
//it's an unhandled error
//if the service returns a detailed error
if (evt.InnerException) {
vm.zipFile.serverErrorMessage = evt.InnerException.ExceptionMessage;
//Check if its the common "too large file" exception
if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) {
vm.zipFile.serverErrorMessage = "File too large to upload";
}
} else if (evt.Message) {
vm.zipFile.serverErrorMessage = evt.Message;
}
}
});
}
function loadPackage() {
if (vm.zipFile.uploadStatus === "done") {
vm.state = "packageDetails";
}
}
function installPackage() {
vm.installState.status = labels.installStateImporting;
vm.installState.progress = "0";
packageResource
.import(vm.localPackage)
.then(function (pack) {
vm.installState.progress = "25";
vm.installState.status = labels.installStateInstalling;
return packageResource.installFiles(pack);
},
installError)
.then(function (pack) {
vm.installState.status = labels.installStateRestarting;
vm.installState.progress = "50";
var deferred = $q.defer();
//check if the app domain is restarted ever 2 seconds
var count = 0;
function checkRestart() {
$timeout(function () {
packageResource.checkRestart(pack).then(function (d) {
count++;
//if there is an id it means it's not restarted yet but we'll limit it to only check 10 times
if (d.isRestarting && count < 10) {
checkRestart();
}
else {
//it's restarted!
deferred.resolve(d);
}
},
installError);
},
2000);
}
checkRestart();
return deferred.promise;
},
installError)
.then(function (pack) {
vm.installState.status = labels.installStateInstalling;
vm.installState.progress = "75";
return packageResource.installData(pack);
},
installError)
.then(function (pack) {
vm.installState.status = labels.installStateComplete;
vm.installState.progress = "100";
return packageResource.cleanUp(pack);
},
installError)
.then(function (result) {
//Put the package data in local storage so we can use after reloading
localStorageService.set("packageInstallData", result);
vm.installState.status = labels.installStateCompleted;
vm.installCompleted = true;
}, installError);
}
function installError() {
//This will return a rejection meaning that the promise change above will stop
return $q.reject();
}
vm.reloadPage = function () {
//reload on next digest (after cookie)
$timeout(function () {
$window.location.reload(true);
});
}
}
angular.module("umbraco").controller("Umbraco.Editors.Packages.InstallLocalController", PackagesInstallLocalController);
})();

View File

@@ -0,0 +1,194 @@
<div ng-controller="Umbraco.Editors.Packages.InstallLocalController as vm">
<div class="umb-packages-view-wrapper" ng-if="vm.state === 'upload'">
<!-- Upload -->
<div class="flex items-center justify-center">
<ng-form novalidate name="localPackageForm" class="flex flex-column justify-center items-center tc">
<!-- Drag and drop files area -->
<div ngf-drop
ng-hide="hideDropzone"
ng-model="filesHolder"
ngf-change="handleFiles($files, $event, $invalidFiles)"
class="umb-upload-local__dropzone"
ngf-drag-over-class="'drag-over'"
ngf-multiple="false"
ngf-allow-dir="false"
ngf-max-size="{{maxFileSize}}"
ngf-pattern="'.zip'"
accept=".zip"
ng-class="{ 'is-small': compact !== 'false' || (done.length + queue.length) > 0 }">
<div class="content" draggable="false">
<!-- Drag and drop illustration -->
<umb-icon icon="icon-box"></umb-icon>
<small class="faded">
<strong><localize key="packager_dropHere">Drop to upload</localize></strong>
</small>
<!-- Select files -->
<div class="umb-upload-local__select-file"
ngf-select
ng-model="filesHolder"
ngf-change="handleFiles($files, $event, $invalidFiles)"
ngf-multiple="true"
ngf-max-size="{{maxFileSize}}"
ngf-pattern="'.zip'"
accept=".zip">
- <localize key="packager_orClickHereToUpload">or click here to choose files</localize>
</div>
</div>
</div>
<h3><strong><localize key="packager_uploadPackage">Upload package</localize></strong></h3>
<p class="faded">
<localize key="packager_localPackageDescription">Install a local package by selecting it from your machine. Only install packages from sources you know and trust</localize>.
</p>
</ng-form>
</div>
</div>
<div ng-if="vm.state === 'uploading'">
<umb-editor-sub-header>
<umb-editor-sub-header-content-left>
<button type="button" class="umb-package-details__back-action" ng-click="vm.state = 'upload'" prevent-default><span aria-hidden="true">&larr;</span> <localize key="packager_uploadAnother">Upload another package</localize></button>
</umb-editor-sub-header-content-left>
</umb-editor-sub-header>
<div class="umb-packages-view-wrapper">
<div class="flex items-center justify-center">
<div class="umb-info-local-items">
<div class="umb-package-icon">
<umb-icon icon="icon-box" ng-if="!vm.localPackage.iconUrl"></umb-icon>
</div>
<div class="umb-package-info">
<h4 class="umb-info-local-item"><strong><localize key="packager_installStateUploading">Uploading package...</localize></strong></h4>
<umb-progress-bar
percentage="{{vm.zipFile.uploadProgress}}">
</umb-progress-bar>
<div class="umb-info-local-item text-error mt3" ng-if="vm.zipFile.uploadStatus === 'error'">
{{ vm.zipFile.serverErrorMessage }}
</div>
</div>
</div>
</div>
</div>
</div>
<div ng-if="vm.state === 'packageDetails'">
<umb-editor-sub-header>
<umb-editor-sub-header-content-left>
<button type="button" class="umb-package-details__back-action" ng-click="vm.state = 'upload'" prevent-default><span aria-hidden="true">&larr;</span> <localize key="packager_cancelAndUploadAnother">Cancel and upload another package</localize></button>
</umb-editor-sub-header-content-left>
</umb-editor-sub-header>
<div class="umb-packages-view-wrapper">
<!-- Package details -->
<div class="flex items-center justify-center">
<div class="umb-info-local-items">
<form novalidate name="localPackageForm" class="w-100">
<div class="umb-package-icon">
<umb-icon ng-if="!vm.localPackage.iconUrl" icon="icon-box"></umb-icon>
<img ng-if="vm.localPackage.iconUrl" ng-src="{{vm.localPackage.iconUrl}}" alt="" />
</div>
<div class="umb-package-info">
<h4 class="umb-info-local-item"><strong>{{ vm.localPackage.name }}</strong></h4>
<div class="umb-info-local-item">
<strong><localize key="packager_packageAuthor">Author</localize></strong><br>
<a href="{{ vm.localPackage.authorUrl }}" target="_blank" rel="noopener">{{ vm.localPackage.author }}</a>
</div>
<div class="umb-info-local-item" ng-if="vm.localPackage.contributors && vm.localPackage.contributors.length > 0">
<strong><localize key="packager_packageContrib">Contributors</localize></strong><br>
<span>{{ vm.localPackage.contributors.join(', ')}}</span>
</div>
<div class="umb-info-local-item">
<strong><localize key="packager_packageVersion">Version</localize></strong><br>
{{ vm.localPackage.version }}
<p ng-if="vm.localPackage.originalVersion">
<small>
<em>
<localize key="packager_packageVersionUpgrade">Upgrading from version</localize>
{{ vm.localPackage.originalVersion }}
</em>
</small>
</p>
</div>
<div class="umb-info-local-item">
<strong><localize key="packager_packageLicense">License</localize></strong><br>
<a href="{{ vm.localPackage.licenseUrl }}" target="_blank" rel="noopener">{{ vm.localPackage.license }}</a>
</div>
<div class="umb-info-local-item">
<strong><localize key="packager_packageReadme">Read me</localize></strong><br>
<small ng-bind-html="vm.localPackage.readme"></small>
</div>
<div class="umb-info-local-item mt4 flex items-center flex-column" ng-if="vm.installState.status == '' && vm.localPackage.isCompatible">
<umb-checkbox model="vm.localPackage.packageLicenseAccept" required="true">
<strong class="label-text"><localize key="packager_accept">I accept</localize> <a href="#" ng-href="{{vm.localPackage.licenseUrl}}" target="_blank" rel="noopener"><localize key="packager_termsOfUse">terms of use</localize></a></strong>
</umb-checkbox>
<umb-button class="mt3"
type="button"
size="m"
button-style="success"
disabled="localPackageForm.$invalid"
action="vm.installPackage()"
label-key="packager_packageInstall">
</umb-button>
</div>
<div class="umb-info-local-item">
<umb-progress-bar ng-if="vm.installState.status !== ''"
percentage="{{vm.installState.progress}}">
</umb-progress-bar>
</div>
<div class="umb-info-local-item text-error" ng-if="!vm.localPackage.isCompatible">
<localize key="packager_targetVersionMismatch">This package cannot be installed, it requires a minimum Umbraco version of</localize> {{vm.localPackage.umbracoVersion}}
</div>
<div class="umb-info-local-item text-info">
<p>{{vm.installState.status}}</p>
</div>
<div class="umb-info-local-item text-info"
ng-show="vm.installCompleted">
<button type="button"
class="btn btn-success flex-inline mt3"
ng-click="vm.reloadPage()">
<localize key="packager_installFinish">Finish</localize>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>

View File

@@ -17,15 +17,12 @@
<ProjectReference Include="../Umbraco.Web.Website/Umbraco.Web.Website.csproj" />
<ProjectReference Include="../Umbraco.Persistence.SqlCe/Umbraco.Persistence.SqlCe.csproj" Condition="'$(OS)' == 'Windows_NT'" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.ICU.ICU4C.Runtime" Version="68.2.0.9" />
<RuntimeHostConfigurationOption
Condition="$(RuntimeIdentifier.StartsWith('linux')) Or $(RuntimeIdentifier.StartsWith('win')) Or ('$(RuntimeIdentifier)' == '' And !$([MSBuild]::IsOSPlatform('osx')))"
Include="System.Globalization.AppLocalIcu"
Value="68.2.0.9" />
<RuntimeHostConfigurationOption Condition="$(RuntimeIdentifier.StartsWith('linux')) Or $(RuntimeIdentifier.StartsWith('win')) Or ('$(RuntimeIdentifier)' == '' And !$([MSBuild]::IsOSPlatform('osx')))" Include="System.Globalization.AppLocalIcu" Value="68.2.0.9" />
</ItemGroup>
<ItemGroup>
@@ -39,6 +36,7 @@
<Compile Remove="umbraco/mediacache/**" />
<Compile Remove="wwwroot/umbraco/**" />
<Compile Remove="App_Data/**" />
<Compile Remove="Controllers\**" />
</ItemGroup>
<ItemGroup>
@@ -46,6 +44,7 @@
<EmbeddedResource Remove="umbraco/Data/**" />
<EmbeddedResource Remove="umbraco/logs/**" />
<EmbeddedResource Remove="umbraco/mediacache/**" />
<EmbeddedResource Remove="Controllers\**" />
</ItemGroup>
<ItemGroup>
@@ -55,6 +54,7 @@
<Content Remove="umbraco/mediacache/**" />
<Content Remove="umbraco\UmbracoWebsite\NotFound.cshtml" />
<Content Remove="wwwroot/Web.config" />
<Content Remove="Controllers\**" />
</ItemGroup>
@@ -72,6 +72,7 @@
<None Remove="umbraco/logs/**" />
<None Remove="umbraco/mediacache/**" />
<None Include="umbraco/UmbracoWebsite/NoNodes.cshtml" />
<None Remove="Controllers\**" />
</ItemGroup>
<!-- We don't want to include the generated files, they will throw a lot of errors -->