diff --git a/build/build.ps1 b/build/build.ps1 index 101fd72ffd..32b401aa53 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -298,7 +298,11 @@ # copy libs Write-Host "Copy SqlCE libraries" - $nugetPackages = [System.Environment]::ExpandEnvironmentVariables("%userprofile%\.nuget\packages") + $nugetPackages = $env:NUGET_PACKAGES + if (-not $nugetPackages) + { + $nugetPackages = [System.Environment]::ExpandEnvironmentVariables("%userprofile%\.nuget\packages") + } $this.CopyFiles("$nugetPackages\umbraco.sqlserverce\4.0.0.1\runtimes\win-x86\native", "*.*", "$tmp\bin\x86") $this.CopyFiles("$nugetPackages\umbraco.sqlserverce\4.0.0.1\runtimes\win-x64\native", "*.*", "$tmp\bin\amd64") $this.CopyFiles("$nugetPackages\umbraco.sqlserverce\4.0.0.1\runtimes\win-x86\native", "*.*", "$tmp\WebApp\bin\x86") diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 092de4d6d6..d18fb4b091 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -75,6 +75,26 @@ namespace Umbraco.Core /// public static bool VariesByCultureAndSegment(this PublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); + /// + /// Determines whether the property type is invariant. + /// + public static bool VariesByNothing(this PublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); + + /// + /// Determines whether the property type varies by culture. + /// + public static bool VariesByCulture(this PublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); + + /// + /// Determines whether the property type varies by segment. + /// + public static bool VariesBySegment(this PublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); + + /// + /// Determines whether the property type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this PublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); + /// /// Determines whether a variation is invariant. /// diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 3729ae1877..48ac39a630 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -113,6 +113,10 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{1350617A-4930-4D61-852F-E3AA9E692173}"); Chain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0 + Chain("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0 + Chain("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0 + Chain("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0 + Chain("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0 //FINAL @@ -127,13 +131,13 @@ namespace Umbraco.Core.Migrations.Upgrade From("{init-7.11.0}").Chain("{init-7.10.0}"); // same as 7.10.0 From("{init-7.11.1}").Chain("{init-7.10.0}"); // same as 7.10.0 - // 7.12.0 has a migration, define a custom chain which copies the chain - // going from {init-7.10.0} to former final, and then goes straight to - // main chain, skipping the migration + // 7.12.0 has migrations, define a custom chain which copies the chain + // going from {init-7.10.0} to former final (1350617A) , and then goes straight to + // main chain, skipping the migrations // From("{init-7.12.0}"); // copy from copy to (former final) main chain - CopyChain("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); + CopyChain("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/AddRelationTypeForMediaFolderOnDelete.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/AddRelationTypeForMediaFolderOnDelete.cs index baf4a9234e..349c39b980 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/AddRelationTypeForMediaFolderOnDelete.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/AddRelationTypeForMediaFolderOnDelete.cs @@ -15,8 +15,10 @@ namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 var exists = Context.Database.FirstOrDefault("WHERE alias=@alias", new { alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias }); if (exists == null) { - Insert.IntoTable(Constants.DatabaseSchema.Tables.RelationType).Row(new + var uniqueId = (Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias + "____" + Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName).ToGuid(); + Insert.IntoTable("umbracoRelationType").Row(new { + typeUniqueId = uniqueId, alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, name = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, childObjectType = Constants.ObjectTypes.MediaType, diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/IncreaseLanguageIsoCodeColumnLength.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/IncreaseLanguageIsoCodeColumnLength.cs index 302069855a..895a28728d 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/IncreaseLanguageIsoCodeColumnLength.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/IncreaseLanguageIsoCodeColumnLength.cs @@ -1,18 +1,30 @@ -using Umbraco.Core.Logging; -using Umbraco.Core.Migrations; -using Umbraco.Core.Persistence.SqlSyntax; +using System.Linq; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwelveZero +namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 { public class IncreaseLanguageIsoCodeColumnLength : MigrationBase { - public IncreaseLanguageIsoCodeColumnLength(IMigrationContext context) : base(context) - { - } + public IncreaseLanguageIsoCodeColumnLength(IMigrationContext context) + : base(context) + { } public override void Migrate() { - Delete.Index("IX_umbracoLanguage_languageISOCode").OnTable("umbracoLanguage").Do(); + var dbIndexes = SqlSyntax.GetDefinedIndexes(Context.Database) + .Select(x => new DbIndexDefinition + { + TableName = x.Item1, + IndexName = x.Item2, + ColumnName = x.Item3, + IsUnique = x.Item4 + }).ToArray(); + + //Ensure the index exists before dropping it + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoLanguage_languageISOCode"))) + { + Delete.Index("IX_umbracoLanguage_languageISOCode").OnTable("umbracoLanguage").Do(); + } Alter.Table("umbracoLanguage") .AlterColumn("languageISOCode") @@ -26,7 +38,5 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwelveZ .Unique() .Do(); } - - } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs index 08598f1779..d8f2d37067 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 if (Context?.Database == null) return; // We need to get all datatypes with an alias of "umbraco.tags" so we can loop over them and set the missing values if needed - var datatypes = Context.Database.Fetch("SELECT * FROM cmsDataType"); + var datatypes = Context.Database.Fetch(); var tagsDataTypes = datatypes.Where(x => string.Equals(x.EditorAlias, Constants.PropertyEditors.Aliases.Tags, StringComparison.InvariantCultureIgnoreCase)); foreach (var datatype in tagsDataTypes) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs index 942d32f83d..7596e3d7dd 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 public UpdateUmbracoConsent(IMigrationContext context) : base(context) { } - - public override void Migrate() { Alter.Table("umbracoConsent").AlterColumn("comment").AsString().Nullable().Do(); } + + public override void Migrate() { Alter.Table(Constants.DatabaseSchema.Tables.Consent).AlterColumn("comment").AsString().Nullable().Do(); } } } diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 46f024429e..09411f6d20 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -279,7 +279,7 @@ AnotherContentFinder public void Resolves_Trees() { var trees = _typeLoader.GetTrees(); - Assert.AreEqual(4, trees.Count()); + Assert.AreEqual(3, trees.Count()); } [Test] @@ -288,7 +288,7 @@ AnotherContentFinder var types = _typeLoader.GetDataEditors(); Assert.AreEqual(43, types.Count()); } - + /// /// This demonstrates this issue: http://issues.umbraco.org/issue/U4-3505 - the TypeList was returning a list of assignable types /// not explicit types which is sort of ideal but is confusing so we'll do it the less confusing way. diff --git a/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs index e972ccb43f..91ac3aacc2 100644 --- a/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs +++ b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs @@ -417,7 +417,6 @@ namespace Umbraco.Tests.CoreXml { var source = new TestSource5(); var nav = new NavigableNavigator(source); - TestContent content; Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); Assert.IsTrue(nav.MoveToFirstChild()); diff --git a/src/Umbraco.Tests/Models/Collections/Item.cs b/src/Umbraco.Tests/Models/Collections/Item.cs index dcb192bcdf..5d1ab24fb0 100644 --- a/src/Umbraco.Tests/Models/Collections/Item.cs +++ b/src/Umbraco.Tests/Models/Collections/Item.cs @@ -12,7 +12,6 @@ namespace Umbraco.Tests.Models.Collections public abstract class Item : IEntity, ICanBeDirty { private bool _hasIdentity; - private int? _hash; private int _id; private Guid _key; @@ -184,12 +183,6 @@ namespace Umbraco.Tests.Models.Collections public static bool operator ==(Item left, Item right) { - /*if (ReferenceEquals(null, left)) - return false; - - if(ReferenceEquals(null, right)) - return false;*/ - return ReferenceEquals(left, right); } diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 38990405c4..59cf30c0fb 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -945,8 +945,6 @@ namespace Umbraco.Tests.Scheduling throw new NotImplementedException(); } - private int i; - public async Task RunAsync(CancellationToken token) { Console.WriteLine("boom"); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js index 181d8a1881..a89477b001 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js @@ -1,6 +1,6 @@ /* - http://vitalets.github.io/checklist-model/ + https://vitalets.github.io/checklist-model/ @@ -19,7 +19,7 @@ angular.module('umbraco.directives') return false; } - // add + // add function add(arr, item) { arr = angular.isArray(arr) ? arr : []; for (var i = 0; i < arr.length; i++) { @@ -44,7 +44,7 @@ angular.module('umbraco.directives') return arr; } - // http://stackoverflow.com/a/19228302/1458162 + // https://stackoverflow.com/a/19228302/1458162 function postLinkFn(scope, elem, attrs) { // compile with `ng-model` pointing to `checked` $compile(elem)(scope); @@ -98,4 +98,4 @@ angular.module('umbraco.directives') return postLinkFn; } }; -}]); \ No newline at end of file +}]); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js index 4bc6f08eb9..66e93d70d8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js @@ -82,7 +82,7 @@ angular.module("umbraco.directives") generateAliasTimeout = $timeout(function () { updateAlias = true; - entityResource.getSafeAlias(value, true).then(function (safeAlias) { + entityResource.getSafeAlias(encodeURIComponent(value), true).then(function (safeAlias) { if (updateAlias) { scope.alias = safeAlias.alias; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js index ca389a74a7..a215bca645 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js @@ -161,7 +161,7 @@ /** * ACE editor session. * @type object - * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session} + * @see [EditSession]{@link https://ace.c9.io/#nav=api&api=edit_session} */ var session = acee.getSession(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorswatches.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorswatches.directive.js index 6ed65a9431..2b4004da11 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorswatches.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorswatches.directive.js @@ -29,9 +29,7 @@ Use this directive to generate color swatches to pick from. function link(scope, el, attr, ctrl) { scope.setColor = function (color) { - //scope.selectedColor({color: color }); scope.selectedColor = color; - if (scope.onSelect) { scope.onSelect(color); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js index 19ab789363..8d2b83d065 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js @@ -7,7 +7,7 @@ @description Added in Umbraco version 7.6 This directive is a wrapper of the bootstrap datetime picker version 3.1.3. Use it to render a date time picker. -For extra details about options and events take a look here: http://eonasdan.github.io/bootstrap-datetimepicker/ +For extra details about options and events take a look here: https://eonasdan.github.io/bootstrap-datetimepicker/ Use this directive to render a date time picker diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js index 7c96e0e050..7914dfc3f0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js @@ -1,7 +1,7 @@ /** * Konami Code directive for AngularJS * @version v0.0.1 - * @license MIT License, http://www.opensource.org/licenses/MIT + * @license MIT License, https://www.opensource.org/licenses/MIT */ angular.module('umbraco.directives') @@ -59,4 +59,4 @@ angular.module('umbraco.directives') scope.$on('$destroy', stopListening); } }; - }]); \ No newline at end of file + }]); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js index 104736530f..800ac87480 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js @@ -2,7 +2,7 @@ * @ngdoc directive * @name umbraco.directives.directive:noDirtyCheck * @restrict A -* @description Can be attached to form inputs to prevent them from setting the form as dirty (http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs) +* @description Can be attached to form inputs to prevent them from setting the form as dirty (https://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs) **/ function noDirtyCheck() { return { @@ -20,4 +20,4 @@ function noDirtyCheck() { } }; } -angular.module('umbraco.directives.validation').directive("noDirtyCheck", noDirtyCheck); \ No newline at end of file +angular.module('umbraco.directives.validation').directive("noDirtyCheck", noDirtyCheck); diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index 651c54bfeb..615a7aeec9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -367,7 +367,7 @@ angular.module('umbraco.mocks'). "installer_databaseHeader": "Database configuration", "installer_databaseInstall": " Press the install button to install the Umbraco %0% database ", "installer_databaseInstallDone": "Umbraco %0% has now been copied to your database. Press Next to proceed.", - "installer_databaseNotFound": "

Database not found! Please check that the information in the 'connection string' of the \"web.config\" file is correct.

To proceed, please edit the 'web.config' file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named 'UmbracoDbDSN' and save the file.

Click the retry button when done.
More information on editing web.config here.

", + "installer_databaseNotFound": "

Database not found! Please check that the information in the 'connection string' of the \"web.config\" file is correct.

To proceed, please edit the 'web.config' file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named 'UmbracoDbDSN' and save the file.

Click the retry button when done.
More information on editing web.config here.

", "installer_databaseText": "To complete this step, you must know some information regarding your database server ('connection string').
Please contact your ISP if necessary. If you're installing on a local machine or server you might need information from your system administrator.", "installer_databaseUpgrade": "

Press the upgrade button to upgrade your database to Umbraco %0%

Don't worry - no content will be deleted and everything will continue working afterwards!

", "installer_databaseUpgradeDone": "Your database has been upgraded to the final version %0%.
Press Next to proceed. ", @@ -395,7 +395,7 @@ angular.module('umbraco.mocks'). "installer_permissionsSettingUpPermissions": "Setting up folder permissions", "installer_permissionsText": " Umbraco needs write/modify access to certain directories in order to store files like pictures and PDF's. It also stores temporary data (aka: cache) for enhancing the performance of your website. ", "installer_runwayFromScratch": "I want to start from scratch", - "installer_runwayFromScratchText": " Your website is completely empty at the moment, so that's perfect if you want to start from scratch and create your own document types and templates. (learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. ", + "installer_runwayFromScratchText": " Your website is completely empty at the moment, so that's perfect if you want to start from scratch and create your own document types and templates. (learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. ", "installer_runwayHeader": "You've just set up a clean Umbraco platform. What do you want to do next?", "installer_runwayInstalled": "Runway is installed", "installer_runwayInstalledText": " You have the foundation in place. Select what modules you wish to install on top of it.
This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules ", @@ -432,7 +432,7 @@ angular.module('umbraco.mocks'). "login_greeting6": "Happy friendly Friday", "login_greeting7": "Happy shiny Saturday", "login_instruction": "Log in below:", - "login_bottomText": "

© 2001 - %0%
Umbraco.org

", + "login_bottomText": "

© 2001 - %0%
Umbraco.org

", "main_dashboard": "Dashboard", "main_sections": "Sections", "main_tree": "Content", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index cec9cf191e..c6842513b2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -293,7 +293,7 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca createContainer: function (parentId, name) { return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateContainer", { parentId: parentId, name: name })), + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateContainer", { parentId: parentId, name: encodeURIComponent(name) })), 'Failed to create a folder under parent id ' + parentId); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js index 7c18e91364..118cda1a4c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js @@ -355,7 +355,7 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { umbRequestHelper.getApiUrl( "dataTypeApiBaseUrl", "PostCreateContainer", - { parentId: parentId, name: name })), + { parentId: parentId, name: encodeURIComponent(name) })), 'Failed to create a folder under parent id ' + parentId); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index e3af5124f8..6647c6fb7f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -46,7 +46,7 @@ function entityResource($q, $http, umbRequestHelper) { $http.get( umbRequestHelper.getApiUrl( "entityApiBaseUrl", - "GetSafeAlias", { value: value, camelCase: camelCase })), + "GetSafeAlias", { value: encodeURIComponent(value), camelCase: camelCase })), 'Failed to retrieve content type scaffold'); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index a98398f3ab..3fd0bdd1fd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -244,7 +244,7 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { umbRequestHelper.getApiUrl( "mediaTypeApiBaseUrl", "PostCreateContainer", - { parentId: parentId, name: name })), + { parentId: parentId, name: encodeURIComponent(name) })), 'Failed to create a folder under parent id ' + parentId); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/help.service.js b/src/Umbraco.Web.UI.Client/src/common/services/help.service.js index 1de992d9db..ff8d487bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/help.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/help.service.js @@ -3,7 +3,7 @@ angular.module('umbraco.services') var helpTopics = {}; var defaultUrl = "https://our.umbraco.com/rss/help"; - var tvUrl = "http://umbraco.tv/feeds/help"; + var tvUrl = "https://umbraco.tv/feeds/help"; function getCachedHelp(url){ if(helpTopics[url]){ @@ -85,4 +85,4 @@ angular.module('umbraco.services') return service; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 42b739fe61..a4432fd1ee 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -497,7 +497,7 @@ }); //we need to use 'apply' to call intersection with an array of arrays, - //see: http://stackoverflow.com/a/16229480/694494 + //see: https://stackoverflow.com/a/16229480/694494 var intersectPermissions = _.intersection.apply(_, arr); return { diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html index ff0ec56620..5fb9d9bda8 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html @@ -25,21 +25,21 @@
-
- - + + - At least {{installer.current.model.minCharLength}} characters long - - - At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} - -
+ At least {{installer.current.model.minCharLength}} characters long + + + At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} + +
diff --git a/src/Umbraco.Web.UI.Client/src/less/application/animations.less b/src/Umbraco.Web.UI.Client/src/less/application/animations.less index 53793b5616..0f2dcb7a7e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/animations.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/animations.less @@ -1,8 +1,8 @@ // Animations // ------------------------- -//Animate.css - http://daneden.me/animate -//Licensed under the MIT license - http://opensource.org/licenses/MIT +//Animate.css - https://daneden.github.io/animate.css/ +//Licensed under the MIT license - https://opensource.org/licenses/MIT //Copyright (c) 2013 Daniel Eden .animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}@-webkit-keyframes bounce{0%,100%,20%,53%,80%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1);-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}40%,43%{-webkit-transition-timing-function:cubic-bezier(0.755,.050,.855,.060);transition-timing-function:cubic-bezier(0.755,.050,.855,.060);-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}70%{-webkit-transition-timing-function:cubic-bezier(0.755,.050,.855,.060);transition-timing-function:cubic-bezier(0.755,.050,.855,.060);-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,100%,20%,53%,80%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1);-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}40%,43%{-webkit-transition-timing-function:cubic-bezier(0.755,.050,.855,.060);transition-timing-function:cubic-bezier(0.755,.050,.855,.060);-webkit-transform:translate3d(0,-30px,0);-ms-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}70%{-webkit-transition-timing-function:cubic-bezier(0.755,.050,.855,.060);transition-timing-function:cubic-bezier(0.755,.050,.855,.060);-webkit-transform:translate3d(0,-15px,0);-ms-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);-ms-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;-ms-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,100%,50%{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,100%,50%{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes pulse{0%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);-ms-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}100%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(0.75,1.25,1);transform:scale3d(0.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes rubberBand{0%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}30%{-webkit-transform:scale3d(1.25,.75,1);-ms-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(0.75,1.25,1);-ms-transform:scale3d(0.75,1.25,1);transform:scale3d(0.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);-ms-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);-ms-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);-ms-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}100%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,100%{-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);-ms-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);-ms-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes swing{20%{-webkit-transform:rotate3d(0,0,1,15deg);transform:rotate3d(0,0,1,15deg)}40%{-webkit-transform:rotate3d(0,0,1,-10deg);transform:rotate3d(0,0,1,-10deg)}60%{-webkit-transform:rotate3d(0,0,1,5deg);transform:rotate3d(0,0,1,5deg)}80%{-webkit-transform:rotate3d(0,0,1,-5deg);transform:rotate3d(0,0,1,-5deg)}100%{-webkit-transform:rotate3d(0,0,1,0deg);transform:rotate3d(0,0,1,0deg)}}@keyframes swing{20%{-webkit-transform:rotate3d(0,0,1,15deg);-ms-transform:rotate3d(0,0,1,15deg);transform:rotate3d(0,0,1,15deg)}40%{-webkit-transform:rotate3d(0,0,1,-10deg);-ms-transform:rotate3d(0,0,1,-10deg);transform:rotate3d(0,0,1,-10deg)}60%{-webkit-transform:rotate3d(0,0,1,5deg);-ms-transform:rotate3d(0,0,1,5deg);transform:rotate3d(0,0,1,5deg)}80%{-webkit-transform:rotate3d(0,0,1,-5deg);-ms-transform:rotate3d(0,0,1,-5deg);transform:rotate3d(0,0,1,-5deg)}100%{-webkit-transform:rotate3d(0,0,1,0deg);-ms-transform:rotate3d(0,0,1,0deg);transform:rotate3d(0,0,1,0deg)}}.swing{-webkit-transform-origin:top center;-ms-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg);transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg);transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg);transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes tada{0%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg);-ms-transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg);transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg);-ms-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg);transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg);-ms-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg);transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg)}100%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg);transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg);transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg);transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg);transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg);transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg)}100%{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;-ms-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg);-ms-transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg);transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg);-ms-transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg);transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg);-ms-transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg);transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg);-ms-transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg);transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg);-ms-transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg);transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg)}100%{-webkit-transform:none;-ms-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes bounceIn{0%,100%,20%,40%,60%,80%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes bounceIn{0%,100%,20%,40%,60%,80%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);-ms-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);-ms-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);-ms-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);-ms-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);-ms-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn;-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounceInDown{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}100%{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);-ms-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);-ms-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);-ms-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);-ms-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}100%{-webkit-transform:none;-ms-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}100%{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);-ms-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);-ms-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);-ms-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);-ms-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}100%{-webkit-transform:none;-ms-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}100%{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);-ms-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);-ms-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);-ms-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);-ms-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}100%{-webkit-transform:none;-ms-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}@keyframes bounceInUp{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);-ms-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);-ms-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);-ms-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}100%{-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);-ms-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);-ms-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);-ms-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut;-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);-ms-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);-ms-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);-ms-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);-ms-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);-ms-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);-ms-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);-ms-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);-ms-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);-ms-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);-ms-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);-ms-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);-ms-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);-ms-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);-ms-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);-ms-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);-ms-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);-ms-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,100%,0);-ms-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);-ms-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-100%,0,0);-ms-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);-ms-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0);-ms-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);-ms-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-100%,0);-ms-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);-ms-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-360deg);transform:perspective(400px) rotate3d(0,1,0,-360deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}100%{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-360deg);-ms-transform:perspective(400px) rotate3d(0,1,0,-360deg);transform:perspective(400px) rotate3d(0,1,0,-360deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);-ms-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);-ms-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);-ms-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}100%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;-ms-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);-ms-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);-ms-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);-ms-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);-ms-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;-ms-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-20deg);transform:perspective(400px) rotate3d(0,1,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,1,0,10deg);transform:perspective(400px) rotate3d(0,1,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-5deg);transform:perspective(400px) rotate3d(0,1,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);-ms-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-20deg);-ms-transform:perspective(400px) rotate3d(0,1,0,-20deg);transform:perspective(400px) rotate3d(0,1,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,1,0,10deg);-ms-transform:perspective(400px) rotate3d(0,1,0,10deg);transform:perspective(400px) rotate3d(0,1,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-5deg);-ms-transform:perspective(400px) rotate3d(0,1,0,-5deg);transform:perspective(400px) rotate3d(0,1,0,-5deg)}100%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;-ms-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);-ms-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);-ms-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-backface-visibility:visible!important;-ms-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-15deg);transform:perspective(400px) rotate3d(0,1,0,-15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-15deg);-ms-transform:perspective(400px) rotate3d(0,1,0,-15deg);transform:perspective(400px) rotate3d(0,1,0,-15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);-ms-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;-ms-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY;-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg);opacity:1}100%{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);-ms-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);-ms-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);-ms-transform:skewX(-5deg);transform:skewX(-5deg);opacity:1}100%{-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}100%{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}100%{-webkit-transform:translate3d(100%,0,0) skewX(30deg);-ms-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{-webkit-transform-origin:center;transform-origin:center;-webkit-transform:rotate3d(0,0,1,-200deg);transform:rotate3d(0,0,1,-200deg);opacity:0}100%{-webkit-transform-origin:center;transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;-webkit-transform:rotate3d(0,0,1,-200deg);-ms-transform:rotate3d(0,0,1,-200deg);transform:rotate3d(0,0,1,-200deg);opacity:0}100%{-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}100%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,-45deg);-ms-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}100%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,45deg);transform:rotate3d(0,0,1,45deg);opacity:0}100%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,45deg);-ms-transform:rotate3d(0,0,1,45deg);transform:rotate3d(0,0,1,45deg);opacity:0}100%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,45deg);transform:rotate3d(0,0,1,45deg);opacity:0}100%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,45deg);-ms-transform:rotate3d(0,0,1,45deg);transform:rotate3d(0,0,1,45deg);opacity:0}100%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,-90deg);transform:rotate3d(0,0,1,-90deg);opacity:0}100%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,-90deg);-ms-transform:rotate3d(0,0,1,-90deg);transform:rotate3d(0,0,1,-90deg);opacity:0}100%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{-webkit-transform-origin:center;transform-origin:center;opacity:1}100%{-webkit-transform-origin:center;transform-origin:center;-webkit-transform:rotate3d(0,0,1,200deg);transform:rotate3d(0,0,1,200deg);opacity:0}}@keyframes rotateOut{0%{-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;opacity:1}100%{-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;-webkit-transform:rotate3d(0,0,1,200deg);-ms-transform:rotate3d(0,0,1,200deg);transform:rotate3d(0,0,1,200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{-webkit-transform-origin:left bottom;transform-origin:left bottom;opacity:1}100%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate(0,0,1,45deg);transform:rotate(0,0,1,45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;opacity:1}100%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate(0,0,1,45deg);-ms-transform:rotate(0,0,1,45deg);transform:rotate(0,0,1,45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{-webkit-transform-origin:right bottom;transform-origin:right bottom;opacity:1}100%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;opacity:1}100%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,-45deg);-ms-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{-webkit-transform-origin:left bottom;transform-origin:left bottom;opacity:1}100%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;opacity:1}100%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,-45deg);-ms-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{-webkit-transform-origin:right bottom;transform-origin:right bottom;opacity:1}100%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,90deg);transform:rotate3d(0,0,1,90deg);opacity:0}}@keyframes rotateOutUpRight{0%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;opacity:1}100%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,90deg);-ms-transform:rotate3d(0,0,1,90deg);transform:rotate3d(0,0,1,90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate3d(0,0,1,80deg);transform:rotate3d(0,0,1,80deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate3d(0,0,1,60deg);transform:rotate3d(0,0,1,60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}100%{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{-webkit-transform-origin:top left;-ms-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate3d(0,0,1,80deg);-ms-transform:rotate3d(0,0,1,80deg);transform:rotate3d(0,0,1,80deg);-webkit-transform-origin:top left;-ms-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate3d(0,0,1,60deg);-ms-transform:rotate3d(0,0,1,60deg);transform:rotate3d(0,0,1,60deg);-webkit-transform-origin:top left;-ms-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}100%{-webkit-transform:translate3d(0,700px,0);-ms-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg);transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg);-ms-transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg);transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg);transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg)}}@keyframes rollOut{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg);-ms-transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg);transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);-ms-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-ms-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-ms-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-ms-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-ms-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-ms-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-ms-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-ms-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-ms-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}100%{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);-ms-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}100%{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}100%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-ms-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}100%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-ms-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;-ms-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}100%{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);-ms-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}100%{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);-ms-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;-ms-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}100%{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);-ms-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}100%{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);-ms-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;-ms-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}100%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-ms-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}100%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-ms-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;-ms-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/check-circle.less b/src/Umbraco.Web.UI.Client/src/less/components/check-circle.less index a4c3a89c99..fadf9c7940 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/check-circle.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/check-circle.less @@ -1,6 +1,5 @@ .check_circle { display: flex; - opacity: 0; width: 20px; height: 20px; margin: 0 auto; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less index a307e5c585..5363a8db9b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less @@ -1,5 +1,10 @@ .umb-prevalues-multivalues { width: 400px; + max-width: 100%; + + .umb-overlay & { + width: 500px; + } } .umb-prevalues-multivalues__left { @@ -27,7 +32,7 @@ .umb-prevalues-multivalues__add button { margin: 0 6px 0 0; - float: right + margin-left: auto; } .umb-prevalues-multivalues__listitem { @@ -46,6 +51,7 @@ .umb-prevalues-multivalues__listitem a { cursor: pointer; + margin-left: auto; } .umb-prevalues-multivalues__listitem input { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less index e27bd0d654..c7905879d8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less @@ -18,12 +18,6 @@ box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); } - &.active { - .check_circle { - opacity: 1; - } - } - &.umb-color-box--m { width: 40px; height: 40px; diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index fd9296259f..9b21a1667c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -139,12 +139,6 @@ ul.color-picker li { border: 2px solid transparent; width: 60px; - &.active { - .check_circle { - opacity: 1; - } - } - .thumbnail{ min-width: auto; width: inherit; @@ -180,13 +174,13 @@ ul.color-picker li { div.color-picker-prediv { display: inline-flex; align-items: center; + max-width: 80%; pre { - display: inline; + display: inline-flex; font-family: monospace; margin-right: 10px; margin-left: 10px; - width: 50%; white-space: nowrap; overflow: hidden; margin-bottom: 0; @@ -194,21 +188,28 @@ ul.color-picker li { padding-top: 7px; padding-bottom: 7px; background: #f7f7f7; + flex: 0 0 auto; } span { margin-left: 5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } } input[type="text"] { - min-width: 40%; - width: 320px; - display: inline-block; + display: flex; + flex: 1 1 100px; margin-top: 1px; + margin-right: 15px; + min-width: auto; + width: auto; } .sp-replacer { + display: inline-flex; margin-right: 18px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 35481af343..c7d2e8d6ed 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -126,7 +126,7 @@ .color-green, .color-green i{color: @green-d1 !important;} .color-yellow, .color-yellow i{color: @yellowIcon !important;} -/* Colors based on http://zavoloklom.github.io/material-design-color-palette/colors.html */ +/* Colors based on https://zavoloklom.github.io/material-design-color-palette/colors.html */ .btn-color-black {background-color: @black;} .color-black i { color: @black;} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index 46acddddb4..0e777a5b9f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -96,7 +96,7 @@
- +
Visit umbraco.tv
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js index f428e64e3e..05c76beeae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js @@ -13,6 +13,28 @@ function IconPickerController($scope, iconHelper, localizationService) { vm.selectIcon = selectIcon; vm.close = close; + vm.colors = [ + { name: "Black", value: "color-black" }, + { name: "Blue Grey", value: "color-blue-grey" }, + { name: "Grey", value: "color-grey" }, + { name: "Brown", value: "color-brown" }, + { name: "Blue", value: "color-blue" }, + { name: "Light Blue", value: "color-light-blue" }, + { name: "Indigo", value: "color-indigo" }, + { name: "Purple", value: "color-purple" }, + { name: "Deep Purple", value: "color-deep-purple" }, + { name: "Cyan", value: "color-cyan" }, + { name: "Green", value: "color-green" }, + { name: "Light Green", value: "color-light-green" }, + { name: "Lime", value: "color-lime" }, + { name: "Yellow", value: "color-yellow" }, + { name: "Amber", value: "color-amber" }, + { name: "Orange", value: "color-orange" }, + { name: "Deep Orange", value: "color-deep-orange" }, + { name: "Red", value: "color-red" }, + { name: "Pink", value: "color-pink" } + ]; + function onInit() { vm.loading = true; @@ -24,6 +46,12 @@ function IconPickerController($scope, iconHelper, localizationService) { vm.loading = false; }); + // set a default color if nothing is passed in + vm.color = $scope.model.color ? $scope.model.color : vm.colors[0].value; + + // if an icon is passed in - preselect it + vm.icon = $scope.model.icon ? $scope.model.icon : undefined; + } function setTitle() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html index 320e0cee8a..8a7358116a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html @@ -32,35 +32,19 @@
- + +
-
    -
  • - +
      +
    • +
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-color-swatches.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-color-swatches.html index eae299e579..d3105d52a1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-color-swatches.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-color-swatches.html @@ -1,7 +1,7 @@ 
      - diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardVideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardVideos.html index 1a83a5e329..5b42657ddd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardVideos.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardVideos.html @@ -1,9 +1,9 @@

      Hours of Umbraco training videos are only a click away

      -

      Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

      +

      Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

      To get you started:

        diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html index d8d3d127eb..5b4bc988c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html @@ -5,10 +5,10 @@

        Find out more:

        diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html index 02ec1e6dd8..680f7cc889 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html @@ -1,7 +1,7 @@

        Hours of Umbraco training videos are only a click away

        -

        Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco, then visit umbraco.tv for even more Umbraco videos.

        +

        Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco, then visit umbraco.tv for even more Umbraco videos.

        -
        diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/desktopmediauploader.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/desktopmediauploader.html index 59a50f44fb..10609bf497 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/desktopmediauploader.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/desktopmediauploader.html @@ -8,7 +8,7 @@

        Download Desktop Media Uploader now.

        - This application requires Adobe® AIR™ to be installed for Mac OS or Windows. + This application requires Adobe® AIR™ to be installed for Mac OS or Windows.

        diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardintro.html index 0af91b4544..b8c28bbd39 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardintro.html @@ -10,4 +10,4 @@
      • Drag the files and folders you wish to upload directly into the Desktop Media Uploader application
      • Click Upload to start uploading
      -

      For a more thorough guide on how to use the Desktop Media Uploader, checkout this video.

      +

      For a more thorough guide on how to use the Desktop Media Uploader, checkout this video.

      diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardvideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardvideos.html index 08e7fb3c41..f04aff24c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardvideos.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardvideos.html @@ -1,9 +1,9 @@

      Hours of Umbraco training videos are only a click away

      -

      Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

      +

      Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

      To get you started:

        @@ -15,4 +15,4 @@
    -
\ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html index 8037e00f58..10e9983e05 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html @@ -7,9 +7,9 @@
  • Read more about working with the Items in Settings in the Documentation section of Our Umbraco
  • Download the Editors Manual for details on working with the Umbraco UI
  • Ask a question in the Community Forum
  • -
  • Watch our tutorial videos (some are free, some require a subscription)
  • -
  • Find out about our productivity boosting tools and commercial support
  • -
  • Find out about real-life training and certification opportunities
  • +
  • Watch our tutorial videos (some are free, some require a subscription)
  • +
  • Find out about our productivity boosting tools and commercial support
  • +
  • Find out about real-life training and certification opportunities
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html index 1cb61f4168..f726800696 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html @@ -5,9 +5,9 @@
      -
    • +
    • -
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html index 7d88fb068f..a70ecabf35 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html @@ -1,4 +1,4 @@ -
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index 8aea4fb97f..c72f8fc777 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -159,7 +159,7 @@ placeholder: 'sortable-placeholder', forcePlaceholderSize: true, helper: function (e, ui) { - // When sorting table rows, the cells collapse. This helper fixes that: http://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/ + // When sorting table rows, the cells collapse. This helper fixes that: https://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/ ui.children().each(function () { $(this).width($(this).width()); }); @@ -181,7 +181,7 @@ start: function (e, ui) { //ui.placeholder.html(""); - // Build a placeholder cell that spans all the cells in the row: http://stackoverflow.com/questions/25845310/jquery-ui-sortable-and-table-cell-size + // Build a placeholder cell that spans all the cells in the row: https://stackoverflow.com/questions/25845310/jquery-ui-sortable-and-table-cell-size var cellCount = 0; $('td, th', ui.helper).each(function () { // For each td or th try and get it's colspan attribute, and add that or 1 to the total diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index 12482f562b..56490406f3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -79,7 +79,7 @@ angular.module("umbraco") addTag($scope.tagToAdd); $scope.tagToAdd = ""; //this clears the value stored in typeahead so it doesn't try to add the text again - // http://issues.umbraco.org/issue/U4-4947 + // https://issues.umbraco.org/issue/U4-4947 $typeahead.typeahead('val', ''); }; diff --git a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js index ddf38733e1..b5921b6576 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js +++ b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js @@ -1,145 +1,145 @@ -Umbraco.Sys.registerNamespace("Umbraco.Dialogs"); - -(function ($) { - - // register AssignDomain dialog - Umbraco.Dialogs.AssignDomain2 = base2.Base.extend({ - - _opts: null, - - _isRepeated: function (element) { - var inputs = $('form input.domain'); - var elementName = element.attr('name'); - var repeated = false; - inputs.each(function() { - var input = $(this); - if (input.attr('name') != elementName && input.val() == element.val()) - repeated = true; - }); - return repeated; - }, - - // constructor - constructor: function (opts) { - // merge options with default - this._opts = $.extend({ - invalidDomain: 'Invalid domain.', - duplicateDomain: 'Domain has already been assigned.' - }, opts); - }, - - // public methods/variables - - languages: null, - language: null, - domains: null, - - addDomain: function () { - this.domains.push({ - Name: "", - Lang: "" - }); - }, - - init: function () { - var self = this; - - self.domains = ko.observableArray(self._opts.domains); - self.languages = self._opts.languages; - self.language = self._opts.language; - self.removeDomain = function() { self.domains.remove(this); }; - - ko.applyBindings(self); - - $.validator.addMethod("domain", function (value, element, param) { - // beware! encode('test') == 'test-' - // read eg https://rt.cpan.org/Public/Bug/Display.html?id=94347 - value = punycode.encode(value); - // that regex is best-effort and certainly not exact - var re = /^(http[s]?:\/\/)?([-\w]+(\.[-\w]+)*)(:\d+)?(\/[-\w]*|-)?$/gi; - var isopt = this.optional(element); - var retest = re.test(value); - var ret = isopt || retest; - return ret; - }, self._opts.invalidDomain); - - function getDuplicateMessage(val, el) { - var other = $(el).nextAll('input').val(); - var msg = self._opts.duplicateDomain - if (other != "" && other != "!!!") - msg = msg + ' (' + other + ')'; - return msg; - } - - $.validator.addMethod("duplicate", function (value, element, param) { - return $(element).nextAll('input').val() == "" && !self._isRepeated($(element)); - }, getDuplicateMessage); - - $.validator.addClassRules({ - domain: { domain: true }, - duplicate: { duplicate: true } - }); - - $('form').validate({ - debug: true, - focusCleanup: true, - onkeyup: false - }); - - $('form input.domain').on('focus', function(event) { - if (event.type != 'focusin') return; - $(this).nextAll('input').val(""); - }); - - // force validation *now* - $('form').valid(); - - $('#btnSave').click(function () { - if (!$('form').valid()) - return false; - - var mask = $('#komask'); - var masked = mask.parent(); - mask.height(masked.height()); - mask.width(masked.width()); - mask.show(); - - var data = { nodeId: self._opts.nodeId, language: self.language ? self.language : 0, domains: self.domains }; - $.post(self._opts.restServiceLocation + 'PostSaveLanguageAndDomains', ko.toJSON(data), function (json) { - mask.hide(); - - if (json.Valid) { - UmbClientMgr.closeModalWindow(); - } - else { - var inputs = $('form input.domain'); - inputs.each(function() { $(this).nextAll('input').val(""); }); - for (var i = 0; i < json.Domains.length; i++) { - var d = json.Domains[i]; - if (d.Duplicate) - inputs.each(function() { - var input = $(this); - if (input.val() == d.Name) - input.nextAll('input').val(d.Other ? d.Other : "!!!"); - }); - } - $('form').valid(); - } - }) - .fail(function (xhr, textStatus, errorThrown) { - mask.css('opacity', 1).css('color', "#ff0000").html(xhr.responseText); - }); - return false; - }); - } - - }); - - // set defaults for jQuery ajax calls - $.ajaxSetup({ - dataType: 'json', - cache: false, - contentType: 'application/json; charset=utf-8' - }); - -})(jQuery); +Umbraco.Sys.registerNamespace("Umbraco.Dialogs"); + +(function ($) { + + // register AssignDomain dialog + Umbraco.Dialogs.AssignDomain2 = base2.Base.extend({ + + _opts: null, + + _isRepeated: function (element) { + var inputs = $('form input.domain'); + var elementName = element.attr('name'); + var repeated = false; + inputs.each(function() { + var input = $(this); + if (input.attr('name') != elementName && input.val() == element.val()) + repeated = true; + }); + return repeated; + }, + + // constructor + constructor: function (opts) { + // merge options with default + this._opts = $.extend({ + invalidDomain: 'Invalid domain.', + duplicateDomain: 'Domain has already been assigned.' + }, opts); + }, + + // public methods/variables + + languages: null, + language: null, + domains: null, + + addDomain: function () { + this.domains.push({ + Name: "", + Lang: "" + }); + }, + + init: function () { + var self = this; + + self.domains = ko.observableArray(self._opts.domains); + self.languages = self._opts.languages; + self.language = self._opts.language; + self.removeDomain = function() { self.domains.remove(this); }; + + ko.applyBindings(self); + + $.validator.addMethod("domain", function (value, element, param) { + // beware! encode('test') == 'test-' + // read eg https://rt.cpan.org/Public/Bug/Display.html?id=94347 + value = punycode.encode(value); + // that regex is best-effort and certainly not exact + var re = /^(http[s]?:\/\/)?([-\w]+(\.[-\w]+)*)(:\d+)?(\/[-\w]*|-)?$/gi; + var isopt = this.optional(element); + var retest = re.test(value); + var ret = isopt || retest; + return ret; + }, self._opts.invalidDomain); + + function getDuplicateMessage(val, el) { + var other = $(el).nextAll('input').val(); + var msg = self._opts.duplicateDomain + if (other != "" && other != "!!!") + msg = msg + ' (' + other + ')'; + return msg; + } + + $.validator.addMethod("duplicate", function (value, element, param) { + return $(element).nextAll('input').val() == "" && !self._isRepeated($(element)); + }, getDuplicateMessage); + + $.validator.addClassRules({ + domain: { domain: true }, + duplicate: { duplicate: true } + }); + + $('form').validate({ + debug: true, + focusCleanup: true, + onkeyup: false + }); + + $('form input.domain').on('focus', function(event) { + if (event.type != 'focusin') return; + $(this).nextAll('input').val(""); + }); + + // force validation *now* + $('form').valid(); + + $('#btnSave').click(function () { + if (!$('form').valid()) + return false; + + var mask = $('#komask'); + var masked = mask.parent(); + mask.height(masked.height()); + mask.width(masked.width()); + mask.show(); + + var data = { nodeId: self._opts.nodeId, language: self.language ? self.language : 0, domains: self.domains }; + $.post(self._opts.restServiceLocation + 'PostSaveLanguageAndDomains', ko.toJSON(data), function (json) { + mask.hide(); + + if (json.Valid) { + UmbClientMgr.closeModalWindow(); + } + else { + var inputs = $('form input.domain'); + inputs.each(function() { $(this).nextAll('input').val(""); }); + for (var i = 0; i < json.Domains.length; i++) { + var d = json.Domains[i]; + if (d.Duplicate) + inputs.each(function() { + var input = $(this); + if (input.val() == d.Name) + input.nextAll('input').val(d.Other ? d.Other : "!!!"); + }); + } + $('form').valid(); + } + }) + .fail(function (xhr, textStatus, errorThrown) { + mask.css('opacity', 1).css('color', "#ff0000").html(xhr.responseText); + }); + return false; + }); + } + + }); + + // set defaults for jQuery ajax calls + $.ajaxSetup({ + dataType: 'json', + cache: false, + contentType: 'application/json; charset=utf-8' + }); + +})(jQuery); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 1c61d3910d..7b5b728e7d 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1,1471 +1,1471 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Binders; -using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Web.PublishedCache; -using Umbraco.Core.Events; -using Umbraco.Core.Models.Validation; -using Umbraco.Web.Models; -using Umbraco.Web.WebServices; -using Umbraco.Web._Legacy.Actions; -using Constants = Umbraco.Core.Constants; -using ContentVariation = Umbraco.Core.Models.ContentVariation; -using Language = Umbraco.Web.Models.ContentEditing.Language; - -namespace Umbraco.Web.Editors -{ - /// - /// The API controller used for editing content - /// - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the content application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorize(Constants.Applications.Content)] - [ContentControllerConfiguration] - public class ContentController : ContentControllerBase - { - private readonly IPublishedSnapshotService _publishedSnapshotService; - - public ContentController(IPublishedSnapshotService publishedSnapshotService) - { - if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService)); - _publishedSnapshotService = publishedSnapshotService; - } - - /// - /// Configures this controller with a custom action selector - /// - private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration - { - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) - )); - } - } - - /// - /// Returns true if any content types have culture variation enabled - /// - /// - [HttpGet] - [WebApi.UmbracoAuthorize, OverrideAuthorization] - public bool AllowsCultureVariation() - { - var contentTypes = Services.ContentTypeService.GetAll(); - return contentTypes.Any(contentType => contentType.VariesByCulture()); - } - - /// - /// Return content for the specified ids - /// - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - //fixme what about cultures? - - var foundContent = Services.ContentService.GetByIds(ids); - return foundContent.Select(x => MapToDisplay(x)); - } - - /// - /// Updates the permissions for a content item for a particular user group - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to update - /// - [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] - public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) - { - if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be alowed to set granular permissions? - - var content = Services.ContentService.GetById(saveModel.ContentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //current permissions explicitly assigned to this content item - var contentPermissions = Services.ContentService.GetPermissions(content) - .ToDictionary(x => x.UserGroupId, x => x); - - var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); - - //loop through each user group - foreach (var userGroup in allUserGroups) - { - //check if there's a permission set posted up for this user group - IEnumerable groupPermissions; - if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) - { - //create a string collection of the assigned letters - var groupPermissionCodes = groupPermissions.ToArray(); - - //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions - //for this group/node which will go back to the defaults - if (groupPermissionCodes.Length == 0) - { - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored - else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) - { - //only remove them if they are actually currently assigned - if (contentPermissions.ContainsKey(userGroup.Id)) - { - //remove these permissions from this node for this group since the ones being assigned are the same as the defaults - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - } - //if they are different we need to update, otherwise there's nothing to update - else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) - { - - Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); - } - } - } - - return GetDetailedPermissions(content, allUserGroups); - } - - /// - /// Returns the user group permissions for user groups assigned to this node - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to view - /// - [EnsureUserPermissionForContent("contentId", 'R')] - public IEnumerable GetDetailedPermissions(int contentId) - { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be able to see detailed permissions? - - var allUserGroups = Services.UserService.GetAllUserGroups(); - - return GetDetailedPermissions(content, allUserGroups); - } - - private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) - { - //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. - //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. - - var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); - - var defaultPermissionsAsDictionary = defaultPermissionsByGroup - .ToDictionary(x => Convert.ToInt32(x.Id), x => x); - - //get the actual assigned permissions - var assignedPermissionsByGroup = Services.ContentService.GetPermissions(content).ToArray(); - - //iterate over assigned and update the defaults with the real values - foreach (var assignedGroupPermission in assignedPermissionsByGroup) - { - var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; - - //clone the default permissions model to the assigned ones - defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); - - //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions - //and we'll re-check it if it's one of the explicitly assigned ones - foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) - { - permission.Checked = false; - permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); - } - - } - - return defaultPermissionsByGroup; - } - - /// - /// Returns an item to be used to display the recycle bin for content - /// - /// - public ContentItemDisplay GetRecycleBin() - { - var display = new ContentItemDisplay - { - Id = Constants.System.RecycleBinContent, - Alias = "recycleBin", - ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), - ContentTypeAlias = "recycleBin", - CreateDate = DateTime.Now, - IsContainer = true, - Path = "-1," + Constants.System.RecycleBinContent - }; - - TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); - - return display; - } - - //fixme what about cultures? - public ContentItemDisplay GetBlueprintById(int id) - { - var foundContent = Services.ContentService.GetBlueprintById(id); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = MapToDisplay(foundContent); - - SetupBlueprint(content, foundContent); - - return content; - } - - private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) - { - content.AllowPreview = false; - - //set a custom path since the tree that renders this has the content type id as the parent - content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); - - content.AllowedActions = new[] { "A" }; - content.IsBlueprint = true; - - var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; - var propsTab = content.Tabs.Last(); - propsTab.Properties = propsTab.Properties - .Where(p => excludeProps.Contains(p.Alias) == false); - } - - /// - /// Gets the content json for the content id - /// - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(int id, string culture = null) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - return null;//irrelevant since the above throws - } - var content = MapToDisplay(foundContent, culture); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Guid id, string culture = null) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - return null;//irrelevant since the above throws - } - - var content = MapToDisplay(foundContent, culture); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Udi id, string culture = null) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetById(guidUdi.Guid, culture); - } - - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - /// - /// Gets an empty content item for the - /// - /// - /// - /// - /// If this is a container type, we'll remove the umbContainerView tab for a new item since - /// it cannot actually list children if it doesn't exist yet. - /// - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) - { - var contentType = Services.ContentTypeService.Get(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); - var mapped = MapToDisplay(emptyContent); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - - if (contentType.VariesByCulture()) - { - //Remove all variants except for the default since currently the default must be saved before other variants can be edited - //TODO: Allow for editing all variants at once ... this will be a future task - mapped.Variants = new[] { mapped.Variants.FirstOrDefault(x => x.IsCurrent) }; - } - - return mapped; - } - - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(int blueprintId, int parentId) - { - var blueprint = Services.ContentService.GetBlueprintById(blueprintId); - if (blueprint == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - blueprint.Id = 0; - blueprint.Name = string.Empty; - blueprint.ParentId = parentId; - - var mapped = Mapper.Map(blueprint); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(int id) - { - var url = Umbraco.Url(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Guid id) - { - var url = Umbraco.UrlProvider.GetUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Udi id) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetNiceUrl(guidUdi.Guid); - } - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - string includeProperties, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - long totalChildren; - IContent[] children; - if (pageNumber > 0 && pageSize > 0) - { - IQuery queryFilter = null; - if (filter.IsNullOrWhiteSpace() == false) - { - //add the default text filter - queryFilter = SqlContext.Query() - .Where(x => x.Name.Contains(filter)); - } - - children = Services.ContentService - .GetPagedChildren( - id, (pageNumber - 1), pageSize, - out totalChildren, - orderBy, orderDirection, orderBySystemField, - queryFilter).ToArray(); - } - else - { - children = Services.ContentService.GetChildren(id).ToArray(); - totalChildren = children.Length; - } - - if (totalChildren == 0) - { - return new PagedResult>(0, 0, 0); - } - - var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); - pagedResult.Items = children.Select(content => - Mapper.Map>(content, - opts => - { - // if there's a list of property aliases to map - we will make sure to store this in the mapping context. - if (String.IsNullOrWhiteSpace(includeProperties) == false) - { - opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); - } - })); - - return pagedResult; - } - - /// - /// Returns permissions for all nodes passed in for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - [HttpPost] - public Dictionary GetPermissions(int[] nodeIds) - { - var permissions = Services.UserService - .GetPermissions(Security.CurrentUser, nodeIds); - - var permissionsDictionary = new Dictionary(); - foreach (var nodeId in nodeIds) - { - var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); - permissionsDictionary.Add(nodeId, aggregatePerms); - } - - return permissionsDictionary; - } - - /// - /// Checks a nodes permission for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - /// - [HttpGet] - public bool HasPermission(string permissionToCheck, int nodeId) - { - var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); - if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) - { - return true; - } - - return false; - } - - /// - /// Creates a blueprint from a content item - /// - /// The content id to copy - /// The name of the blueprint - /// - [HttpPost] - public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); - - var content = Services.ContentService.GetById(contentId); - if (content == null) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - EnsureUniqueName(name, content, "name"); - - var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.GetUserId().ResultOr(0)); - - Services.ContentService.SaveBlueprint(blueprint, Security.GetUserId().ResultOr(0)); - - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddSuccessNotification( - Services.TextService.Localize("blueprints/createdBlueprintHeading"), - Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) - ); - - return notificationModel; - } - - private void EnsureUniqueName(string name, IContent content, string modelName) - { - var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); - if (existing.Any(x => x.Name == name && x.Id != content.Id)) - { - ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); - } - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - public ContentItemDisplay PostSaveBlueprint([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) - { - var contentItemDisplay = PostSaveInternal(contentItem, - content => - { - EnsureUniqueName(content.Name, content, "Name"); - - Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); - //we need to reuse the underlying logic so return the result that it wants - return OperationResult.Succeed(new EventMessages()); - }); - SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); - - return contentItemDisplay; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) - { - var contentItemDisplay = PostSaveInternal(contentItem, content => Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id)); - //ensure the active culture is still selected - if (!contentItem.Culture.IsNullOrWhiteSpace()) - { - foreach (var contentVariation in contentItemDisplay.Variants) - { - contentVariation.IsCurrent = contentVariation.Language.IsoCode.InvariantEquals(contentItem.Culture); - } - } - return contentItemDisplay; - } - - private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func saveMethod) - { - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - MapPropertyValues(contentItem); - - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this - if (ModelState.IsValid == false) - { - if (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(contentItem) && IsCreatingAction(contentItem.Action)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw a validation message - var forDisplay = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - - } - - //if the model state is not valid we cannot publish so change it to save - switch (contentItem.Action) - { - case ContentSaveAction.Publish: - contentItem.Action = ContentSaveAction.Save; - break; - case ContentSaveAction.PublishNew: - contentItem.Action = ContentSaveAction.SaveNew; - break; - } - } - - //initialize this to successful - var publishStatus = new PublishResult(null, contentItem.PersistedContent); - var wasCancelled = false; - - if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) - { - //save the item - var saveResult = saveMethod(contentItem.PersistedContent); - - wasCancelled = saveResult.Success == false && saveResult.Result == OperationResultType.FailedCancelledByEvent; - } - else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) - { - var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = sendResult == false; - } - else - { - PublishInternal(contentItem, ref publishStatus, ref wasCancelled); - } - - //get the updated model - var display = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); - - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize("speechBubbles/editContentSavedText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.SendPublish: - case ContentSaveAction.SendPublishNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.Publish: - case ContentSaveAction.PublishNew: - ShowMessageForPublishStatus(publishStatus, display); - break; - } - - //If the item is new and the operation was cancelled, we need to return a different - // status code so the UI can handle it since it won't be able to redirect since there - // is no Id to redirect to! - if (wasCancelled && IsCreatingAction(contentItem.Action)) - { - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - - display.PersistedContent = contentItem.PersistedContent; - - return display; - } - - /// - /// Performs the publishing operation for a content item - /// - /// - /// - /// - /// - /// If this is a culture variant than we need to do some validation, if it's not we'll publish as normal - /// - private void PublishInternal(ContentItemSave contentItem, ref PublishResult publishStatus, ref bool wasCancelled) - { - if (!contentItem.PersistedContent.ContentType.VariesByCulture()) - { - //its invariant, proceed normally - publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, userId: Security.CurrentUser.Id); - wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; - } - else - { - var canPublish = true; - - //check if we are publishing other variants and validate them - var allLangs = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase); - var otherVariantsToValidate = contentItem.PublishVariations.Where(x => !x.Culture.InvariantEquals(contentItem.Culture)).ToList(); - - //validate any mandatory variants that are not in the list - var mandatoryLangs = Mapper.Map, IEnumerable>(allLangs.Values) - .Where(x => otherVariantsToValidate.All(v => !v.Culture.InvariantEquals(x.IsoCode))) //don't include variants above - .Where(x => !x.IsoCode.InvariantEquals(contentItem.Culture)) //don't include the current variant - .Where(x => x.Mandatory); - foreach (var lang in mandatoryLangs) - { - //cannot continue publishing since a required language that is not currently being published isn't published - if (!contentItem.PersistedContent.IsCulturePublished(lang.IsoCode)) - { - var errMsg = Services.TextService.Localize("speechBubbles/contentReqCulturePublishError", new[] { allLangs[lang.IsoCode].CultureName }); - ModelState.AddModelError("publish_variant_" + lang.IsoCode + "_", errMsg); - canPublish = false; - } - } - - if (canPublish) - { - //validate all other variants to be published - foreach (var publishVariation in otherVariantsToValidate) - { - //validate the content item and the culture property values, we don't need to validate any invariant property values here because they will have - //been validated in the post. - var valid = contentItem.PersistedContent.IsValid(publishVariation.Culture); - if (!valid) - { - var errMsg = Services.TextService.Localize("speechBubbles/contentCultureValidationError", new[] { allLangs[publishVariation.Culture].CultureName }); - ModelState.AddModelError("publish_variant_" + publishVariation.Culture + "_", errMsg); - canPublish = false; - } - } - } - - if (canPublish) - { - //try to publish all the values on the model - canPublish = PublishCulture(contentItem, otherVariantsToValidate, allLangs); - } - - if (canPublish) - { - //proceed to publish if all validation still succeeds - publishStatus = Services.ContentService.SavePublishing(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; - } - else - { - //can only save - var saveResult = Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id); - publishStatus = new PublishResult(PublishResultType.FailedCannotPublish, null, contentItem.PersistedContent); - wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; - } - } - } - - /// - /// This will call TryPublishValues on the content item for each culture that needs to be published including the invariant culture - /// - /// - /// - /// - /// - private bool PublishCulture(ContentItemSave contentItem, IEnumerable otherVariantsToValidate, IDictionary allLangs) - { - var culturesToPublish = new List { contentItem.Culture }; - culturesToPublish.AddRange(otherVariantsToValidate.Select(x => x.Culture)); - - foreach(var culture in culturesToPublish) - { - // publishing any culture, implies the invariant culture - var valid = contentItem.PersistedContent.PublishCulture(culture); - if (!valid) - { - var errMsg = Services.TextService.Localize("speechBubbles/contentCultureUnexpectedValidationError", new[] { allLangs[culture].CultureName }); - ModelState.AddModelError("publish_variant_" + culture + "_", errMsg); - return false; - } - } - - return true; - } - - /// - /// Publishes a document with a given ID - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Publish access to this node. - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - public HttpResponseMessage PostPublishById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - var publishResult = Services.ContentService.SavePublishing(foundContent, Security.GetUserId().ResultOr(0)); - if (publishResult.Success == false) - { - var notificationModel = new SimpleNotificationModel(); - ShowMessageForPublishStatus(publishResult, notificationModel); - return Request.CreateValidationErrorResponse(notificationModel); - } - - //return ok - return Request.CreateResponse(HttpStatusCode.OK); - - } - - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteBlueprint(int id) - { - var found = Services.ContentService.GetBlueprintById(id); - - if (found == null) - { - return HandleContentNotFound(id, false); - } - - Services.ContentService.DeleteBlueprint(found); - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Moves an item to the recycle bin, if it is already there then it will permanently delete it - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Delete access to this node. - /// - [EnsureUserPermissionForContent("id", 'D')] - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - //if the current item is in the recycle bin - if (foundContent.Trashed == false) - { - var moveResult = Services.ContentService.MoveToRecycleBin(foundContent, Security.GetUserId().ResultOr(0)); - if (moveResult.Success == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - else - { - var deleteResult = Services.ContentService.Delete(foundContent, Security.GetUserId().ResultOr(0)); - if (deleteResult.Success == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Empties the recycle bin - /// - /// - /// - /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin - /// - [HttpDelete] - [HttpPost] - [EnsureUserPermissionForContent(Constants.System.RecycleBinContent)] - public HttpResponseMessage EmptyRecycleBin() - { - Services.ContentService.EmptyRecycleBin(); - - return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); - } - - /// - /// Change the sort order for content - /// - /// - /// - [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public HttpResponseMessage PostSort(ContentSortOrder sorted) - { - if (sorted == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - - try - { - var contentService = Services.ContentService; - - // Save content with new sort order and update content xml in db accordingly - if (contentService.Sort(sorted.IdSortOrder) == false) - { - Logger.Warn("Content sorting failed, this was probably caused by an event being cancelled"); - return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); - } - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception ex) - { - Logger.Error("Could not update content sort order", ex); - throw; - } - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForContent("move.ParentId", 'M')] - public HttpResponseMessage PostMove(MoveOrCopy move) - { - var toMove = ValidateMoveOrCopy(move); - - Services.ContentService.Move(toMove, move.ParentId, Security.GetUserId().ResultOr(0)); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Copies a content item and places the copy as a child of a given parent Id - /// - /// - /// - [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public HttpResponseMessage PostCopy(MoveOrCopy copy) - { - var toCopy = ValidateMoveOrCopy(copy); - - var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.GetUserId().ResultOr(0)); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Unpublishes a node with a given Id and returns the unpublished entity - /// - /// The content id to unpublish - /// The culture variant for the content id to unpublish, if none specified will unpublish all variants of the content - /// - [EnsureUserPermissionForContent("id", 'U')] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostUnPublish(int id, string culture = null) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - HandleContentNotFound(id); - - var unpublishResult = Services.ContentService.Unpublish(foundContent, culture: culture, userId: Security.GetUserId().ResultOr(0)); - - var content = MapToDisplay(foundContent, culture); - - if (!unpublishResult.Success) - { - AddCancelMessage(content); - throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); - } - else - { - //fixme should have a better localized method for when we have the UnpublishResultType.SuccessMandatoryCulture status - - content.AddSuccessNotification( - Services.TextService.Localize("content/unPublish"), - unpublishResult.Result == UnpublishResultType.SuccessCulture - ? Services.TextService.Localize("speechBubbles/contentVariationUnpublished", new[] { culture }) - : Services.TextService.Localize("speechBubbles/contentUnpublished")); - - return content; - } - } - - [HttpPost] - public DomainSave PostSaveLanguageAndDomains(DomainSave model) - { - var node = Services.ContentService.GetById(model.NodeId); - - if (node == null) - { - var response = Request.CreateResponse(HttpStatusCode.BadRequest); - response.Content = new StringContent($"There is no content node with id {model.NodeId}."); - response.ReasonPhrase = "Node Not Found."; - throw new HttpResponseException(response); - } - - var permission = Services.UserService.GetPermissions(Security.CurrentUser, node.Path); - - if (permission.AssignedPermissions.Contains(ActionAssignDomain.Instance.Letter.ToString(), StringComparer.Ordinal) == false) - { - var response = Request.CreateResponse(HttpStatusCode.BadRequest); - response.Content = new StringContent("You do not have permission to assign domains on that node."); - response.ReasonPhrase = "Permission Denied."; - throw new HttpResponseException(response); - } - - model.Valid = true; - var domains = Services.DomainService.GetAssignedDomains(model.NodeId, true).ToArray(); - var languages = Services.LocalizationService.GetAllLanguages().ToArray(); - var language = model.Language > 0 ? languages.FirstOrDefault(l => l.Id == model.Language) : null; - - // process wildcard - if (language != null) - { - // yet there is a race condition here... - var wildcard = domains.FirstOrDefault(d => d.IsWildcard); - if (wildcard != null) - { - wildcard.LanguageId = language.Id; - } - else - { - wildcard = new UmbracoDomain("*" + model.NodeId) - { - LanguageId = model.Language, - RootContentId = model.NodeId - }; - } - - var saveAttempt = Services.DomainService.Save(wildcard); - if (saveAttempt == false) - { - var response = Request.CreateResponse(HttpStatusCode.BadRequest); - response.Content = new StringContent("Saving domain failed"); - response.ReasonPhrase = saveAttempt.Result.Result.ToString(); - throw new HttpResponseException(response); - } - } - else - { - var wildcard = domains.FirstOrDefault(d => d.IsWildcard); - if (wildcard != null) - { - Services.DomainService.Delete(wildcard); - } - } - - // process domains - // delete every (non-wildcard) domain, that exists in the DB yet is not in the model - foreach (var domain in domains.Where(d => d.IsWildcard == false && model.Domains.All(m => m.Name.InvariantEquals(d.DomainName) == false))) - { - Services.DomainService.Delete(domain); - } - - var names = new List(); - - // create or update domains in the model - foreach (var domainModel in model.Domains.Where(m => string.IsNullOrWhiteSpace(m.Name) == false)) - { - language = languages.FirstOrDefault(l => l.Id == domainModel.Lang); - if (language == null) - { - continue; - } - - var name = domainModel.Name.ToLowerInvariant(); - if (names.Contains(name)) - { - domainModel.Duplicate = true; - continue; - } - names.Add(name); - var domain = domains.FirstOrDefault(d => d.DomainName.InvariantEquals(domainModel.Name)); - if (domain != null) - { - domain.LanguageId = language.Id; - Services.DomainService.Save(domain); - } - else if (Services.DomainService.Exists(domainModel.Name)) - { - domainModel.Duplicate = true; - var xdomain = Services.DomainService.GetByName(domainModel.Name); - var xrcid = xdomain.RootContentId; - if (xrcid.HasValue) - { - var xcontent = Services.ContentService.GetById(xrcid.Value); - var xnames = new List(); - while (xcontent != null) - { - xnames.Add(xcontent.Name); - if (xcontent.ParentId < -1) - xnames.Add("Recycle Bin"); - xcontent = xcontent.Parent(Services.ContentService); - } - xnames.Reverse(); - domainModel.Other = "/" + string.Join("/", xnames); - } - } - else - { - // yet there is a race condition here... - var newDomain = new UmbracoDomain(name) - { - LanguageId = domainModel.Lang, - RootContentId = model.NodeId - }; - var saveAttempt = Services.DomainService.Save(newDomain); - if (saveAttempt == false) - { - var response = Request.CreateResponse(HttpStatusCode.BadRequest); - response.Content = new StringContent("Saving new domain failed"); - response.ReasonPhrase = saveAttempt.Result.Result.ToString(); - throw new HttpResponseException(response); - } - } - } - - model.Valid = model.Domains.All(m => m.Duplicate == false); - - return model; - } - - /// - /// Maps the dto property values to the persisted model - /// - /// - private void MapPropertyValues(ContentItemSave contentItem) - { - //Don't update the name if it is empty - if (!contentItem.Name.IsNullOrWhiteSpace()) - { - if (contentItem.PersistedContent.ContentType.VariesByCulture()) - { - if (contentItem.Culture.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"Cannot set culture name without a culture."); - contentItem.PersistedContent.SetCultureName(contentItem.Name, contentItem.Culture); - } - else - { - contentItem.PersistedContent.Name = contentItem.Name; - } - } - - //TODO: We need to support 'send to publish' - - contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; - contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; - //only set the template if it didn't change - var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); - if (templateChanged) - { - var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); - if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - { - //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); - Logger.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); - } - else - { - //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template - contentItem.PersistedContent.Template = template; - } - } - - bool Varies(Property property) => property.PropertyType.VariesByCulture(); - - MapPropertyValues( - contentItem, - (save, property) => Varies(property) ? property.GetValue(save.Culture) : property.GetValue(), //get prop val - (save, property, v) => { if (Varies(property)) property.SetValue(v, save.Culture); else property.SetValue(v); }); //set prop val - } - - /// - /// Ensures the item can be moved/copied to the new location - /// - /// - /// - private IContent ValidateMoveOrCopy(MoveOrCopy model) - { - if (model == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var contentService = Services.ContentService; - var toMove = contentService.GetById(model.Id); - if (toMove == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - if (model.ParentId < 0) - { - //cannot move if the content item is not allowed at the root - if (toMove.ContentType.AllowedAsRoot == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); - } - } - else - { - var parent = contentService.GetById(model.ParentId); - if (parent == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //check if the item is allowed under this one - if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); - } - - // Check on paths - if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); - } - } - - return toMove; - } - - private void ShowMessageForPublishStatus(PublishResult status, INotificationModel display) - { - switch (status.Result) - { - case PublishResultType.Success: - case PublishResultType.SuccessAlready: - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editContentPublishedText")); - break; - case PublishResultType.FailedPathNotPublished: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); - break; - case PublishResultType.FailedCancelledByEvent: - AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; - case PublishResultType.FailedAwaitingRelease: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); - break; - case PublishResultType.FailedHasExpired: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] { $"{status.Content.Name} ({status.Content.Id})", }).Trim()); - break; - case PublishResultType.FailedIsTrashed: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - "publish/contentPublishedFailedIsTrashed"); // fixme properly localize! - break; - case PublishResultType.FailedContentInvalid: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - $"{status.Content.Name} ({status.Content.Id})", - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); - break; - case PublishResultType.FailedByCulture: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - "publish/contentPublishedFailedByCulture"); // fixme properly localize! - break; - default: - throw new IndexOutOfRangeException($"PublishedResultType \"{status.Result}\" was not expected."); - } - } - - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// - /// Specifies the already resolved content item to check against - /// - internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - IEntityService entityService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null) - { - if (storage == null) throw new ArgumentNullException("storage"); - if (user == null) throw new ArgumentNullException("user"); - if (userService == null) throw new ArgumentNullException("userService"); - if (contentService == null) throw new ArgumentNullException("contentService"); - if (entityService == null) throw new ArgumentNullException("entityService"); - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - contentItem = contentService.GetById(nodeId); - //put the content item into storage so it can be retreived - // in the controller (saves a lookup) - storage[typeof(IContent).ToString()] = contentItem; - } - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasContentRootAccess(entityService) - : (nodeId == Constants.System.RecycleBinContent) - ? user.HasContentBinAccess(entityService) - : user.HasPathAccess(contentItem, entityService); - - if (hasPathAccess == false) - { - return false; - } - - if (permissionsToCheck == null || permissionsToCheck.Length == 0) - { - return true; - } - - //get the implicit/inherited permissions for the user for this path, - //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) - var path = contentItem != null ? contentItem.Path : nodeId.ToString(); - var permission = userService.GetPermissionsForPath(user, path); - - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null - || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; - } - - /// - /// Used to map an instance to a and ensuring a language is present if required - /// - /// - /// - /// - private ContentItemDisplay MapToDisplay(IContent content, string culture = null) - { - //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, - // the Cuture property of ContentItemDisplay must exist (at least currently). - if (culture == null && content.ContentType.VariesByCulture()) - { - //If a culture is not explicitly sent up, then it means that the user is editing the default variant language. - culture = Services.LocalizationService.GetDefaultLanguageIsoCode(); - } - - var display = ContextMapper.Map(content, - new Dictionary { { ContextMapper.CultureKey, culture } }); - - return display; - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Binders; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Web.PublishedCache; +using Umbraco.Core.Events; +using Umbraco.Core.Models.Validation; +using Umbraco.Web.Models; +using Umbraco.Web.WebServices; +using Umbraco.Web._Legacy.Actions; +using Constants = Umbraco.Core.Constants; +using ContentVariation = Umbraco.Core.Models.ContentVariation; +using Language = Umbraco.Web.Models.ContentEditing.Language; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for editing content + /// + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the content application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [ContentControllerConfiguration] + public class ContentController : ContentControllerBase + { + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public ContentController(IPublishedSnapshotService publishedSnapshotService) + { + if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService)); + _publishedSnapshotService = publishedSnapshotService; + } + + /// + /// Configures this controller with a custom action selector + /// + private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); + } + } + + /// + /// Returns true if any content types have culture variation enabled + /// + /// + [HttpGet] + [WebApi.UmbracoAuthorize, OverrideAuthorization] + public bool AllowsCultureVariation() + { + var contentTypes = Services.ContentTypeService.GetAll(); + return contentTypes.Any(contentType => contentType.VariesByCulture()); + } + + /// + /// Return content for the specified ids + /// + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + //fixme what about cultures? + + var foundContent = Services.ContentService.GetByIds(ids); + return foundContent.Select(x => MapToDisplay(x)); + } + + /// + /// Updates the permissions for a content item for a particular user group + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to update + /// + [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] + public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) + { + if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be alowed to set granular permissions? + + var content = Services.ContentService.GetById(saveModel.ContentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //current permissions explicitly assigned to this content item + var contentPermissions = Services.ContentService.GetPermissions(content) + .ToDictionary(x => x.UserGroupId, x => x); + + var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); + + //loop through each user group + foreach (var userGroup in allUserGroups) + { + //check if there's a permission set posted up for this user group + IEnumerable groupPermissions; + if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) + { + //create a string collection of the assigned letters + var groupPermissionCodes = groupPermissions.ToArray(); + + //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions + //for this group/node which will go back to the defaults + if (groupPermissionCodes.Length == 0) + { + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored + else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) + { + //only remove them if they are actually currently assigned + if (contentPermissions.ContainsKey(userGroup.Id)) + { + //remove these permissions from this node for this group since the ones being assigned are the same as the defaults + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + } + //if they are different we need to update, otherwise there's nothing to update + else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) + { + + Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); + } + } + } + + return GetDetailedPermissions(content, allUserGroups); + } + + /// + /// Returns the user group permissions for user groups assigned to this node + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to view + /// + [EnsureUserPermissionForContent("contentId", 'R')] + public IEnumerable GetDetailedPermissions(int contentId) + { + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = Services.ContentService.GetById(contentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be able to see detailed permissions? + + var allUserGroups = Services.UserService.GetAllUserGroups(); + + return GetDetailedPermissions(content, allUserGroups); + } + + private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) + { + //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. + //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. + + var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); + + var defaultPermissionsAsDictionary = defaultPermissionsByGroup + .ToDictionary(x => Convert.ToInt32(x.Id), x => x); + + //get the actual assigned permissions + var assignedPermissionsByGroup = Services.ContentService.GetPermissions(content).ToArray(); + + //iterate over assigned and update the defaults with the real values + foreach (var assignedGroupPermission in assignedPermissionsByGroup) + { + var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; + + //clone the default permissions model to the assigned ones + defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); + + //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions + //and we'll re-check it if it's one of the explicitly assigned ones + foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) + { + permission.Checked = false; + permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); + } + + } + + return defaultPermissionsByGroup; + } + + /// + /// Returns an item to be used to display the recycle bin for content + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinContent, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinContent + }; + + TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); + + return display; + } + + //fixme what about cultures? + public ContentItemDisplay GetBlueprintById(int id) + { + var foundContent = Services.ContentService.GetBlueprintById(id); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = MapToDisplay(foundContent); + + SetupBlueprint(content, foundContent); + + return content; + } + + private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) + { + content.AllowPreview = false; + + //set a custom path since the tree that renders this has the content type id as the parent + content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); + + content.AllowedActions = new[] { "A" }; + content.IsBlueprint = true; + + var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; + var propsTab = content.Tabs.Last(); + propsTab.Properties = propsTab.Properties + .Where(p => excludeProps.Contains(p.Alias) == false); + } + + /// + /// Gets the content json for the content id + /// + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(int id, string culture = null) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + return null;//irrelevant since the above throws + } + var content = MapToDisplay(foundContent, culture); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Guid id, string culture = null) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + return null;//irrelevant since the above throws + } + + var content = MapToDisplay(foundContent, culture); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Udi id, string culture = null) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetById(guidUdi.Guid, culture); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// Gets an empty content item for the + /// + /// + /// + /// + /// If this is a container type, we'll remove the umbContainerView tab for a new item since + /// it cannot actually list children if it doesn't exist yet. + /// + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) + { + var contentType = Services.ContentTypeService.Get(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); + var mapped = MapToDisplay(emptyContent); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + + if (contentType.VariesByCulture()) + { + //Remove all variants except for the default since currently the default must be saved before other variants can be edited + //TODO: Allow for editing all variants at once ... this will be a future task + mapped.Variants = new[] { mapped.Variants.FirstOrDefault(x => x.IsCurrent) }; + } + + return mapped; + } + + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(int blueprintId, int parentId) + { + var blueprint = Services.ContentService.GetBlueprintById(blueprintId); + if (blueprint == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + blueprint.Id = 0; + blueprint.Name = string.Empty; + blueprint.ParentId = parentId; + + var mapped = Mapper.Map(blueprint); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(int id) + { + var url = Umbraco.Url(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Guid id) + { + var url = Umbraco.UrlProvider.GetUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetNiceUrl(guidUdi.Guid); + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + string includeProperties, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + long totalChildren; + IContent[] children; + if (pageNumber > 0 && pageSize > 0) + { + IQuery queryFilter = null; + if (filter.IsNullOrWhiteSpace() == false) + { + //add the default text filter + queryFilter = SqlContext.Query() + .Where(x => x.Name.Contains(filter)); + } + + children = Services.ContentService + .GetPagedChildren( + id, (pageNumber - 1), pageSize, + out totalChildren, + orderBy, orderDirection, orderBySystemField, + queryFilter).ToArray(); + } + else + { + children = Services.ContentService.GetChildren(id).ToArray(); + totalChildren = children.Length; + } + + if (totalChildren == 0) + { + return new PagedResult>(0, 0, 0); + } + + var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); + pagedResult.Items = children.Select(content => + Mapper.Map>(content, + opts => + { + // if there's a list of property aliases to map - we will make sure to store this in the mapping context. + if (String.IsNullOrWhiteSpace(includeProperties) == false) + { + opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); + } + })); + + return pagedResult; + } + + /// + /// Returns permissions for all nodes passed in for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + [HttpPost] + public Dictionary GetPermissions(int[] nodeIds) + { + var permissions = Services.UserService + .GetPermissions(Security.CurrentUser, nodeIds); + + var permissionsDictionary = new Dictionary(); + foreach (var nodeId in nodeIds) + { + var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); + permissionsDictionary.Add(nodeId, aggregatePerms); + } + + return permissionsDictionary; + } + + /// + /// Checks a nodes permission for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + /// + [HttpGet] + public bool HasPermission(string permissionToCheck, int nodeId) + { + var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); + if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) + { + return true; + } + + return false; + } + + /// + /// Creates a blueprint from a content item + /// + /// The content id to copy + /// The name of the blueprint + /// + [HttpPost] + public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); + + var content = Services.ContentService.GetById(contentId); + if (content == null) + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + EnsureUniqueName(name, content, "name"); + + var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.GetUserId().ResultOr(0)); + + Services.ContentService.SaveBlueprint(blueprint, Security.GetUserId().ResultOr(0)); + + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddSuccessNotification( + Services.TextService.Localize("blueprints/createdBlueprintHeading"), + Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) + ); + + return notificationModel; + } + + private void EnsureUniqueName(string name, IContent content, string modelName) + { + var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); + if (existing.Any(x => x.Name == name && x.Id != content.Id)) + { + ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + public ContentItemDisplay PostSaveBlueprint([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) + { + var contentItemDisplay = PostSaveInternal(contentItem, + content => + { + EnsureUniqueName(content.Name, content, "Name"); + + Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); + //we need to reuse the underlying logic so return the result that it wants + return OperationResult.Succeed(new EventMessages()); + }); + SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); + + return contentItemDisplay; + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) + { + var contentItemDisplay = PostSaveInternal(contentItem, content => Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id)); + //ensure the active culture is still selected + if (!contentItem.Culture.IsNullOrWhiteSpace()) + { + foreach (var contentVariation in contentItemDisplay.Variants) + { + contentVariation.IsCurrent = contentVariation.Language.IsoCode.InvariantEquals(contentItem.Culture); + } + } + return contentItemDisplay; + } + + private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func saveMethod) + { + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid + MapPropertyValues(contentItem); + + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (ModelState.IsValid == false) + { + if (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(contentItem) && IsCreatingAction(contentItem.Action)) + { + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw a validation message + var forDisplay = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + + } + + //if the model state is not valid we cannot publish so change it to save + switch (contentItem.Action) + { + case ContentSaveAction.Publish: + contentItem.Action = ContentSaveAction.Save; + break; + case ContentSaveAction.PublishNew: + contentItem.Action = ContentSaveAction.SaveNew; + break; + } + } + + //initialize this to successful + var publishStatus = new PublishResult(null, contentItem.PersistedContent); + var wasCancelled = false; + + if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) + { + //save the item + var saveResult = saveMethod(contentItem.PersistedContent); + + wasCancelled = saveResult.Success == false && saveResult.Result == OperationResultType.FailedCancelledByEvent; + } + else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) + { + var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = sendResult == false; + } + else + { + PublishInternal(contentItem, ref publishStatus, ref wasCancelled); + } + + //get the updated model + var display = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); + + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + Services.TextService.Localize("speechBubbles/editContentSavedText")); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.SendPublish: + case ContentSaveAction.SendPublishNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + ShowMessageForPublishStatus(publishStatus, display); + break; + } + + //If the item is new and the operation was cancelled, we need to return a different + // status code so the UI can handle it since it won't be able to redirect since there + // is no Id to redirect to! + if (wasCancelled && IsCreatingAction(contentItem.Action)) + { + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + + display.PersistedContent = contentItem.PersistedContent; + + return display; + } + + /// + /// Performs the publishing operation for a content item + /// + /// + /// + /// + /// + /// If this is a culture variant than we need to do some validation, if it's not we'll publish as normal + /// + private void PublishInternal(ContentItemSave contentItem, ref PublishResult publishStatus, ref bool wasCancelled) + { + if (!contentItem.PersistedContent.ContentType.VariesByCulture()) + { + //its invariant, proceed normally + publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, userId: Security.CurrentUser.Id); + wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; + } + else + { + var canPublish = true; + + //check if we are publishing other variants and validate them + var allLangs = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase); + var otherVariantsToValidate = contentItem.PublishVariations.Where(x => !x.Culture.InvariantEquals(contentItem.Culture)).ToList(); + + //validate any mandatory variants that are not in the list + var mandatoryLangs = Mapper.Map, IEnumerable>(allLangs.Values) + .Where(x => otherVariantsToValidate.All(v => !v.Culture.InvariantEquals(x.IsoCode))) //don't include variants above + .Where(x => !x.IsoCode.InvariantEquals(contentItem.Culture)) //don't include the current variant + .Where(x => x.Mandatory); + foreach (var lang in mandatoryLangs) + { + //cannot continue publishing since a required language that is not currently being published isn't published + if (!contentItem.PersistedContent.IsCulturePublished(lang.IsoCode)) + { + var errMsg = Services.TextService.Localize("speechBubbles/contentReqCulturePublishError", new[] { allLangs[lang.IsoCode].CultureName }); + ModelState.AddModelError("publish_variant_" + lang.IsoCode + "_", errMsg); + canPublish = false; + } + } + + if (canPublish) + { + //validate all other variants to be published + foreach (var publishVariation in otherVariantsToValidate) + { + //validate the content item and the culture property values, we don't need to validate any invariant property values here because they will have + //been validated in the post. + var valid = contentItem.PersistedContent.IsValid(publishVariation.Culture); + if (!valid) + { + var errMsg = Services.TextService.Localize("speechBubbles/contentCultureValidationError", new[] { allLangs[publishVariation.Culture].CultureName }); + ModelState.AddModelError("publish_variant_" + publishVariation.Culture + "_", errMsg); + canPublish = false; + } + } + } + + if (canPublish) + { + //try to publish all the values on the model + canPublish = PublishCulture(contentItem, otherVariantsToValidate, allLangs); + } + + if (canPublish) + { + //proceed to publish if all validation still succeeds + publishStatus = Services.ContentService.SavePublishing(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; + } + else + { + //can only save + var saveResult = Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id); + publishStatus = new PublishResult(PublishResultType.FailedCannotPublish, null, contentItem.PersistedContent); + wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; + } + } + } + + /// + /// This will call TryPublishValues on the content item for each culture that needs to be published including the invariant culture + /// + /// + /// + /// + /// + private bool PublishCulture(ContentItemSave contentItem, IEnumerable otherVariantsToValidate, IDictionary allLangs) + { + var culturesToPublish = new List { contentItem.Culture }; + culturesToPublish.AddRange(otherVariantsToValidate.Select(x => x.Culture)); + + foreach(var culture in culturesToPublish) + { + // publishing any culture, implies the invariant culture + var valid = contentItem.PersistedContent.PublishCulture(culture); + if (!valid) + { + var errMsg = Services.TextService.Localize("speechBubbles/contentCultureUnexpectedValidationError", new[] { allLangs[culture].CultureName }); + ModelState.AddModelError("publish_variant_" + culture + "_", errMsg); + return false; + } + } + + return true; + } + + /// + /// Publishes a document with a given ID + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Publish access to this node. + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + public HttpResponseMessage PostPublishById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + var publishResult = Services.ContentService.SavePublishing(foundContent, Security.GetUserId().ResultOr(0)); + if (publishResult.Success == false) + { + var notificationModel = new SimpleNotificationModel(); + ShowMessageForPublishStatus(publishResult, notificationModel); + return Request.CreateValidationErrorResponse(notificationModel); + } + + //return ok + return Request.CreateResponse(HttpStatusCode.OK); + + } + + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteBlueprint(int id) + { + var found = Services.ContentService.GetBlueprintById(id); + + if (found == null) + { + return HandleContentNotFound(id, false); + } + + Services.ContentService.DeleteBlueprint(found); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Moves an item to the recycle bin, if it is already there then it will permanently delete it + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Delete access to this node. + /// + [EnsureUserPermissionForContent("id", 'D')] + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + //if the current item is in the recycle bin + if (foundContent.Trashed == false) + { + var moveResult = Services.ContentService.MoveToRecycleBin(foundContent, Security.GetUserId().ResultOr(0)); + if (moveResult.Success == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + else + { + var deleteResult = Services.ContentService.Delete(foundContent, Security.GetUserId().ResultOr(0)); + if (deleteResult.Success == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Empties the recycle bin + /// + /// + /// + /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin + /// + [HttpDelete] + [HttpPost] + [EnsureUserPermissionForContent(Constants.System.RecycleBinContent)] + public HttpResponseMessage EmptyRecycleBin() + { + Services.ContentService.EmptyRecycleBin(); + + return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); + } + + /// + /// Change the sort order for content + /// + /// + /// + [EnsureUserPermissionForContent("sorted.ParentId", 'S')] + public HttpResponseMessage PostSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + try + { + var contentService = Services.ContentService; + + // Save content with new sort order and update content xml in db accordingly + if (contentService.Sort(sorted.IdSortOrder) == false) + { + Logger.Warn("Content sorting failed, this was probably caused by an event being cancelled"); + return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); + } + return Request.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + Logger.Error("Could not update content sort order", ex); + throw; + } + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForContent("move.ParentId", 'M')] + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = ValidateMoveOrCopy(move); + + Services.ContentService.Move(toMove, move.ParentId, Security.GetUserId().ResultOr(0)); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Copies a content item and places the copy as a child of a given parent Id + /// + /// + /// + [EnsureUserPermissionForContent("copy.ParentId", 'C')] + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + var toCopy = ValidateMoveOrCopy(copy); + + var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.GetUserId().ResultOr(0)); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Unpublishes a node with a given Id and returns the unpublished entity + /// + /// The content id to unpublish + /// The culture variant for the content id to unpublish, if none specified will unpublish all variants of the content + /// + [EnsureUserPermissionForContent("id", 'U')] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostUnPublish(int id, string culture = null) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + HandleContentNotFound(id); + + var unpublishResult = Services.ContentService.Unpublish(foundContent, culture: culture, userId: Security.GetUserId().ResultOr(0)); + + var content = MapToDisplay(foundContent, culture); + + if (!unpublishResult.Success) + { + AddCancelMessage(content); + throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + } + else + { + //fixme should have a better localized method for when we have the UnpublishResultType.SuccessMandatoryCulture status + + content.AddSuccessNotification( + Services.TextService.Localize("content/unPublish"), + unpublishResult.Result == UnpublishResultType.SuccessCulture + ? Services.TextService.Localize("speechBubbles/contentVariationUnpublished", new[] { culture }) + : Services.TextService.Localize("speechBubbles/contentUnpublished")); + + return content; + } + } + + [HttpPost] + public DomainSave PostSaveLanguageAndDomains(DomainSave model) + { + var node = Services.ContentService.GetById(model.NodeId); + + if (node == null) + { + var response = Request.CreateResponse(HttpStatusCode.BadRequest); + response.Content = new StringContent($"There is no content node with id {model.NodeId}."); + response.ReasonPhrase = "Node Not Found."; + throw new HttpResponseException(response); + } + + var permission = Services.UserService.GetPermissions(Security.CurrentUser, node.Path); + + if (permission.AssignedPermissions.Contains(ActionAssignDomain.Instance.Letter.ToString(), StringComparer.Ordinal) == false) + { + var response = Request.CreateResponse(HttpStatusCode.BadRequest); + response.Content = new StringContent("You do not have permission to assign domains on that node."); + response.ReasonPhrase = "Permission Denied."; + throw new HttpResponseException(response); + } + + model.Valid = true; + var domains = Services.DomainService.GetAssignedDomains(model.NodeId, true).ToArray(); + var languages = Services.LocalizationService.GetAllLanguages().ToArray(); + var language = model.Language > 0 ? languages.FirstOrDefault(l => l.Id == model.Language) : null; + + // process wildcard + if (language != null) + { + // yet there is a race condition here... + var wildcard = domains.FirstOrDefault(d => d.IsWildcard); + if (wildcard != null) + { + wildcard.LanguageId = language.Id; + } + else + { + wildcard = new UmbracoDomain("*" + model.NodeId) + { + LanguageId = model.Language, + RootContentId = model.NodeId + }; + } + + var saveAttempt = Services.DomainService.Save(wildcard); + if (saveAttempt == false) + { + var response = Request.CreateResponse(HttpStatusCode.BadRequest); + response.Content = new StringContent("Saving domain failed"); + response.ReasonPhrase = saveAttempt.Result.Result.ToString(); + throw new HttpResponseException(response); + } + } + else + { + var wildcard = domains.FirstOrDefault(d => d.IsWildcard); + if (wildcard != null) + { + Services.DomainService.Delete(wildcard); + } + } + + // process domains + // delete every (non-wildcard) domain, that exists in the DB yet is not in the model + foreach (var domain in domains.Where(d => d.IsWildcard == false && model.Domains.All(m => m.Name.InvariantEquals(d.DomainName) == false))) + { + Services.DomainService.Delete(domain); + } + + var names = new List(); + + // create or update domains in the model + foreach (var domainModel in model.Domains.Where(m => string.IsNullOrWhiteSpace(m.Name) == false)) + { + language = languages.FirstOrDefault(l => l.Id == domainModel.Lang); + if (language == null) + { + continue; + } + + var name = domainModel.Name.ToLowerInvariant(); + if (names.Contains(name)) + { + domainModel.Duplicate = true; + continue; + } + names.Add(name); + var domain = domains.FirstOrDefault(d => d.DomainName.InvariantEquals(domainModel.Name)); + if (domain != null) + { + domain.LanguageId = language.Id; + Services.DomainService.Save(domain); + } + else if (Services.DomainService.Exists(domainModel.Name)) + { + domainModel.Duplicate = true; + var xdomain = Services.DomainService.GetByName(domainModel.Name); + var xrcid = xdomain.RootContentId; + if (xrcid.HasValue) + { + var xcontent = Services.ContentService.GetById(xrcid.Value); + var xnames = new List(); + while (xcontent != null) + { + xnames.Add(xcontent.Name); + if (xcontent.ParentId < -1) + xnames.Add("Recycle Bin"); + xcontent = xcontent.Parent(Services.ContentService); + } + xnames.Reverse(); + domainModel.Other = "/" + string.Join("/", xnames); + } + } + else + { + // yet there is a race condition here... + var newDomain = new UmbracoDomain(name) + { + LanguageId = domainModel.Lang, + RootContentId = model.NodeId + }; + var saveAttempt = Services.DomainService.Save(newDomain); + if (saveAttempt == false) + { + var response = Request.CreateResponse(HttpStatusCode.BadRequest); + response.Content = new StringContent("Saving new domain failed"); + response.ReasonPhrase = saveAttempt.Result.Result.ToString(); + throw new HttpResponseException(response); + } + } + } + + model.Valid = model.Domains.All(m => m.Duplicate == false); + + return model; + } + + /// + /// Maps the dto property values to the persisted model + /// + /// + private void MapPropertyValues(ContentItemSave contentItem) + { + //Don't update the name if it is empty + if (!contentItem.Name.IsNullOrWhiteSpace()) + { + if (contentItem.PersistedContent.ContentType.VariesByCulture()) + { + if (contentItem.Culture.IsNullOrWhiteSpace()) + throw new InvalidOperationException($"Cannot set culture name without a culture."); + contentItem.PersistedContent.SetCultureName(contentItem.Name, contentItem.Culture); + } + else + { + contentItem.PersistedContent.Name = contentItem.Name; + } + } + + //TODO: We need to support 'send to publish' + + contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; + contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; + //only set the template if it didn't change + var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + if (templateChanged) + { + var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); + if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + { + //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + Logger.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); + } + else + { + //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template + contentItem.PersistedContent.Template = template; + } + } + + bool Varies(Property property) => property.PropertyType.VariesByCulture(); + + MapPropertyValues( + contentItem, + (save, property) => Varies(property) ? property.GetValue(save.Culture) : property.GetValue(), //get prop val + (save, property, v) => { if (Varies(property)) property.SetValue(v, save.Culture); else property.SetValue(v); }); //set prop val + } + + /// + /// Ensures the item can be moved/copied to the new location + /// + /// + /// + private IContent ValidateMoveOrCopy(MoveOrCopy model) + { + if (model == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var contentService = Services.ContentService; + var toMove = contentService.GetById(model.Id); + if (toMove == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + if (model.ParentId < 0) + { + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + } + } + else + { + var parent = contentService.GetById(model.ParentId); + if (parent == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //check if the item is allowed under this one + if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + .Any(x => x.Value == toMove.ContentType.Id) == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + } + + // Check on paths + if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + } + } + + return toMove; + } + + private void ShowMessageForPublishStatus(PublishResult status, INotificationModel display) + { + switch (status.Result) + { + case PublishResultType.Success: + case PublishResultType.SuccessAlready: + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles/editContentPublishedText")); + break; + case PublishResultType.FailedPathNotPublished: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); + break; + case PublishResultType.FailedCancelledByEvent: + AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); + break; + case PublishResultType.FailedAwaitingRelease: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); + break; + case PublishResultType.FailedHasExpired: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] { $"{status.Content.Name} ({status.Content.Id})", }).Trim()); + break; + case PublishResultType.FailedIsTrashed: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + "publish/contentPublishedFailedIsTrashed"); // fixme properly localize! + break; + case PublishResultType.FailedContentInvalid: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + $"{status.Content.Name} ({status.Content.Id})", + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + case PublishResultType.FailedByCulture: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + "publish/contentPublishedFailedByCulture"); // fixme properly localize! + break; + default: + throw new IndexOutOfRangeException($"PublishedResultType \"{status.Result}\" was not expected."); + } + } + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// The storage to add the content item to so it can be reused + /// + /// + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + /// Specifies the already resolved content item to check against + /// + internal static bool CheckPermissions( + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + IEntityService entityService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null) + { + if (storage == null) throw new ArgumentNullException("storage"); + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (contentService == null) throw new ArgumentNullException("contentService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + contentItem = contentService.GetById(nodeId); + //put the content item into storage so it can be retreived + // in the controller (saves a lookup) + storage[typeof(IContent).ToString()] = contentItem; + } + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? user.HasContentRootAccess(entityService) + : (nodeId == Constants.System.RecycleBinContent) + ? user.HasContentBinAccess(entityService) + : user.HasPathAccess(contentItem, entityService); + + if (hasPathAccess == false) + { + return false; + } + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + { + return true; + } + + //get the implicit/inherited permissions for the user for this path, + //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + var path = contentItem != null ? contentItem.Path : nodeId.ToString(); + var permission = userService.GetPermissionsForPath(user, path); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + return allowed; + } + + /// + /// Used to map an instance to a and ensuring a language is present if required + /// + /// + /// + /// + private ContentItemDisplay MapToDisplay(IContent content, string culture = null) + { + //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, + // the Cuture property of ContentItemDisplay must exist (at least currently). + if (culture == null && content.ContentType.VariesByCulture()) + { + //If a culture is not explicitly sent up, then it means that the user is editing the default variant language. + culture = Services.LocalizationService.GetDefaultLanguageIsoCode(); + } + + var display = ContextMapper.Map(content, + new Dictionary { { ContextMapper.CultureKey, culture } }); + + return display; + } + } +} diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 4bd74bcbd3..c864ed8f16 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -275,6 +275,19 @@ namespace Umbraco.Web.Editors template = tryCreateTemplate.Result.Entity; } + // If the alias has been manually updated before the first save, + // make sure to also update the first allowed template, as the + // name will come back as a SafeAlias of the document type name, + // not as the actual document type alias. + // For more info: http://issues.umbraco.org/issue/U4-11059 + if (ctSave.DefaultTemplate != template.Alias) + { + var allowedTemplates = ctSave.AllowedTemplates.ToArray(); + if (allowedTemplates.Any()) + allowedTemplates[0] = template.Alias; + ctSave.AllowedTemplates = allowedTemplates; + } + //make sure the template alias is set on the default and allowed template so we can map it back ctSave.DefaultTemplate = template.Alias; diff --git a/src/Umbraco.Web/Editors/TemplateController.cs b/src/Umbraco.Web/Editors/TemplateController.cs index dae9cacb4c..74cb2f0167 100644 --- a/src/Umbraco.Web/Editors/TemplateController.cs +++ b/src/Umbraco.Web/Editors/TemplateController.cs @@ -184,7 +184,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var template = Services.FileService.CreateTemplateWithIdentity(display.Name, display.Content, master); + var template = Services.FileService.CreateTemplateWithIdentity(display.Alias, display.Content, master); Mapper.Map(template, display); } diff --git a/src/Umbraco.Web/Mvc/UrlHelperExtensions.cs b/src/Umbraco.Web/Mvc/UrlHelperExtensions.cs index 3e29d88d5f..6eb568c6af 100644 --- a/src/Umbraco.Web/Mvc/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/Mvc/UrlHelperExtensions.cs @@ -1,79 +1,79 @@ -using System; -using System.Web.Mvc; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Xml; - -namespace Umbraco.Web.Mvc -{ - /// - /// Extension methods for UrlHelper - /// - public static class UrlHelperExtensions - { - /// - /// Utility method for checking for valid proxy urls or redirect urls to prevent Open Redirect security issues - /// - /// - /// The url to validate - /// The url of the current local domain (to ensure we can validate if the requested url is local without dependency on the request) - /// True if it's an allowed url - public static bool ValidateProxyUrl(this UrlHelper urlHelper, string url, string callerUrl) - { - if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) - { - return false; - } - - if (url.StartsWith("//")) - return false; - - Uri requestUri; - if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out requestUri)) - { - if (string.IsNullOrEmpty(callerUrl) == false) - { - Uri localUri; - if (Uri.TryCreate(callerUrl, UriKind.RelativeOrAbsolute, out localUri)) - { - // check for local urls - - //Cannot start with // since that is not a local url - if (requestUri.OriginalString.StartsWith("//") == false - //cannot be non-absolute and also contain the char : since that will indicate a protocol - && (requestUri.IsAbsoluteUri == false && requestUri.OriginalString.Contains(":") == false) - //needs to be non-absolute or the hosts must match the current request - && (requestUri.IsAbsoluteUri == false || requestUri.Host == localUri.Host)) - { - return true; - } - } - else - { - return false; - } - } - - //we cannot continue if the url is not absolute - if (requestUri.IsAbsoluteUri == false) - { - return false; - } - - // check for valid proxy urls - var feedProxyXml = XmlHelper.OpenAsXmlDocument(IOHelper.MapPath(SystemFiles.FeedProxyConfig)); - if (feedProxyXml != null && - feedProxyXml.SelectSingleNode(string.Concat("//allow[@host = '", requestUri.Host, "']")) != null) - { - return true; - } - } - else - { - return false; - } - return false; - } - } -} +using System; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Xml; + +namespace Umbraco.Web.Mvc +{ + /// + /// Extension methods for UrlHelper + /// + public static class UrlHelperExtensions + { + /// + /// Utility method for checking for valid proxy urls or redirect urls to prevent Open Redirect security issues + /// + /// + /// The url to validate + /// The url of the current local domain (to ensure we can validate if the requested url is local without dependency on the request) + /// True if it's an allowed url + public static bool ValidateProxyUrl(this UrlHelper urlHelper, string url, string callerUrl) + { + if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) + { + return false; + } + + if (url.StartsWith("//")) + return false; + + Uri requestUri; + if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out requestUri)) + { + if (string.IsNullOrEmpty(callerUrl) == false) + { + Uri localUri; + if (Uri.TryCreate(callerUrl, UriKind.RelativeOrAbsolute, out localUri)) + { + // check for local urls + + //Cannot start with // since that is not a local url + if (requestUri.OriginalString.StartsWith("//") == false + //cannot be non-absolute and also contain the char : since that will indicate a protocol + && (requestUri.IsAbsoluteUri == false && requestUri.OriginalString.Contains(":") == false) + //needs to be non-absolute or the hosts must match the current request + && (requestUri.IsAbsoluteUri == false || requestUri.Host == localUri.Host)) + { + return true; + } + } + else + { + return false; + } + } + + //we cannot continue if the url is not absolute + if (requestUri.IsAbsoluteUri == false) + { + return false; + } + + // check for valid proxy urls + var feedProxyXml = XmlHelper.OpenAsXmlDocument(IOHelper.MapPath(SystemFiles.FeedProxyConfig)); + if (feedProxyXml != null && + feedProxyXml.SelectSingleNode(string.Concat("//allow[@host = '", requestUri.Host, "']")) != null) + { + return true; + } + } + else + { + return false; + } + return false; + } + } +} diff --git a/src/Umbraco.Web/Properties/Settings1.Designer.cs b/src/Umbraco.Web/Properties/Settings.Designer.cs similarity index 98% rename from src/Umbraco.Web/Properties/Settings1.Designer.cs rename to src/Umbraco.Web/Properties/Settings.Designer.cs index 2f68c9774c..5a5a863f4f 100644 --- a/src/Umbraco.Web/Properties/Settings1.Designer.cs +++ b/src/Umbraco.Web/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.6.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -44,7 +44,7 @@ namespace Umbraco.Web.Properties { [global::System.Configuration.ApplicationScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("Somthing")] + [global::System.Configuration.DefaultSettingValueAttribute("Something")] public string test { get { return ((string)(this["test"])); diff --git a/src/Umbraco.Web/Properties/Settings.settings b/src/Umbraco.Web/Properties/Settings.settings index d6489e3251..757b363da2 100644 --- a/src/Umbraco.Web/Properties/Settings.settings +++ b/src/Umbraco.Web/Properties/Settings.settings @@ -9,7 +9,7 @@ http://update.umbraco.org/checkforupgrade.asmx - Somthing + Something https://our.umbraco.com/umbraco/webservices/api/repository.asmx diff --git a/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs deleted file mode 100644 index 60785d283e..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors -{ - internal class ColorListPreValueEditor : ValueListPreValueEditor - { - - public ColorListPreValueEditor() - { - var field = Fields.First(); - - //use a custom editor too - field.View = "views/propertyeditors/colorpicker/colorpicker.prevalues.html"; - //change the description - field.Description = "Add, remove or sort colors."; - //change the label - field.Name = "Colors"; - //need to have some custom validation happening here - field.Validators.Add(new ColorListValidator()); - - Fields.Insert(0, new PreValueField - { - Name = "Include labels?", - View = "boolean", - Key = "useLabel", - Description = "Stores colors as a Json object containing both the color hex string and label, rather than just the hex string." - }); - } - - public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) - { - var dictionary = persistedPreVals.FormatAsDictionary(); - var items = dictionary.Where(x => x.Key != "useLabel") - .OrderBy(x => x.Value.SortOrder); - - var items2 = new Dictionary(); - foreach (var item in items) - { - var valueItem = new ColorPickerColor - { - Color = item.Value.Value, - Label = item.Value.Value, - SortOrder = item.Value.SortOrder - }; - - if (item.Value.Value.DetectIsJson()) - { - try - { - var valueObject = JsonConvert.DeserializeObject(item.Value.Value); - valueItem = new ColorPickerColor - { - Color = valueObject.Color, - Label = valueObject.Label, - SortOrder = valueObject.SortOrder - }; - } - catch - { - // let's say parsing Json failed, we'll not do anything, - // we'll just use the valueItem we built in the first place - } - } - - items2[item.Value.Id] = new JObject - { - { "value", valueItem.Color }, - { "label", valueItem.Label }, - { "sortOrder", valueItem.SortOrder } - }; - } - - var result = new Dictionary { { "items", items2 } }; - var useLabel = dictionary.ContainsKey("useLabel") && dictionary["useLabel"].Value == "1"; - if (useLabel) - result["useLabel"] = dictionary["useLabel"].Value; - - return result; - } - - public override IDictionary ConvertEditorToDb(IDictionary editorValue, PreValueCollection currentValue) - { - var val = editorValue["items"] as JArray; - var result = new Dictionary(); - if (val == null) return result; - - try - { - var useLabel = false; - if (editorValue.TryGetValue("useLabel", out var useLabelObj)) - { - useLabel = useLabelObj is string && (string) useLabelObj == "1"; - result["useLabel"] = new PreValue(useLabel ? "1" : "0"); - } - - // get all non-empty values - var index = 0; - // items get submitted in the sorted order, so just count them up - var sortOrder = -1; - foreach (var preValue in val.OfType() - .Where(x => x["value"] != null) - .Select(x => - { - var idString = x["id"] == null ? "0" : x["id"].ToString(); - int.TryParse(idString, out var id); - - var color = x["value"].ToString(); - if (string.IsNullOrWhiteSpace(color)) return null; - - var label = x["label"].ToString(); - - sortOrder++; - var value = useLabel - ? JsonConvert.SerializeObject(new { value = color, label = label, sortOrder = sortOrder }) - : color; - - return new PreValue(id, value, sortOrder); - }) - .WhereNotNull()) - { - result.Add(index.ToInvariantString(), preValue); - index++; - } - } - catch (Exception ex) - { - LogHelper.Error("Could not deserialize the posted value: " + val, ex); - } - - return result; - } - - internal class ColorListValidator : IPropertyValidator - { - public IEnumerable Validate(object value, PreValueCollection preValues, PropertyEditor editor) - { - var json = value as JArray; - if (json == null) yield break; - - //validate each item which is a json object - for (var index = 0; index < json.Count; index++) - { - var i = json[index]; - var jItem = i as JObject; - if (jItem == null || jItem["value"] == null) continue; - - //NOTE: we will be removing empty values when persisting so no need to validate - var asString = jItem["value"].ToString(); - if (asString.IsNullOrWhiteSpace()) continue; - - if (Regex.IsMatch(asString, "^([0-9a-f]{3}|[0-9a-f]{6})$", RegexOptions.IgnoreCase) == false) - { - yield return new ValidationResult("The value " + asString + " is not a valid hex color", new[] - { - //we'll make the server field the index number of the value so it can be wired up to the view - "item_" + index.ToInvariantString() - }); - } - } - } - } - } - - internal class ColorPickerColor - { - [JsonProperty("value")] - public string Color { get; set; } - [JsonProperty("label")] - public string Label { get; set; } - [JsonProperty("sortOrder")] - public int SortOrder { get; set; } - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/ColorPickerConfigurationEditor.cs index ddc4b50858..cbd4e69a9e 100644 --- a/src/Umbraco.Web/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -17,39 +17,85 @@ namespace Umbraco.Web.PropertyEditors // customize the items field items.View = "views/propertyeditors/colorpicker/colorpicker.prevalues.html"; - items.Description = "Add and remove colors"; - items.Name = "Add color"; + items.Description = "Add, remove or sort colors"; + items.Name = "Colors"; items.Validators.Add(new ColorListValidator()); } public override Dictionary ToConfigurationEditor(ColorPickerConfiguration configuration) { - var items = configuration?.Items.ToDictionary(x => x.Id.ToString(), x => GetItemValue(x, configuration.UseLabel)) ?? new object(); + var configuredItems = configuration?.Items; // ordered + object editorItems; + + if (configuredItems == null) + { + editorItems = new object(); + } + else + { + var d = new Dictionary(); + editorItems = d; + var sortOrder = 0; + foreach (var item in configuredItems) + d[item.Id.ToString()] = GetItemValue(item, configuration.UseLabel, sortOrder++); + } + var useLabel = configuration?.UseLabel ?? false; return new Dictionary { - { "items", items }, + { "items", editorItems }, { "useLabel", useLabel } }; } - private object GetItemValue(ValueListConfiguration.ValueListItem item, bool useLabel) + private object GetItemValue(ValueListConfiguration.ValueListItem item, bool useLabel, int sortOrder) { - if (useLabel) + // in: ValueListItem, Id = , Value = | { "value": "", "label": "
    public CultureInfo Culture { - get { return _culture; } + get { return _culture ?? Thread.CurrentThread.CurrentCulture; } set { EnsureWriteable(); diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index ce09bdc645..b8671b5735 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -110,11 +110,14 @@ namespace Umbraco.Web.Routing foreach (var (text, infos) in dmsg) ret.Add(UrlInfo.Message(text, infos.Count == cultures.Count ? null : string.Join(", ", infos.Select(x => x.Culture)))); - // fixme - need to add 'others' urls - // but, when? - //// get the 'other' urls - //foreach(var otherUrl in urlProvider.GetOtherUrls(content.Id)) - // urls.Add(otherUrl); + // get the 'other' urls - ie not what you'd get with GetUrl() but urls that would route to the document, nevertheless. + // for these 'other' urls, we don't check whether they are routable, collide, anything - we just report them. + // also, we are not dealing with cultures at all - that will have to wait + foreach(var otherUrl in umbracoContext.UrlProvider.GetOtherUrls(content.Id)) + { + if (urls.Any(x => x.IsUrl && x.Text == otherUrl)) continue; + ret.Add(UrlInfo.Url(otherUrl)); + } return ret; } diff --git a/src/Umbraco.Web/Trees/PackagesTreeController.cs b/src/Umbraco.Web/Trees/PackagesTreeController.cs index 5553ad00b2..31283acebf 100644 --- a/src/Umbraco.Web/Trees/PackagesTreeController.cs +++ b/src/Umbraco.Web/Trees/PackagesTreeController.cs @@ -32,7 +32,6 @@ // return root; // } - // protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) // { // var nodes = new TreeNodeCollection(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0da2704c6c..b71750fe2b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1204,7 +1204,7 @@ Code - + True True Settings.settings @@ -1524,7 +1524,7 @@ SettingsSingleFileGenerator - Settings1.Designer.cs + Settings.Designer.cs diff --git a/src/Umbraco.Web/UrlHelperExtensions.cs b/src/Umbraco.Web/UrlHelperExtensions.cs index 98856119c1..9b1c282aab 100644 --- a/src/Umbraco.Web/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/UrlHelperExtensions.cs @@ -1,193 +1,193 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Management.Instrumentation; -using System.Web.Mvc; -using System.Web.Routing; -using ClientDependency.Core.Config; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Exceptions; -using Umbraco.Web.Composing; -using Umbraco.Web.Editors; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebServices; - -namespace Umbraco.Web -{ - /// - /// Extension methods for UrlHelper - /// - public static class UrlHelperExtensions - { - /// - /// Returns the base path (not including the 'action') of the MVC controller "ExamineManagementController" - /// - /// - /// - public static string GetExamineManagementServicePath(this UrlHelper url) - { - // TODO: Possibly remove this method, I think it's unused... - var result = url.GetUmbracoApiService("GetIndexerDetails"); - return result.TrimEnd("GetIndexerDetails").EnsureEndsWith('/'); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this UrlHelper url, string actionName, RouteValueDictionary routeVals = null) - where T : UmbracoApiController - { - return url.GetUmbracoApiService(actionName, typeof(T), routeVals); - } - - /// - /// Return the Base Url (not including the action) for a Web Api service - /// - /// - /// - /// - /// - public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, string actionName) - where T : UmbracoApiController - { - return url.GetUmbracoApiService(actionName).TrimEnd(actionName); - } - - public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, Expression> methodSelector) - where T : UmbracoApiController - { - var method = Core.ExpressionHelper.GetMethodInfo(methodSelector); - if (method == null) - { - throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); - } - return url.GetUmbracoApiService(method.Name).TrimEnd(method.Name); - } - - public static string GetUmbracoApiService(this UrlHelper url, Expression> methodSelector) - where T : UmbracoApiController - { - var method = Core.ExpressionHelper.GetMethodInfo(methodSelector); - if (method == null) - { - throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); - } - var parameters = Core.ExpressionHelper.GetMethodParams(methodSelector); - var routeVals = new RouteValueDictionary(parameters); - return url.GetUmbracoApiService(method.Name, routeVals); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this UrlHelper url, string actionName, Type apiControllerType, RouteValueDictionary routeVals = null) - { - if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); - if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); - - var area = ""; - - var apiController = Current.UmbracoApiControllerTypes - .SingleOrDefault(x => x == apiControllerType); - if (apiController == null) - throw new InvalidOperationException("Could not find the umbraco api controller of type " + apiControllerType.FullName); - var metaData = PluginController.GetMetadata(apiController); - if (!metaData.AreaName.IsNullOrWhiteSpace()) - { - //set the area to the plugin area - area = metaData.AreaName; - } - return url.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area, routeVals); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, RouteValueDictionary routeVals = null) - { - return url.GetUmbracoApiService(actionName, controllerName, "", routeVals); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, string area, RouteValueDictionary routeVals = null) - { - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); - if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); - - if (routeVals == null) - { - routeVals = new RouteValueDictionary(new {httproute = "", area = area}); - } - else - { - var requiredRouteVals = new RouteValueDictionary(new { httproute = "", area = area }); - requiredRouteVals.MergeLeft(routeVals); - //copy it back now - routeVals = requiredRouteVals; - } - - return url.Action(actionName, controllerName, routeVals); - } - - - /// - /// Return the Url for an action with a cache-busting hash appended - /// - /// - /// - /// - /// - /// - public static string GetUrlWithCacheBust(this UrlHelper url, string actionName, string controllerName, RouteValueDictionary routeVals = null) - { - var applicationJs = url.Action(actionName, controllerName, routeVals); - applicationJs = applicationJs + "?umb__rnd=" + GetCacheBustHash(); - return applicationJs; - } - - /// - /// - /// - /// - public static string GetCacheBustHash() - { - //make a hash of umbraco and client dependency version - //in case the user bypasses the installer and just bumps the web.config or clientdep config - - //if in debug mode, always burst the cache - if (GlobalSettings.DebugMode) - { - return DateTime.Now.Ticks.ToString(CultureInfo.InvariantCulture).GenerateHash(); - } - - var version = Current.RuntimeState.SemanticVersion.ToSemanticString(); - return $"{version}.{ClientDependencySettings.Instance.Version}".GenerateHash(); - } - } -} +using System; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Management.Instrumentation; +using System.Web.Mvc; +using System.Web.Routing; +using ClientDependency.Core.Config; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Exceptions; +using Umbraco.Web.Composing; +using Umbraco.Web.Editors; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebServices; + +namespace Umbraco.Web +{ + /// + /// Extension methods for UrlHelper + /// + public static class UrlHelperExtensions + { + /// + /// Returns the base path (not including the 'action') of the MVC controller "ExamineManagementController" + /// + /// + /// + public static string GetExamineManagementServicePath(this UrlHelper url) + { + // TODO: Possibly remove this method, I think it's unused... + var result = url.GetUmbracoApiService("GetIndexerDetails"); + return result.TrimEnd("GetIndexerDetails").EnsureEndsWith('/'); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this UrlHelper url, string actionName, RouteValueDictionary routeVals = null) + where T : UmbracoApiController + { + return url.GetUmbracoApiService(actionName, typeof(T), routeVals); + } + + /// + /// Return the Base Url (not including the action) for a Web Api service + /// + /// + /// + /// + /// + public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, string actionName) + where T : UmbracoApiController + { + return url.GetUmbracoApiService(actionName).TrimEnd(actionName); + } + + public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, Expression> methodSelector) + where T : UmbracoApiController + { + var method = Core.ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); + } + return url.GetUmbracoApiService(method.Name).TrimEnd(method.Name); + } + + public static string GetUmbracoApiService(this UrlHelper url, Expression> methodSelector) + where T : UmbracoApiController + { + var method = Core.ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); + } + var parameters = Core.ExpressionHelper.GetMethodParams(methodSelector); + var routeVals = new RouteValueDictionary(parameters); + return url.GetUmbracoApiService(method.Name, routeVals); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this UrlHelper url, string actionName, Type apiControllerType, RouteValueDictionary routeVals = null) + { + if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); + if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); + + var area = ""; + + var apiController = Current.UmbracoApiControllerTypes + .SingleOrDefault(x => x == apiControllerType); + if (apiController == null) + throw new InvalidOperationException("Could not find the umbraco api controller of type " + apiControllerType.FullName); + var metaData = PluginController.GetMetadata(apiController); + if (!metaData.AreaName.IsNullOrWhiteSpace()) + { + //set the area to the plugin area + area = metaData.AreaName; + } + return url.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area, routeVals); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, RouteValueDictionary routeVals = null) + { + return url.GetUmbracoApiService(actionName, controllerName, "", routeVals); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, string area, RouteValueDictionary routeVals = null) + { + if (string.IsNullOrEmpty(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); + if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); + + if (routeVals == null) + { + routeVals = new RouteValueDictionary(new {httproute = "", area = area}); + } + else + { + var requiredRouteVals = new RouteValueDictionary(new { httproute = "", area = area }); + requiredRouteVals.MergeLeft(routeVals); + //copy it back now + routeVals = requiredRouteVals; + } + + return url.Action(actionName, controllerName, routeVals); + } + + + /// + /// Return the Url for an action with a cache-busting hash appended + /// + /// + /// + /// + /// + /// + public static string GetUrlWithCacheBust(this UrlHelper url, string actionName, string controllerName, RouteValueDictionary routeVals = null) + { + var applicationJs = url.Action(actionName, controllerName, routeVals); + applicationJs = applicationJs + "?umb__rnd=" + GetCacheBustHash(); + return applicationJs; + } + + /// + /// + /// + /// + public static string GetCacheBustHash() + { + //make a hash of umbraco and client dependency version + //in case the user bypasses the installer and just bumps the web.config or clientdep config + + //if in debug mode, always burst the cache + if (GlobalSettings.DebugMode) + { + return DateTime.Now.Ticks.ToString(CultureInfo.InvariantCulture).GenerateHash(); + } + + var version = Current.RuntimeState.SemanticVersion.ToSemanticString(); + return $"{version}.{ClientDependencySettings.Instance.Version}".GenerateHash(); + } + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs index b8f4506cb1..6d91847888 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs @@ -1,154 +1,154 @@ -using System; -using Umbraco.Core.Security; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Script.Services; -using System.Web.Services; -using System.Web.UI; -using umbraco; -using umbraco.cms.businesslogic; -using umbraco.cms.presentation.Trees; -using umbraco.controls.Tree; -using Umbraco.Core.Services; -using Umbraco.Web; -using Umbraco.Web.Security; -using Umbraco.Web.WebServices; - -namespace umbraco.controls.Tree -{ - /// - /// Client side ajax utlities for the tree - /// - [ScriptService] - [WebService] - public class CustomTreeService : UmbracoWebService - { - /// - /// Returns some info about the node such as path and id - /// - /// - /// - [WebMethod] - [ScriptMethod(ResponseFormat = ResponseFormat.Json)] - public NodeInfo GetNodeInfo(int id) - { - Authorize(); - - //var node = new CMSNode(id); - var node = Services.EntityService.Get(id); - return new NodeInfo() - { - Id = node.Id, - Path = node.Path, - PathAsNames = string.Join("->", - GetPathNames(node.Path.Split(',') - .Select(x => int.Parse(x)) - .ToArray())) - }; - } - - /// - /// returns the node names for each id passed in - /// - /// - /// - private string[] GetPathNames(int[] ids) - { - return ids - .Where(x => x != -1) - //.Select(x => new CMSNode(x).Text).ToArray(); - .Select(x => Services.EntityService.Get(x).Name).ToArray(); - } - - /// - /// Returns a key/value object with: json, app, js as the keys - /// - /// - [WebMethod] - [ScriptMethod(ResponseFormat = ResponseFormat.Json)] - public Dictionary GetInitAppTreeData(string app, string treeType, bool showContextMenu, bool isDialog, TreeDialogModes dialogMode, string functionToCall, string nodeKey) - { - Authorize(); - - var treeCtl = new TreeControl() - { - ShowContextMenu = showContextMenu, - IsDialog = isDialog, - DialogMode = dialogMode, - App = app, - TreeType = string.IsNullOrEmpty(treeType) ? "" : treeType, //don't set the tree type unless explicitly set - NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey, - //StartNodeID = -1, //TODO: set this based on parameters! - FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall - }; - - var returnVal = new Dictionary(); - - if (string.IsNullOrEmpty(treeType)) - { - //if there's not tree type specified, then render out the tree as per normal with the normal - //way of doing things - returnVal.Add("json", treeCtl.GetJSONInitNode()); - } - else - { - //since 4.5.1 has a bug in it, it ignores if the treeType is specified and will always only render - //the whole APP not just a specific tree. - //this is a work around for this bug until it is fixed (which should be fixed in 4.5.2 - - //get the tree that we need to render - var tree = TreeDefinitionCollection.Instance.FindTree(treeType).CreateInstance(); - tree.ShowContextMenu = showContextMenu; - tree.IsDialog = isDialog; - tree.DialogMode = dialogMode; - tree.NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey; - tree.FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall; - - //now render it's start node - var xTree = new XmlTree(); - - //we're going to hijack the node name here to make it say content/media - var node = tree.RootNode; - if (node.Text.Equals("[FilteredContentTree]")) node.Text = Services.TextService.Localize("content"); - else if (node.Text.Equals("[FilteredMediaTree]")) node.Text = Services.TextService.Localize("media"); - xTree.Add(node); - - returnVal.Add("json", xTree.ToString()); - } - - returnVal.Add("app", app); - returnVal.Add("js", treeCtl.JSCurrApp); - - return returnVal; - } - - internal void Authorize() - { - if (ValidateCurrentUser() == false) - throw new Exception("Client authorization failed. User is not logged in"); - } - - - /// - /// Validates the currently logged in user and ensures they are not timed out - /// - /// - private bool ValidateCurrentUser() - { - var identity = Context.GetCurrentIdentity( - //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! - // Without this check, anything that is using this legacy API, like ui.Text will - // automatically log the back office user in even if it is a front-end request (if there is - // a back office user logged in. This can cause problems becaues the identity is changing mid - // request. For example: http://issues.umbraco.org/issue/U4-4010 - HttpContext.Current.CurrentHandler is Page); - - if (identity != null) - { - return true; - } - return false; - } - } -} +using System; +using Umbraco.Core.Security; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Script.Services; +using System.Web.Services; +using System.Web.UI; +using umbraco; +using umbraco.cms.businesslogic; +using umbraco.cms.presentation.Trees; +using umbraco.controls.Tree; +using Umbraco.Core.Services; +using Umbraco.Web; +using Umbraco.Web.Security; +using Umbraco.Web.WebServices; + +namespace umbraco.controls.Tree +{ + /// + /// Client side ajax utlities for the tree + /// + [ScriptService] + [WebService] + public class CustomTreeService : UmbracoWebService + { + /// + /// Returns some info about the node such as path and id + /// + /// + /// + [WebMethod] + [ScriptMethod(ResponseFormat = ResponseFormat.Json)] + public NodeInfo GetNodeInfo(int id) + { + Authorize(); + + //var node = new CMSNode(id); + var node = Services.EntityService.Get(id); + return new NodeInfo() + { + Id = node.Id, + Path = node.Path, + PathAsNames = string.Join("->", + GetPathNames(node.Path.Split(',') + .Select(x => int.Parse(x)) + .ToArray())) + }; + } + + /// + /// returns the node names for each id passed in + /// + /// + /// + private string[] GetPathNames(int[] ids) + { + return ids + .Where(x => x != -1) + //.Select(x => new CMSNode(x).Text).ToArray(); + .Select(x => Services.EntityService.Get(x).Name).ToArray(); + } + + /// + /// Returns a key/value object with: json, app, js as the keys + /// + /// + [WebMethod] + [ScriptMethod(ResponseFormat = ResponseFormat.Json)] + public Dictionary GetInitAppTreeData(string app, string treeType, bool showContextMenu, bool isDialog, TreeDialogModes dialogMode, string functionToCall, string nodeKey) + { + Authorize(); + + var treeCtl = new TreeControl() + { + ShowContextMenu = showContextMenu, + IsDialog = isDialog, + DialogMode = dialogMode, + App = app, + TreeType = string.IsNullOrEmpty(treeType) ? "" : treeType, //don't set the tree type unless explicitly set + NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey, + //StartNodeID = -1, //TODO: set this based on parameters! + FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall + }; + + var returnVal = new Dictionary(); + + if (string.IsNullOrEmpty(treeType)) + { + //if there's not tree type specified, then render out the tree as per normal with the normal + //way of doing things + returnVal.Add("json", treeCtl.GetJSONInitNode()); + } + else + { + //since 4.5.1 has a bug in it, it ignores if the treeType is specified and will always only render + //the whole APP not just a specific tree. + //this is a work around for this bug until it is fixed (which should be fixed in 4.5.2 + + //get the tree that we need to render + var tree = TreeDefinitionCollection.Instance.FindTree(treeType).CreateInstance(); + tree.ShowContextMenu = showContextMenu; + tree.IsDialog = isDialog; + tree.DialogMode = dialogMode; + tree.NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey; + tree.FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall; + + //now render it's start node + var xTree = new XmlTree(); + + //we're going to hijack the node name here to make it say content/media + var node = tree.RootNode; + if (node.Text.Equals("[FilteredContentTree]")) node.Text = Services.TextService.Localize("content"); + else if (node.Text.Equals("[FilteredMediaTree]")) node.Text = Services.TextService.Localize("media"); + xTree.Add(node); + + returnVal.Add("json", xTree.ToString()); + } + + returnVal.Add("app", app); + returnVal.Add("js", treeCtl.JSCurrApp); + + return returnVal; + } + + internal void Authorize() + { + if (ValidateCurrentUser() == false) + throw new Exception("Client authorization failed. User is not logged in"); + } + + + /// + /// Validates the currently logged in user and ensures they are not timed out + /// + /// + private bool ValidateCurrentUser() + { + var identity = Context.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + + if (identity != null) + { + return true; + } + return false; + } + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs index 00788fab3f..db04f26bcf 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs @@ -1,83 +1,83 @@ -using System; -using System.Text; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web.UI.Pages; -using Umbraco.Web; -using Umbraco.Web.Composing; -using Umbraco.Web.Editors; -using Umbraco.Web.WebServices; -using Umbraco.Web._Legacy.Actions; - - -namespace umbraco.dialogs -{ - public partial class AssignDomain2 : UmbracoEnsuredPage - { - protected override void OnInit(EventArgs e) - { - base.OnInit(e); - - var nodeId = GetNodeId(); - CheckPathAndPermissions(nodeId, UmbracoObjectTypes.Document, ActionAssignDomain.Instance); - } - - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - - var nodeId = GetNodeId(); - var node = Services.ContentService.GetById(nodeId); - - if (node == null) - { - feedback.Text = Services.TextService.Localize("assignDomain/invalidNode"); - pane_language.Visible = false; - pane_domains.Visible = false; - p_buttons.Visible = false; - return; - } - - pane_language.Title = Services.TextService.Localize("assignDomain/setLanguage"); - pane_domains.Title = Services.TextService.Localize("assignDomain/setDomains"); - prop_language.Text = Services.TextService.Localize("assignDomain/language"); - - var nodeDomains = Services.DomainService.GetAssignedDomains(nodeId, true).ToArray(); - var wildcard = nodeDomains.FirstOrDefault(d => d.IsWildcard); - - var sb = new StringBuilder(); - sb.Append("languages: ["); - var i = 0; - foreach (var language in Current.Services.LocalizationService.GetAllLanguages()) - sb.AppendFormat("{0}{{ \"Id\": {1}, \"Code\": \"{2}\" }}", (i++ == 0 ? "" : ","), language.Id, language.IsoCode); - sb.Append("]\r\n"); - - sb.AppendFormat(",language: {0}", wildcard == null ? "undefined" : wildcard.LanguageId.ToString()); - - sb.Append(",domains: ["); - i = 0; - foreach (var domain in nodeDomains.Where(d => d.IsWildcard == false)) - sb.AppendFormat("{0}{{ \"Name\": \"{1}\", \"Lang\": \"{2}\" }}", (i++ == 0 ? "" :","), domain.DomainName, domain.LanguageId); - sb.Append("]\r\n"); - - data.Text = sb.ToString(); - } - - protected int GetNodeId() - { - int nodeId; - if (int.TryParse(Request.QueryString["id"], out nodeId) == false) - nodeId = -1; - return nodeId; - } - - protected string GetRestServicePath() - { - const string action = "ListDomains"; - var path = Url.GetUmbracoApiService(action); - return path.TrimEnd(action).EnsureEndsWith('/'); - } - } -} +using System; +using System.Text; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.UI.Pages; +using Umbraco.Web; +using Umbraco.Web.Composing; +using Umbraco.Web.Editors; +using Umbraco.Web.WebServices; +using Umbraco.Web._Legacy.Actions; + + +namespace umbraco.dialogs +{ + public partial class AssignDomain2 : UmbracoEnsuredPage + { + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + + var nodeId = GetNodeId(); + CheckPathAndPermissions(nodeId, UmbracoObjectTypes.Document, ActionAssignDomain.Instance); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + var nodeId = GetNodeId(); + var node = Services.ContentService.GetById(nodeId); + + if (node == null) + { + feedback.Text = Services.TextService.Localize("assignDomain/invalidNode"); + pane_language.Visible = false; + pane_domains.Visible = false; + p_buttons.Visible = false; + return; + } + + pane_language.Title = Services.TextService.Localize("assignDomain/setLanguage"); + pane_domains.Title = Services.TextService.Localize("assignDomain/setDomains"); + prop_language.Text = Services.TextService.Localize("assignDomain/language"); + + var nodeDomains = Services.DomainService.GetAssignedDomains(nodeId, true).ToArray(); + var wildcard = nodeDomains.FirstOrDefault(d => d.IsWildcard); + + var sb = new StringBuilder(); + sb.Append("languages: ["); + var i = 0; + foreach (var language in Current.Services.LocalizationService.GetAllLanguages()) + sb.AppendFormat("{0}{{ \"Id\": {1}, \"Code\": \"{2}\" }}", (i++ == 0 ? "" : ","), language.Id, language.IsoCode); + sb.Append("]\r\n"); + + sb.AppendFormat(",language: {0}", wildcard == null ? "undefined" : wildcard.LanguageId.ToString()); + + sb.Append(",domains: ["); + i = 0; + foreach (var domain in nodeDomains.Where(d => d.IsWildcard == false)) + sb.AppendFormat("{0}{{ \"Name\": \"{1}\", \"Lang\": \"{2}\" }}", (i++ == 0 ? "" :","), domain.DomainName, domain.LanguageId); + sb.Append("]\r\n"); + + data.Text = sb.ToString(); + } + + protected int GetNodeId() + { + int nodeId; + if (int.TryParse(Request.QueryString["id"], out nodeId) == false) + nodeId = -1; + return nodeId; + } + + protected string GetRestServicePath() + { + const string action = "ListDomains"; + var path = Url.GetUmbracoApiService(action); + return path.TrimEnd(action).EnsureEndsWith('/'); + } + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs index 95fe3626b6..ca26c5c08d 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs @@ -1,262 +1,262 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Web.Script.Services; -using System.Web.Services; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Web; -using Umbraco.Web.Composing; -using Umbraco.Web._Legacy.Actions; - -namespace umbraco.presentation.webservices -{ - /// - /// Summary description for nodeSorter - /// - [WebService(Namespace = "http://umbraco.org/")] - [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] - [ToolboxItem(false)] - [ScriptService] - public class nodeSorter : UmbracoAuthorizedWebService - { - [WebMethod] - public SortNode GetNodes(string ParentId, string App) - { - if (AuthorizeRequest()) - { - var nodes = new List(); - - // "hack for stylesheet" - if (App == "settings") - { - var stylesheet = Services.FileService.GetStylesheetByName(ParentId.EnsureEndsWith(".css")); - if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + ParentId); - - var sort = 0; - foreach (var child in stylesheet.Properties) - { - nodes.Add(new SortNode(child.Name.GetHashCode(), sort, child.Name, DateTime.Now)); - sort++; - } - - return new SortNode() - { - SortNodes = nodes.ToArray() - }; - } - else - { - var asInt = int.Parse(ParentId); - - var parent = new SortNode { Id = asInt }; - - var entityService = Services.EntityService; - - // Root nodes? - if (asInt == -1) - { - if (App == "media") - { - var rootMedia = entityService.GetRootEntities(UmbracoObjectTypes.Media); - nodes.AddRange(rootMedia.Select(media => new SortNode(media.Id, media.SortOrder, media.Name, media.CreateDate))); - } - else - { - var rootContent = entityService.GetRootEntities(UmbracoObjectTypes.Document); - nodes.AddRange(rootContent.Select(content => new SortNode(content.Id, content.SortOrder, content.Name, content.CreateDate))); - } - } - else - { - var children = entityService.GetChildren(asInt); - nodes.AddRange(children.Select(child => new SortNode(child.Id, child.SortOrder, child.Name, child.CreateDate))); - } - - - parent.SortNodes = nodes.ToArray(); - - return parent; - } - } - - throw new ArgumentException("User not logged in"); - } - - public void UpdateSortOrder(int ParentId, string SortOrder) - { - UpdateSortOrder(ParentId.ToString(), SortOrder); - } - - [WebMethod] - public void UpdateSortOrder(string ParentId, string SortOrder) - { - if (AuthorizeRequest() == false) return; - if (SortOrder.Trim().Length <= 0) return; - - var isContent = Context.Request.GetItemAsString("app") == "content" | Context.Request.GetItemAsString("app") == ""; - var isMedia = Context.Request.GetItemAsString("app") == "media"; - - //ensure user is authorized for the app requested - if (isContent && AuthorizeRequest(Constants.Applications.Content.ToString()) == false) return; - if (isMedia && AuthorizeRequest(Constants.Applications.Media.ToString()) == false) return; - - var ids = SortOrder.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); - if (isContent) - { - SortContent(ids, int.Parse(ParentId)); - } - else if (isMedia) - { - SortMedia(ids); - } - else - { - SortStylesheetProperties(ParentId, ids); - } - } - - private void SortMedia(string[] ids) - { - var mediaService = Services.MediaService; - var sortedMedia = new List(); - try - { - for (var i = 0; i < ids.Length; i++) - { - var id = int.Parse(ids[i]); - var m = mediaService.GetById(id); - sortedMedia.Add(m); - } - - // Save Media with new sort order and update content xml in db accordingly - var sorted = mediaService.Sort(sortedMedia); - } - catch (Exception ex) - { - Current.Logger.Error("Could not update media sort order", ex); - } - } - - - private void SortStylesheetProperties(string stylesheetName, string[] names) - { - var stylesheet = Services.FileService.GetStylesheetByName(stylesheetName.EnsureEndsWith(".css")); - if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + stylesheetName); - - var currProps = stylesheet.Properties.ToArray(); - //remove them all first - foreach (var prop in currProps) - { - stylesheet.RemoveProperty(prop.Name); - } - - //re-add them in the right order - for (var i = 0; i < names.Length; i++) - { - var found = currProps.Single(x => x.Name == names[i]); - stylesheet.AddProperty(found); - } - - Services.FileService.SaveStylesheet(stylesheet); - } - - private void SortContent(string[] ids, int parentId) - { - var contentService = Services.ContentService; - try - { - // Save content with new sort order and update db+cache accordingly - var intIds = new List(); - foreach (var stringId in ids) - { - int intId; - if (int.TryParse(stringId, out intId)) - intIds.Add(intId); - } - var sorted = contentService.Sort(intIds.ToArray()); - - // refresh sort order on cached xml - // but no... this is not distributed - solely relying on content service & events should be enough - //content.Instance.SortNodes(parentId); - - //send notifications! TODO: This should be put somewhere centralized instead of hard coded directly here - if (parentId > 0) - { - Services.NotificationService.SendNotification(contentService.GetById(parentId), ActionSort.Instance, UmbracoContext, Services.TextService, GlobalSettings); - } - - } - catch (Exception ex) - { - Current.Logger.Error("Could not update content sort order", ex); - } - } - - } - - [Serializable] - public class SortNode - { - public SortNode() - { - } - - private SortNode[] _sortNodes; - - public SortNode[] SortNodes - { - get { return _sortNodes; } - set { _sortNodes = value; } - } - - public int TotalNodes - { - get { return _sortNodes != null ? _sortNodes.Length : 0; } - set { int test = value; } - } - - public SortNode(int Id, int SortOrder, string Name, DateTime CreateDate) - { - _id = Id; - _sortOrder = SortOrder; - _name = Name; - _createDate = CreateDate; - } - - private DateTime _createDate; - - public DateTime CreateDate - { - get { return _createDate; } - set { _createDate = value; } - } - - private string _name; - - public string Name - { - get { return _name; } - set { _name = value; } - } - - private int _sortOrder; - - public int SortOrder - { - get { return _sortOrder; } - set { _sortOrder = value; } - } - - private int _id; - - public int Id - { - get { return _id; } - set { _id = value; } - } - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Web.Script.Services; +using System.Web.Services; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Web; +using Umbraco.Web.Composing; +using Umbraco.Web._Legacy.Actions; + +namespace umbraco.presentation.webservices +{ + /// + /// Summary description for nodeSorter + /// + [WebService(Namespace = "http://umbraco.org/")] + [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] + [ToolboxItem(false)] + [ScriptService] + public class nodeSorter : UmbracoAuthorizedWebService + { + [WebMethod] + public SortNode GetNodes(string ParentId, string App) + { + if (AuthorizeRequest()) + { + var nodes = new List(); + + // "hack for stylesheet" + if (App == "settings") + { + var stylesheet = Services.FileService.GetStylesheetByName(ParentId.EnsureEndsWith(".css")); + if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + ParentId); + + var sort = 0; + foreach (var child in stylesheet.Properties) + { + nodes.Add(new SortNode(child.Name.GetHashCode(), sort, child.Name, DateTime.Now)); + sort++; + } + + return new SortNode() + { + SortNodes = nodes.ToArray() + }; + } + else + { + var asInt = int.Parse(ParentId); + + var parent = new SortNode { Id = asInt }; + + var entityService = Services.EntityService; + + // Root nodes? + if (asInt == -1) + { + if (App == "media") + { + var rootMedia = entityService.GetRootEntities(UmbracoObjectTypes.Media); + nodes.AddRange(rootMedia.Select(media => new SortNode(media.Id, media.SortOrder, media.Name, media.CreateDate))); + } + else + { + var rootContent = entityService.GetRootEntities(UmbracoObjectTypes.Document); + nodes.AddRange(rootContent.Select(content => new SortNode(content.Id, content.SortOrder, content.Name, content.CreateDate))); + } + } + else + { + var children = entityService.GetChildren(asInt); + nodes.AddRange(children.Select(child => new SortNode(child.Id, child.SortOrder, child.Name, child.CreateDate))); + } + + + parent.SortNodes = nodes.ToArray(); + + return parent; + } + } + + throw new ArgumentException("User not logged in"); + } + + public void UpdateSortOrder(int ParentId, string SortOrder) + { + UpdateSortOrder(ParentId.ToString(), SortOrder); + } + + [WebMethod] + public void UpdateSortOrder(string ParentId, string SortOrder) + { + if (AuthorizeRequest() == false) return; + if (SortOrder.Trim().Length <= 0) return; + + var isContent = Context.Request.GetItemAsString("app") == "content" | Context.Request.GetItemAsString("app") == ""; + var isMedia = Context.Request.GetItemAsString("app") == "media"; + + //ensure user is authorized for the app requested + if (isContent && AuthorizeRequest(Constants.Applications.Content.ToString()) == false) return; + if (isMedia && AuthorizeRequest(Constants.Applications.Media.ToString()) == false) return; + + var ids = SortOrder.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + if (isContent) + { + SortContent(ids, int.Parse(ParentId)); + } + else if (isMedia) + { + SortMedia(ids); + } + else + { + SortStylesheetProperties(ParentId, ids); + } + } + + private void SortMedia(string[] ids) + { + var mediaService = Services.MediaService; + var sortedMedia = new List(); + try + { + for (var i = 0; i < ids.Length; i++) + { + var id = int.Parse(ids[i]); + var m = mediaService.GetById(id); + sortedMedia.Add(m); + } + + // Save Media with new sort order and update content xml in db accordingly + var sorted = mediaService.Sort(sortedMedia); + } + catch (Exception ex) + { + Current.Logger.Error("Could not update media sort order", ex); + } + } + + + private void SortStylesheetProperties(string stylesheetName, string[] names) + { + var stylesheet = Services.FileService.GetStylesheetByName(stylesheetName.EnsureEndsWith(".css")); + if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + stylesheetName); + + var currProps = stylesheet.Properties.ToArray(); + //remove them all first + foreach (var prop in currProps) + { + stylesheet.RemoveProperty(prop.Name); + } + + //re-add them in the right order + for (var i = 0; i < names.Length; i++) + { + var found = currProps.Single(x => x.Name == names[i]); + stylesheet.AddProperty(found); + } + + Services.FileService.SaveStylesheet(stylesheet); + } + + private void SortContent(string[] ids, int parentId) + { + var contentService = Services.ContentService; + try + { + // Save content with new sort order and update db+cache accordingly + var intIds = new List(); + foreach (var stringId in ids) + { + int intId; + if (int.TryParse(stringId, out intId)) + intIds.Add(intId); + } + var sorted = contentService.Sort(intIds.ToArray()); + + // refresh sort order on cached xml + // but no... this is not distributed - solely relying on content service & events should be enough + //content.Instance.SortNodes(parentId); + + //send notifications! TODO: This should be put somewhere centralized instead of hard coded directly here + if (parentId > 0) + { + Services.NotificationService.SendNotification(contentService.GetById(parentId), ActionSort.Instance, UmbracoContext, Services.TextService, GlobalSettings); + } + + } + catch (Exception ex) + { + Current.Logger.Error("Could not update content sort order", ex); + } + } + + } + + [Serializable] + public class SortNode + { + public SortNode() + { + } + + private SortNode[] _sortNodes; + + public SortNode[] SortNodes + { + get { return _sortNodes; } + set { _sortNodes = value; } + } + + public int TotalNodes + { + get { return _sortNodes != null ? _sortNodes.Length : 0; } + set { int test = value; } + } + + public SortNode(int Id, int SortOrder, string Name, DateTime CreateDate) + { + _id = Id; + _sortOrder = SortOrder; + _name = Name; + _createDate = CreateDate; + } + + private DateTime _createDate; + + public DateTime CreateDate + { + get { return _createDate; } + set { _createDate = value; } + } + + private string _name; + + public string Name + { + get { return _name; } + set { _name = value; } + } + + private int _sortOrder; + + public int SortOrder + { + get { return _sortOrder; } + set { _sortOrder = value; } + } + + private int _id; + + public int Id + { + get { return _id; } + set { _id = value; } + } + } +}