diff --git a/src/DotVVM.Framework.Hosting.AspNetCore/ApplicationBuilderExtensions.cs b/src/DotVVM.Framework.Hosting.AspNetCore/ApplicationBuilderExtensions.cs index a8a576977e..40b61552fc 100644 --- a/src/DotVVM.Framework.Hosting.AspNetCore/ApplicationBuilderExtensions.cs +++ b/src/DotVVM.Framework.Hosting.AspNetCore/ApplicationBuilderExtensions.cs @@ -63,8 +63,8 @@ private static DotvvmConfiguration UseDotVVM(this IApplicationBuilder app, strin app.UseMiddleware(); } - app.UseMiddleware(config, new List - { + app.UseMiddleware(config, new List { + ActivatorUtilities.CreateInstance(config.ServiceProvider), ActivatorUtilities.CreateInstance(app.ApplicationServices), DotvvmFileUploadMiddleware.TryCreate(app.ApplicationServices), new DotvvmReturnedFileMiddleware(), diff --git a/src/DotVVM.Framework.Hosting.AspNetCore/Security/DefaultCsrfProtector.cs b/src/DotVVM.Framework.Hosting.AspNetCore/Security/DefaultCsrfProtector.cs index 1635be4efd..f928453cb0 100644 --- a/src/DotVVM.Framework.Hosting.AspNetCore/Security/DefaultCsrfProtector.cs +++ b/src/DotVVM.Framework.Hosting.AspNetCore/Security/DefaultCsrfProtector.cs @@ -54,7 +54,7 @@ public string GenerateToken(IDotvvmRequestContext context) public void VerifyToken(IDotvvmRequestContext context, string token) { if (context == null) throw new ArgumentNullException(nameof(context)); - if (string.IsNullOrWhiteSpace(token)) throw new SecurityException("CSRF protection token is missing."); + if (string.IsNullOrWhiteSpace(token)) throw new CorruptedCsrfTokenException("CSRF protection token is missing."); // Construct protector with purposes var protector = this.protectionProvider.CreateProtector(PURPOSE_TOKEN); @@ -69,7 +69,7 @@ public void VerifyToken(IDotvvmRequestContext context, string token) catch (Exception ex) { // Incorrect Base64 formatting of crypto protection error - throw new SecurityException("CSRF protection token is invalid.", ex); + throw new CorruptedCsrfTokenException("CSRF protection token is invalid.", ex); } // Get SID from cookie and compare with token one @@ -104,7 +104,7 @@ private byte[] GetOrCreateSessionId(IDotvvmRequestContext context, bool canGener // Incorrect Base64 formatting of crypto protection error // Generate new one or thow error if can't if (!canGenerate) - throw new SecurityException("Value of the SessionID cookie is corrupted or has been tampered with.", ex); + throw new CorruptedCsrfTokenException("Value of the SessionID cookie is corrupted or has been tampered with.", ex); // else suppress error and generate new SID } } @@ -136,7 +136,7 @@ private byte[] GetOrCreateSessionId(IDotvvmRequestContext context, bool canGener } else { - throw new SecurityException("SessionID cookie is missing, so can't verify CSRF token."); + throw new CorruptedCsrfTokenException("SessionID cookie is missing, so can't verify CSRF token."); } } diff --git a/src/DotVVM.Framework.Hosting.Owin/AppBuilderExtensions.cs b/src/DotVVM.Framework.Hosting.Owin/AppBuilderExtensions.cs index 7e71bf2445..158d120ecb 100644 --- a/src/DotVVM.Framework.Hosting.Owin/AppBuilderExtensions.cs +++ b/src/DotVVM.Framework.Hosting.Owin/AppBuilderExtensions.cs @@ -78,6 +78,7 @@ private static DotvvmConfiguration UseDotVVM(this IAppBuilder app, string applic } app.Use(config, new List { + ActivatorUtilities.CreateInstance(config.ServiceProvider), ActivatorUtilities.CreateInstance(config.ServiceProvider), DotvvmFileUploadMiddleware.TryCreate(config.ServiceProvider), new DotvvmReturnedFileMiddleware(), diff --git a/src/DotVVM.Framework.Hosting.Owin/Security/DefaultCsrfProtector.cs b/src/DotVVM.Framework.Hosting.Owin/Security/DefaultCsrfProtector.cs index 7a5debec5a..1d9cb9e37a 100644 --- a/src/DotVVM.Framework.Hosting.Owin/Security/DefaultCsrfProtector.cs +++ b/src/DotVVM.Framework.Hosting.Owin/Security/DefaultCsrfProtector.cs @@ -55,7 +55,7 @@ public string GenerateToken(IDotvvmRequestContext context) public void VerifyToken(IDotvvmRequestContext context, string token) { if (context == null) throw new ArgumentNullException(nameof(context)); - if (string.IsNullOrWhiteSpace(token)) throw new SecurityException("CSRF protection token is missing."); + if (string.IsNullOrWhiteSpace(token)) throw new CorruptedCsrfTokenException("CSRF protection token is missing."); // Construct protector with purposes var protector = this.protectionProvider.Create(PURPOSE_TOKEN); @@ -70,12 +70,12 @@ public void VerifyToken(IDotvvmRequestContext context, string token) catch (Exception ex) { // Incorrect Base64 formatting of crypto protection error - throw new SecurityException("CSRF protection token is invalid.", ex); + throw new CorruptedCsrfTokenException("CSRF protection token is invalid.", ex); } // Get SID from cookie and compare with token one var cookieSid = this.GetOrCreateSessionId(context, canGenerate: false); // should not generate new token - if (!cookieSid.SequenceEqual(tokenSid)) throw new SecurityException("CSRF protection token is invalid."); + if (!cookieSid.SequenceEqual(tokenSid)) throw new CorruptedCsrfTokenException("CSRF protection token is invalid."); } private byte[] GetOrCreateSessionId(IDotvvmRequestContext context, bool canGenerate = true) @@ -104,7 +104,7 @@ private byte[] GetOrCreateSessionId(IDotvvmRequestContext context, bool canGener // Incorrect Base64 formatting of crypto protection error // Generate new one or thow error if can't if (!canGenerate) - throw new SecurityException("Value of the SessionID cookie is corrupted or has been tampered with.", ex); + throw new CorruptedCsrfTokenException("Value of the SessionID cookie is corrupted or has been tampered with.", ex); // else suppress error and generate new SID } } diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index a553f14611..085f8bc16f 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -295,7 +295,6 @@ public void BindingCompiler_Valid_MemberAssignment() Assert.AreEqual("42", vm.TestViewModel2.SomeString); } - [TestMethod] public void BindingCompiler_Valid_NamespaceAlias() { @@ -410,6 +409,34 @@ public void BindingCompiler_SimpleBlockExpression() Assert.AreEqual("akkll", result); } + [TestMethod] + public void BindingCompiler_SimpleBlockExpression_TaskSequence_TaskNonTask() + { + var vm = new TestViewModel4(); + var resultTask = (Task)ExecuteBinding("Increment(); Number = Number * 5", new[] { vm }); + resultTask.Wait(); + Assert.AreEqual(5, vm.Number); + } + + [TestMethod] + public void BindingCompiler_SimpleBlockExpression_TaskSequence_NonTaskTask() + { + var vm = new TestViewModel4(); + var resultTask = (Task)ExecuteBinding("Number = 10; Increment();", new[] { vm }); + resultTask.Wait(); + Assert.AreEqual(11, vm.Number); + } + + [TestMethod] + public void BindingCompiler_SimpleBlockExpression_TaskSequence_VoidTaskJoining() + { + var vm = new TestViewModel4(); + var resultTask = (Task)ExecuteBinding("Increment(); Multiply()", new[] { vm }); + resultTask.Wait(); + Assert.AreEqual(10, vm.Number); + } + + [TestMethod] public void BindingCompiler_MultiBlockExpression() { @@ -618,6 +645,23 @@ class TestViewModel3 : DotvvmViewModelBase public string SomeString { get; set; } } + class TestViewModel4 + { + public int Number { get; set; } + + public async Task Increment() + { + await Task.Delay(100); + Number += 1; + } + + public Task Multiply() + { + Number *= 10; + return Task.Delay(100); + } + } + class Something { public bool Value { get; set; } diff --git a/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs index 564f48be6e..50d32faca6 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs @@ -58,14 +58,14 @@ public string CompileBinding(string expression, Type[] contexts, Type expectedTy public void StaticCommandCompilation_SimpleCommand() { var result = CompileBinding("StaticCommands.GetLength(StringProp)", typeof(TestViewModel)); - Assert.AreEqual("(function(a,b){return new Promise(function(resolve){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[b.$data.StringProp()],function(r_0){resolve(r_0);});});}(this,ko.contextFor(this)))", result); + Assert.AreEqual("(function(a,b){return new Promise(function(resolve,reject){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[b.$data.StringProp()],function(r_0){resolve(r_0);},reject);});}(this,ko.contextFor(this)))", result); } [TestMethod] public void StaticCommandCompilation_AssignedCommand() { var result = CompileBinding("StringProp = StaticCommands.GetLength(StringProp).ToString()", typeof(TestViewModel)); - Assert.AreEqual("(function(a,b){return new Promise(function(resolve){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[b.$data.StringProp()],function(r_0){resolve(b.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_0)()).StringProp());});});}(this,ko.contextFor(this)))", result); + Assert.AreEqual("(function(a,b){return new Promise(function(resolve,reject){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[b.$data.StringProp()],function(r_0){resolve(b.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_0)()).StringProp());},reject);});}(this,ko.contextFor(this)))", result); } [TestMethod] @@ -79,21 +79,21 @@ public void StaticCommandCompilation_JsOnlyCommand() public void StaticCommandCompilation_ChainedCommands() { var result = CompileBinding("StringProp = StaticCommands.GetLength(StaticCommands.GetLength(StringProp).ToString()).ToString()", typeof(TestViewModel)); - Assert.AreEqual("(function(a,b){return new Promise(function(resolve){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[b.$data.StringProp()],function(r_0){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[dotvvm.globalize.bindingNumberToString(r_0)()],function(r_1){resolve(b.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_1)()).StringProp());});});});}(this,ko.contextFor(this)))", result); + Assert.AreEqual("(function(a,b){return new Promise(function(resolve,reject){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[b.$data.StringProp()],function(r_0){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[dotvvm.globalize.bindingNumberToString(r_0)()],function(r_1){resolve(b.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_1)()).StringProp());},reject);},reject);});}(this,ko.contextFor(this)))", result); } [TestMethod] public void StaticCommandCompilation_ChainedCommandsWithSemicolon() { var result = CompileBinding("StringProp = StaticCommands.GetLength(StringProp).ToString(); StringProp = StaticCommands.GetLength(StringProp).ToString()", typeof(TestViewModel)); - Assert.AreEqual("(function(a,c,b){return new Promise(function(resolve){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[c.$data.StringProp()],function(r_0){(b=c.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_0)()).StringProp(),dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[c.$data.StringProp()],function(r_1){resolve((b,c.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_1)()).StringProp()));}));});});}(this,ko.contextFor(this)))", result); + Assert.AreEqual("(function(a,c,b){return new Promise(function(resolve,reject){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[c.$data.StringProp()],function(r_0){(b=c.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_0)()).StringProp(),dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0TGVuZ3RoIixbXSwiQUE9PSJd\",[c.$data.StringProp()],function(r_1){resolve((b,c.$data.StringProp(dotvvm.globalize.bindingNumberToString(r_1)()).StringProp()));},reject));},reject);});}(this,ko.contextFor(this)))", result); } [TestMethod] public void StaticCommandCompilation_DateTimeResultAssignment() { var result = CompileBinding("DateFrom = StaticCommands.GetDate()", typeof(TestViewModel)); - Assert.AreEqual("(function(a,b){return new Promise(function(resolve){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0RGF0ZSIsW10sIiJd\",[],function(r_0){resolve(b.$data.DateFrom(dotvvm.serialization.serializeDate(r_0,false)).DateFrom());});});}(this,ko.contextFor(this)))", result); + Assert.AreEqual("(function(a,b){return new Promise(function(resolve,reject){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuU3RhdGljQ29tbWFuZHMsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiR2V0RGF0ZSIsW10sIiJd\",[],function(r_0){resolve(b.$data.DateFrom(dotvvm.serialization.serializeDate(r_0,false)).DateFrom());},reject);});}(this,ko.contextFor(this)))", result); } [TestMethod] @@ -115,7 +115,7 @@ public void StaticCommandCompilation_PossibleAmniguousMatch() { var result = CompileBinding("SomeString = injectedService.Load(SomeString)", new[] { typeof(TestViewModel3) }, typeof(Func)); - Assert.AreEqual("(function(a,b){return new Promise(function(resolve){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuVGVzdFNlcnZpY2UsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiTG9hZCIsW10sIkFRQT0iXQ==\",[b.$data.SomeString()],function(r_0){resolve(b.$data.SomeString(r_0).SomeString());});});}(this,ko.contextFor(this)))", result); + Assert.AreEqual("(function(a,b){return new Promise(function(resolve,reject){dotvvm.staticCommandPostback(\"root\",a,\"WARNING/NOT/ENCRYPTED+++WyJEb3RWVk0uRnJhbWV3b3JrLlRlc3RzLkJpbmRpbmcuVGVzdFNlcnZpY2UsIERvdFZWTS5GcmFtZXdvcmsuVGVzdHMuQ29tbW9uIiwiTG9hZCIsW10sIkFRQT0iXQ==\",[b.$data.SomeString()],function(r_0){resolve(b.$data.SomeString(r_0).SomeString());},reject);});}(this,ko.contextFor(this)))", result); } [TestMethod] diff --git a/src/DotVVM.Framework.Tests.Common/Routing/UrlHelperTests.cs b/src/DotVVM.Framework.Tests.Common/Routing/UrlHelperTests.cs index 70f68b583b..f3abee3e8b 100644 --- a/src/DotVVM.Framework.Tests.Common/Routing/UrlHelperTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Routing/UrlHelperTests.cs @@ -24,6 +24,7 @@ public class UrlHelperTests [DataRow(@"\\www.google.com", false)] // Chrome replaces backslashes with forward slashes... [DataRow(@"\/www.google.com", false)] [DataRow(@"/\www.google.com", false)] + [DataRow(@"/4aef74ba-388c-4292-9d53-98387e4f797b/reservation?LocationId=e5eed4c5-dfe9-45fd-a341-7408205d76ce&BeginDate=201909011300&Duration=2", true)] public void UrlHelper_IsLocalUrl(string url, bool exepectedResult) { var result = UrlHelper.IsLocalUrl(url); diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index 8c20389ecc..3b54cd7add 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -5,6 +5,7 @@ using DotVVM.Framework.Compilation.Parser.Binding.Tokenizer; using DotVVM.Framework.Utils; using System.Linq; +using System.Threading.Tasks; namespace DotVVM.Framework.Compilation.Binding { @@ -276,6 +277,11 @@ protected override Expression VisitBlock(BlockBindingParserNode node) var right = HandleErrors(node.SecondExpression, Visit) ?? Expression.Default(typeof(void)); ThrowOnErrors(); + if (typeof(Task).IsAssignableFrom(left.Type)) + { + return ExpressionHelper.RewriteTaskSequence(left, right); + } + if (right is BlockExpression rightBlock) { // flat the `(a; b; c; d; e; ...)` expression down @@ -283,6 +289,7 @@ protected override Expression VisitBlock(BlockBindingParserNode node) } else return Expression.Block(left, right); } + private Expression GetMemberOrTypeExpression(IdentifierNameBindingParserNode node, Type[] typeParameters) { diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs index a08448dc50..bac18584c9 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs @@ -5,8 +5,10 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Threading.Tasks; using DotVVM.Framework.Binding; using DotVVM.Framework.Controls; +using DotVVM.Framework.Runtime; using DotVVM.Framework.Utils; using Microsoft.CSharp.RuntimeBinder; @@ -327,13 +329,50 @@ public static Expression CompareMethod(Expression left, Expression right) throw new NotSupportedException("IComparable is not implemented on any of specified types"); } + public static Expression RewriteTaskSequence(Expression left, Expression right) + { + // if the left side is a task, make the right side also a task and join them + Expression rightTask; + if (right.Type == typeof(void)) + { + // return Task.CompletedTask + rightTask = Expression.Call(typeof(CommandTaskSequenceHelper), nameof(CommandTaskSequenceHelper.WrapAsTask), + Type.EmptyTypes, Expression.Lambda(right)); + } + else if (!typeof(Task).IsAssignableFrom(right.Type)) + { + // wrap the right expression into Task.FromResult + rightTask = Expression.Call(typeof(CommandTaskSequenceHelper), nameof(CommandTaskSequenceHelper.WrapAsTask), + new[] { right.Type }, Expression.Lambda(right)); + } + else + { + // right side is also a task + rightTask = right; + } + + // join the tasks using CommandTaskSequenceHelper + if (rightTask.Type.IsGenericType) + { + return Expression.Call(typeof(CommandTaskSequenceHelper), nameof(CommandTaskSequenceHelper.JoinTasks), new[] { rightTask.Type.GetGenericArguments()[0] }, left, Expression.Lambda(rightTask)); + } + else + { + return Expression.Call(typeof(CommandTaskSequenceHelper), nameof(CommandTaskSequenceHelper.JoinTasks), Type.EmptyTypes, left, Expression.Lambda(rightTask)); + } + } + + public static Expression UnwrapNullable(this Expression expression) => expression.Type.IsNullable() ? Expression.Property(expression, "Value") : expression; public static Expression GetBinaryOperator(Expression left, Expression right, ExpressionType operation) { if (operation == ExpressionType.Coalesce) return Expression.Coalesce(left, right); - if (operation == ExpressionType.Assign) return Expression.Assign(left, TypeConversion.ImplicitConversion(right, left.Type, true, true)); + if (operation == ExpressionType.Assign) + { + return Expression.Assign(left, TypeConversion.ImplicitConversion(right, left.Type, true, true)); + } // TODO: type conversions if (operation == ExpressionType.AndAlso) return Expression.AndAlso(left, right); diff --git a/src/DotVVM.Framework/Compilation/Binding/StaticCommandBindingCompiler.cs b/src/DotVVM.Framework/Compilation/Binding/StaticCommandBindingCompiler.cs index b9c9160789..6373115857 100644 --- a/src/DotVVM.Framework/Compilation/Binding/StaticCommandBindingCompiler.cs +++ b/src/DotVVM.Framework/Compilation/Binding/StaticCommandBindingCompiler.cs @@ -59,6 +59,10 @@ public JsExpression CompileToJavascript(DataContextStack dataContext, Expression var currentContextVariable = new JsTemporaryVariableParameter(knockoutContext); // var resultPromiseVariable = new JsNewExpression("DotvvmPromise")); var senderVariable = new JsTemporaryVariableParameter(new JsSymbolicParameter(CommandBindingExpression.SenderElementParameter)); + + var rewriter = new TaskSequenceRewriterExpressionVisitor(); + expression = rewriter.Visit(expression); + var visitor = new ExtractExpressionVisitor(ex => { if (ex.NodeType == ExpressionType.Call && ex is MethodCallExpression methodCall) { @@ -73,6 +77,7 @@ public JsExpression CompileToJavascript(DataContextStack dataContext, Expression return null; }); var rootCallback = visitor.Visit(expression); + var errorCallback = new JsIdentifierExpression("reject"); var js = SouldCompileCallback(rootCallback) ? new JsIdentifierExpression("resolve").Invoke(javascriptTranslator.CompileToJavascript(rootCallback, dataContext)) : null; foreach (var param in visitor.ParameterOrder.Reverse()) { @@ -80,7 +85,7 @@ public JsExpression CompileToJavascript(DataContextStack dataContext, Expression var replacedNode = js.DescendantNodes().SingleOrDefault(n => n is JsIdentifierExpression identifier && identifier.Identifier == param.Name); var callback = new JsFunctionExpression(new[] { new JsIdentifier(param.Name) }, new JsBlockStatement(new JsExpressionStatement(js))); var method = visitor.Replaced[param] as MethodCallExpression; - var methodInvocation = CompileMethodCall(method, dataContext, callback); + var methodInvocation = CompileMethodCall(method, dataContext, callback, errorCallback.Clone()); var invocationExpressions = methodInvocation is JsInvocationExpression invocation && invocation.Target.ToString() == "dotvvm.staticCommandPostback" ? @@ -149,7 +154,7 @@ public JsExpression CompileToJavascript(DataContextStack dataContext, Expression else { return new JsNewExpression(new JsIdentifierExpression("Promise"), new JsFunctionExpression( - new [] { new JsIdentifier("resolve") }, + new [] { new JsIdentifier("resolve"), new JsIdentifier("reject") }, new JsBlockStatement(new JsExpressionStatement(js)) )); } @@ -162,7 +167,7 @@ protected virtual bool SouldCompileCallback(Expression c) return true; } - protected virtual JsExpression CompileMethodCall(MethodCallExpression methodExpression, DataContextStack dataContext, JsExpression callbackFunction = null) + protected virtual JsExpression CompileMethodCall(MethodCallExpression methodExpression, DataContextStack dataContext, JsExpression callbackFunction, JsExpression errorCallback) { var jsTranslation = javascriptTranslator.TryTranslateMethodCall(methodExpression.Object, methodExpression.Arguments.ToArray(), methodExpression.Method, dataContext) ?.ApplyAction(javascriptTranslator.AdjustViewModelProperties); @@ -170,21 +175,20 @@ protected virtual JsExpression CompileMethodCall(MethodCallExpression methodExpr { if (!(jsTranslation.Annotation() is ResultIsPromiseAnnotation promiseAnnotation)) throw new Exception($"Expected javascript translation that returns a promise"); - var expr = promiseAnnotation.GetPromiseFromExpression?.Invoke(jsTranslation) ?? jsTranslation; - return expr.Member("then").Invoke(callbackFunction); + var resultPromise = promiseAnnotation.GetPromiseFromExpression?.Invoke(jsTranslation) ?? jsTranslation; + return resultPromise.Member("then").Invoke(callbackFunction, errorCallback); } if (!methodExpression.Method.IsDefined(typeof(AllowStaticCommandAttribute))) throw new Exception($"Method '{methodExpression.Method.DeclaringType.Name}.{methodExpression.Method.Name}' used in static command has to be marked with [AllowStaticCommand] attribute."); - if (callbackFunction == null) callbackFunction = new JsLiteral(null); if (methodExpression == null) throw new NotSupportedException("Static command binding must be a method call!"); var (plan, args) = CreateExecutionPlan(methodExpression, dataContext); var encryptedPlan = EncryptJson(SerializePlan(plan), protector).Apply(Convert.ToBase64String); return new JsIdentifierExpression("dotvvm").Member("staticCommandPostback") - .Invoke(new JsSymbolicParameter(CommandBindingExpression.ViewModelNameParameter), new JsSymbolicParameter(CommandBindingExpression.SenderElementParameter), new JsLiteral(encryptedPlan), new JsArrayExpression(args), callbackFunction) + .Invoke(new JsSymbolicParameter(CommandBindingExpression.ViewModelNameParameter), new JsSymbolicParameter(CommandBindingExpression.SenderElementParameter), new JsLiteral(encryptedPlan), new JsArrayExpression(args), callbackFunction, errorCallback) .WithAnnotation(new StaticCommandInvocationJsAnnotation(plan)); } diff --git a/src/DotVVM.Framework/Compilation/ControlTree/DataContextStack.cs b/src/DotVVM.Framework/Compilation/ControlTree/DataContextStack.cs index 0452836fd8..4672784b3c 100644 --- a/src/DotVVM.Framework/Compilation/ControlTree/DataContextStack.cs +++ b/src/DotVVM.Framework/Compilation/ControlTree/DataContextStack.cs @@ -11,6 +11,7 @@ namespace DotVVM.Framework.Compilation.ControlTree /// /// Represents compile-time DataContext info - Type of current DataContext, it's parent and other available parameters /// + [HandleAsImmutableObjectInDotvvmPropertyAttribute] public sealed class DataContextStack : IDataContextStack { public DataContextStack Parent { get; } diff --git a/src/DotVVM.Framework/Compilation/HandleAsImmutableObjectInDotvvmPropertyAttribute.cs b/src/DotVVM.Framework/Compilation/HandleAsImmutableObjectInDotvvmPropertyAttribute.cs new file mode 100644 index 0000000000..8097f4deb6 --- /dev/null +++ b/src/DotVVM.Framework/Compilation/HandleAsImmutableObjectInDotvvmPropertyAttribute.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DotVVM.Framework.Compilation +{ + /// + /// Tells the DotVVM view compiler that instance of a marked type may be used in DotvvmProperty. It is supposed to be immutable, as it will be shared accross all requests and controls with the same property setter. + /// + public class HandleAsImmutableObjectInDotvvmPropertyAttribute : Attribute + { + } +} diff --git a/src/DotVVM.Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs b/src/DotVVM.Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs index 7e69514dcb..96cd3ae997 100755 --- a/src/DotVVM.Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs +++ b/src/DotVVM.Framework/Compilation/Javascript/JavascriptTranslatableMethodCollection.cs @@ -25,19 +25,19 @@ public JavascriptTranslatableMethodCollection() AddDefaultMethodTranslators(); } - public void AddMethodTranslator(Type declaringType, string methodName, IJavascriptMethodTranslator translator, Type[] parameters = null, bool allowGeneric = true) + public void AddMethodTranslator(Type declaringType, string methodName, IJavascriptMethodTranslator translator, Type[] parameters = null, bool allowGeneric = true, bool allowMultipleMethods = false) { var methods = declaringType.GetMethods() .Where(m => m.Name == methodName && (allowGeneric || !m.IsGenericMethod)); if (parameters != null) { - methods = methods.Where(m => - { + methods = methods.Where(m => { var mp = m.GetParameters(); return mp.Length == parameters.Length && parameters.Zip(mp, (specified, method) => method.ParameterType.IsAssignableFrom(specified)).All(t => t); }); } - AddMethodTranslator(methods.Single(), translator); + + AddMethodsCore(methods.ToArray(), translator, allowMultipleMethods); } public void AddMethodTranslator(Type declaringType, string methodName, IJavascriptMethodTranslator translator, int parameterCount, bool allowMultipleMethods = false) @@ -46,8 +46,15 @@ public void AddMethodTranslator(Type declaringType, string methodName, IJavascri .Where(m => m.Name == methodName) .Where(m => m.GetParameters().Length == parameterCount) .ToArray(); - if (methods.Length > 1 && !allowMultipleMethods) throw new Exception("more then one methods"); - foreach (var method in methods) + + AddMethodsCore(methods, translator, allowMultipleMethods); + } + + private void AddMethodsCore(MethodInfo[] methodsList, IJavascriptMethodTranslator translator, bool allowMultipleMethods) + { + if (methodsList.Length > 1 && !allowMultipleMethods) throw new Exception("More then one method was found."); + if (methodsList.Length == 0) throw new Exception("No methods found."); + foreach (var method in methodsList) { AddMethodTranslator(method, translator); } diff --git a/src/DotVVM.Framework/Compilation/Javascript/TaskSequenceRewriterExpressionVisitor.cs b/src/DotVVM.Framework/Compilation/Javascript/TaskSequenceRewriterExpressionVisitor.cs new file mode 100644 index 0000000000..0dbc42c310 --- /dev/null +++ b/src/DotVVM.Framework/Compilation/Javascript/TaskSequenceRewriterExpressionVisitor.cs @@ -0,0 +1,34 @@ +using System.Linq.Expressions; +using DotVVM.Framework.Runtime; + +namespace DotVVM.Framework.Compilation.Javascript +{ + public class TaskSequenceRewriterExpressionVisitor : ExpressionVisitor + { + + protected override Expression VisitMethodCall(MethodCallExpression expression) + { + if (expression.Method.DeclaringType == typeof(CommandTaskSequenceHelper)) + { + // replace helper methods for joining tasks and handling async assignments + if (expression.Method.Name == nameof(CommandTaskSequenceHelper.JoinTasks)) + { + var first = expression.Arguments[0]; + var second = ((LambdaExpression)expression.Arguments[1]).Body; + if (second is MethodCallExpression secondMethod) + { + if (secondMethod.Method.DeclaringType == typeof(CommandTaskSequenceHelper) + && secondMethod.Method.Name == nameof(CommandTaskSequenceHelper.WrapAsTask)) + { + second = ((LambdaExpression)secondMethod.Arguments[0]).Body; + } + } + return VisitBlock(Expression.Block(first, second)); + } + } + + return base.VisitMethodCall(expression); + } + + } +} diff --git a/src/DotVVM.Framework/Compilation/RoslynValueEmitter.cs b/src/DotVVM.Framework/Compilation/RoslynValueEmitter.cs index 8dd1173ecf..63bd75ccea 100644 --- a/src/DotVVM.Framework/Compilation/RoslynValueEmitter.cs +++ b/src/DotVVM.Framework/Compilation/RoslynValueEmitter.cs @@ -230,7 +230,7 @@ private ExpressionSyntax EmitStrangeIntegerValue(long value, Type type) public static DotvvmProperty[][] _ViewImmutableObjects_PropArray = new DotvvmProperty[16][]; public static object[][] _ViewImmutableObjects_ObjArray = new object[16][]; public static object[] _ViewImmutableObjects = new object[16]; - private static Func IsImmutableObject = t => typeof(IBinding).IsAssignableFrom(t) || t == typeof(DataContextStack); + private static Func IsImmutableObject = t => typeof(IBinding).IsAssignableFrom(t) || t.GetCustomAttribute() is object; private static int _viewObjectsCount = 0; private static int _viewObjectsCount_PropArray = 0; private static int _viewObjectsCount_ObjArray = 0; diff --git a/src/DotVVM.Framework/Configuration/DotvvmConfiguration.cs b/src/DotVVM.Framework/Configuration/DotvvmConfiguration.cs index 6b608e6c91..399a4f6fdf 100644 --- a/src/DotVVM.Framework/Configuration/DotvvmConfiguration.cs +++ b/src/DotVVM.Framework/Configuration/DotvvmConfiguration.cs @@ -101,7 +101,7 @@ public class DotvvmConfiguration /// /// Gets or sets whether the application should run in debug mode. - /// For ASP.NET Core checkout https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments + /// For ASP.NET Core checkout https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments /// [JsonProperty("debug", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool Debug diff --git a/src/DotVVM.Framework/Controls/ConcurrencyQueueSetting.cs b/src/DotVVM.Framework/Controls/ConcurrencyQueueSetting.cs new file mode 100644 index 0000000000..8610d99c9c --- /dev/null +++ b/src/DotVVM.Framework/Controls/ConcurrencyQueueSetting.cs @@ -0,0 +1,32 @@ +using DotVVM.Framework.Binding; +namespace DotVVM.Framework.Controls +{ + public class ConcurrencyQueueSetting : DotvvmBindableObject + { + /// + /// Gets or sets the name of the event which the rule applies to. + /// + [MarkupOptions(AllowBinding = false)] + public string EventName + { + get { return (string)GetValue(EventNameProperty); } + set { SetValue(EventNameProperty, value); } + } + public static readonly DotvvmProperty EventNameProperty + = DotvvmProperty.Register(c => c.EventName, null); + + + /// + /// Gets or sets the name of the concurrency queue that will be used for the specified event. + /// + [MarkupOptions(AllowBinding = false)] + public string ConcurrencyQueue + { + get { return (string)GetValue(ConcurrencyQueueProperty); } + set { SetValue(ConcurrencyQueueProperty, value); } + } + public static readonly DotvvmProperty ConcurrencyQueueProperty + = DotvvmProperty.Register(c => c.ConcurrencyQueue, "default"); + + } +} diff --git a/src/DotVVM.Framework/Controls/ConcurrencyQueueSettingsCollection.cs b/src/DotVVM.Framework/Controls/ConcurrencyQueueSettingsCollection.cs new file mode 100644 index 0000000000..c99bf07286 --- /dev/null +++ b/src/DotVVM.Framework/Controls/ConcurrencyQueueSettingsCollection.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace DotVVM.Framework.Controls +{ + public class ConcurrencyQueueSettingsCollection : List + { + } +} diff --git a/src/DotVVM.Framework/Controls/DotvvmBindableObject.cs b/src/DotVVM.Framework/Controls/DotvvmBindableObject.cs index 3882b48f7a..92f1f34a44 100644 --- a/src/DotVVM.Framework/Controls/DotvvmBindableObject.cs +++ b/src/DotVVM.Framework/Controls/DotvvmBindableObject.cs @@ -81,7 +81,7 @@ public T GetValue(DotvvmProperty property, bool inherit = true) internal object EvalPropertyValue(DotvvmProperty property, object value) { if (property.IsBindingProperty) return value; - while (value is IBinding) + if (value is IBinding) { DotvvmBindableObject control = this; // DataContext is always bound to it's parent, setting it right here is a bit faster @@ -95,6 +95,10 @@ internal object EvalPropertyValue(DotvvmProperty property, object value) { value = command.GetCommandDelegate(control); } + else + { + throw new NotSupportedException($"Cannot evaluate binding {value} of type {value.GetType().Name}."); + } } return value; } diff --git a/src/DotVVM.Framework/Controls/HtmlGenericControl.cs b/src/DotVVM.Framework/Controls/HtmlGenericControl.cs index de16ae5db2..582adb5706 100644 --- a/src/DotVVM.Framework/Controls/HtmlGenericControl.cs +++ b/src/DotVVM.Framework/Controls/HtmlGenericControl.cs @@ -256,7 +256,7 @@ private void AddCssClassesToRender(IHtmlWriter writer) if (true.Equals(this.GetValue(cssClass))) writer.AddAttribute("class", cssClass.GroupMemberName, append: true, appendSeparator: " "); } - catch { } + catch when (HasValueBinding(cssClass)) { } } if (!cssClassBindingGroup.IsEmpty) writer.AddKnockoutDataBind("css", cssClassBindingGroup); @@ -281,7 +281,8 @@ private void AddCssStylesToRender(IHtmlWriter writer) writer.AddStyleAttribute(styleProperty.GroupMemberName, value); } } - catch { } + // suppress all errors when we have rendered the value binding anyway + catch when (HasValueBinding(styleProperty)) { } } if (cssStylesBindingGroup != null) diff --git a/src/DotVVM.Framework/Controls/HtmlLiteral.cs b/src/DotVVM.Framework/Controls/HtmlLiteral.cs index 083e31dd2d..87b35a3ef1 100644 --- a/src/DotVVM.Framework/Controls/HtmlLiteral.cs +++ b/src/DotVVM.Framework/Controls/HtmlLiteral.cs @@ -27,9 +27,8 @@ public string Html public static readonly DotvvmProperty HtmlProperty = DotvvmProperty.Register(t => t.Html, ""); - /// - /// Gets or sets whether the control should render a wrapper element. + /// Gets or sets the name of the tag that wraps the HtmlLiteral. /// [MarkupOptions(AllowBinding = false)] public string WrapperTagName @@ -40,10 +39,9 @@ public string WrapperTagName public static readonly DotvvmProperty WrapperTagNameProperty = DotvvmProperty.Register(c => c.WrapperTagName, "div"); - /// - /// Gets or sets the name of the tag that wraps the HtmlLiteral. - /// + /// Gets or sets whether the control should render a wrapper element. + /// [MarkupOptions(AllowBinding = false)] public bool RenderWrapperTag { diff --git a/src/DotVVM.Framework/Controls/Infrastructure/BodyResourceLinks.cs b/src/DotVVM.Framework/Controls/Infrastructure/BodyResourceLinks.cs index 443cb105b0..81b64eb126 100644 --- a/src/DotVVM.Framework/Controls/Infrastructure/BodyResourceLinks.cs +++ b/src/DotVVM.Framework/Controls/Infrastructure/BodyResourceLinks.cs @@ -21,11 +21,7 @@ protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext var resourceManager = context.ResourceManager; if (resourceManager.BodyRendered) return; resourceManager.BodyRendered = true; // set the flag before the resources are rendered, so they can't add more resources to the list during the render - foreach (var resource in resourceManager.GetNamedResourcesInOrder()) - { - if (resource.Resource.RenderPosition == ResourceRenderPosition.Body) - resource.RenderResourceCached(writer, context); - } + ResourcesRenderer.RenderResources(resourceManager, writer, context, ResourceRenderPosition.Body); // render the serialized viewmodel var serializedViewModel = ((DotvvmRequestContext) context).GetSerializedViewModel(); diff --git a/src/DotVVM.Framework/Controls/Infrastructure/HeadResourceLinks.cs b/src/DotVVM.Framework/Controls/Infrastructure/HeadResourceLinks.cs index 5bdc5ba107..299d9156e2 100644 --- a/src/DotVVM.Framework/Controls/Infrastructure/HeadResourceLinks.cs +++ b/src/DotVVM.Framework/Controls/Infrastructure/HeadResourceLinks.cs @@ -21,17 +21,7 @@ protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext resourceManager.HeadRendered = true; // render resource links and preloads - foreach (var resource in resourceManager.GetNamedResourcesInOrder()) - { - if (resource.Resource.RenderPosition == ResourceRenderPosition.Head) - { - resource.RenderResourceCached(writer, context); - } - else if (resource.Resource is IPreloadResource preloadResource) - { - preloadResource.RenderPreloadLink(writer, context, resource.Name); - } - } + ResourcesRenderer.RenderResources(resourceManager, writer, context, ResourceRenderPosition.Head); } } } diff --git a/src/DotVVM.Framework/Controls/KnockoutHelper.cs b/src/DotVVM.Framework/Controls/KnockoutHelper.cs index f3a1ebdc4c..d627391e79 100644 --- a/src/DotVVM.Framework/Controls/KnockoutHelper.cs +++ b/src/DotVVM.Framework/Controls/KnockoutHelper.cs @@ -136,7 +136,7 @@ string getHandlerScript() options.IsOnChange ? "\"suppressOnUpdating\"" : null, - GenerateConcurrencyModeHandler(control) + GenerateConcurrencyModeHandler(propertyName, control) ); } string generatedPostbackHandlers = null; @@ -251,14 +251,36 @@ private static JsExpression TransformOptionValueToExpression(DotvvmBindableObjec } } - static string GenerateConcurrencyModeHandler(DotvvmBindableObject obj) + static string GenerateConcurrencyModeHandler(string propertyName, DotvvmBindableObject obj) { var mode = (obj.GetValue(PostBack.ConcurrencyProperty) as PostbackConcurrencyMode?) ?? PostbackConcurrencyMode.Default; - var queueName = obj.GetValueRaw(PostBack.ConcurrencyQueueProperty) ?? "default"; - if (mode == PostbackConcurrencyMode.Default && "default".Equals(queueName)) return null; + + // determine concurrency queue + string queueName = null; + var queueSettings = obj.GetValueRaw(PostBack.ConcurrencyQueueSettingsProperty) as ConcurrencyQueueSettingsCollection; + if (queueSettings != null) + { + queueName = queueSettings.FirstOrDefault(q => string.Equals(q.EventName, propertyName, StringComparison.OrdinalIgnoreCase))?.ConcurrencyQueue; + } + if (queueName == null) + { + queueName = obj.GetValue(PostBack.ConcurrencyQueueProperty) as string ?? "default"; + } + + // return the handler script + if (mode == PostbackConcurrencyMode.Default && "default".Equals(queueName)) + { + return null; + } var handlerName = $"concurrency-{mode.ToString().ToLower()}"; - if ("default".Equals(queueName)) return JsonConvert.ToString(handlerName); - return $"[{JsonConvert.ToString(handlerName)},{GenerateHandlerOptions(obj, new Dictionary { ["q"] = queueName })}]"; + if ("default".Equals(queueName)) + { + return JsonConvert.ToString(handlerName); + } + else + { + return $"[{JsonConvert.ToString(handlerName)},{GenerateHandlerOptions(obj, new Dictionary { ["q"] = queueName })}]"; + } } public static IEnumerable GetContextPath(DotvvmBindableObject control) diff --git a/src/DotVVM.Framework/Controls/PostBack.cs b/src/DotVVM.Framework/Controls/PostBack.cs index b8476e7a40..15a3a007b3 100644 --- a/src/DotVVM.Framework/Controls/PostBack.cs +++ b/src/DotVVM.Framework/Controls/PostBack.cs @@ -21,12 +21,16 @@ public class PostBack public static readonly DotvvmProperty ConcurrencyProperty = DotvvmProperty.Register(() => ConcurrencyProperty, PostbackConcurrencyMode.Default, isValueInherited: true); - [MarkupOptions(AllowBinding = false)] [AttachedProperty(typeof(string))] public static readonly DotvvmProperty ConcurrencyQueueProperty = DotvvmProperty.Register(() => ConcurrencyQueueProperty, "default", isValueInherited: true); + [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] + [AttachedProperty(typeof(ConcurrencyQueueSettingsCollection))] + public static readonly DotvvmProperty ConcurrencyQueueSettingsProperty = + DotvvmProperty.Register(() => ConcurrencyQueueSettingsProperty, null); + } public enum PostbackConcurrencyMode diff --git a/src/DotVVM.Framework/Controls/PostBackHandlerCollection.cs b/src/DotVVM.Framework/Controls/PostBackHandlerCollection.cs index 32d08d744e..9c24912f65 100644 --- a/src/DotVVM.Framework/Controls/PostBackHandlerCollection.cs +++ b/src/DotVVM.Framework/Controls/PostBackHandlerCollection.cs @@ -5,4 +5,4 @@ namespace DotVVM.Framework.Controls public class PostBackHandlerCollection : List { } -} \ No newline at end of file +} diff --git a/src/DotVVM.Framework/Controls/UpdateProgress.cs b/src/DotVVM.Framework/Controls/UpdateProgress.cs index 23866c54dd..5994cd5663 100644 --- a/src/DotVVM.Framework/Controls/UpdateProgress.cs +++ b/src/DotVVM.Framework/Controls/UpdateProgress.cs @@ -6,6 +6,11 @@ using DotVVM.Framework.Binding; using DotVVM.Framework.Runtime; using DotVVM.Framework.Hosting; +using DotVVM.Framework.Compilation.Validation; +using DotVVM.Framework.Compilation.ControlTree.Resolved; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Utils; +using System.Text.RegularExpressions; namespace DotVVM.Framework.Controls { @@ -23,21 +28,38 @@ public int Delay get { return (int)GetValue(DelayProperty); } set { SetValue(DelayProperty, value); } } - public static readonly DotvvmProperty DelayProperty = DotvvmProperty.Register(t => t.Delay, 0); - public UpdateProgress() : base("div") + /// + /// Gets or sets the comma-separated names of PostBack.ConcurrencyQueue names for which this control should be enabled. + /// If not set, all queues are included automatically. + /// + [MarkupOptions(AllowBinding = false)] + public string[] IncludedQueues { + get { return (string[])GetValue(IncludedQueuesProperty); } + set { SetValue(IncludedQueuesProperty, value); } } + public static readonly DotvvmProperty IncludedQueuesProperty + = DotvvmProperty.Register(c => c.IncludedQueues, null); - protected internal override void OnInit(IDotvvmRequestContext context) + /// + /// Gets or sets the comma-separated names of PostBack.ConcurrencyQueue names that should be ignored by this control. + /// If you don't want to exclude any queue, use an empty string. + /// + [MarkupOptions(AllowBinding = false)] + public string[] ExcludedQueues + { + get { return (string[])GetValue(ExcludedQueuesProperty); } + set { SetValue(ExcludedQueuesProperty, value); } + } + public static readonly DotvvmProperty ExcludedQueuesProperty + = DotvvmProperty.Register(c => c.ExcludedQueues, null); + + + public UpdateProgress() : base("div") { - if (Delay<0) - { - throw new DotvvmControlException(this,"Delay cannot be set to negative number."); - } - base.OnInit(context); } protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context) @@ -48,8 +70,52 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest { writer.AddAttribute("data-delay", Delay.ToString()); } + if (IncludedQueues != null) + { + writer.AddAttribute("data-included-queues", string.Join(",", IncludedQueues)); + } + if (ExcludedQueues != null) + { + writer.AddAttribute("data-excluded-queues", string.Join(",", ExcludedQueues)); + } base.AddAttributesToRender(writer, context); } + + + + [ControlUsageValidator] + public static IEnumerable ValidateUsage(ResolvedControl control) + { + var delayProperty = control.GetValue(DelayProperty) as ResolvedPropertyValue; + if (delayProperty != null) + { + if ((int)delayProperty.Value < 0) + { + yield return new ControlUsageError("Delay cannot be set to negative number."); + } + } + + // validate queue settings + var includedQueues = (control.GetValue(IncludedQueuesProperty) as ResolvedPropertyValue)?.Value as string[]; + var excludedQueues = (control.GetValue(ExcludedQueuesProperty) as ResolvedPropertyValue)?.Value as string[]; + if (includedQueues != null && excludedQueues != null) + { + yield return new ControlUsageError("The IncludedQueues and ExcludedQueues cannot be used together!"); + } + if (includedQueues != null && !ValidateQueueNames(includedQueues)) + { + yield return new ControlUsageError("The IncludedQueues must contain comma-separated list of queue names (which can contain alphanumeric characters, underscore or dash)!"); + } + if (excludedQueues != null && !ValidateQueueNames(excludedQueues)) + { + yield return new ControlUsageError("The ExcludedQueues must contain comma-separated list of queue names (which can contain alphanumeric characters, underscore or dash)!"); + } + } + + private static bool ValidateQueueNames(string[] queues) + { + return queues.All(NamingUtils.IsValidConcurrencyQueueName); + } } } diff --git a/src/DotVVM.Framework/Hosting/DotvvmPresenter.cs b/src/DotVVM.Framework/Hosting/DotvvmPresenter.cs index fc316c1d17..71cc244108 100644 --- a/src/DotVVM.Framework/Hosting/DotvvmPresenter.cs +++ b/src/DotVVM.Framework/Hosting/DotvvmPresenter.cs @@ -78,6 +78,13 @@ public async Task ProcessRequest(IDotvvmRequestContext context) { context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } + catch (CorruptedCsrfTokenException ex) + { + // TODO this should be done by IOutputRender or something like that. IOutputRenderer does not support that, so should we make another IJsonErrorOutputWriter? + context.HttpContext.Response.StatusCode = 400; + context.HttpContext.Response.ContentType = "application/json; charset=utf-8"; + await context.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(new { action = "invalidCsrfToken", message = ex.Message })); + } catch (DotvvmControlException ex) { if (ex.FileName != null) @@ -239,7 +246,7 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context) await requestTracer.TraceEvent(RequestTracingConstants.PreRenderCompleted, context); // generate CSRF token if required - if (string.IsNullOrEmpty(context.CsrfToken)) + if (string.IsNullOrEmpty(context.CsrfToken) && !context.Configuration.ExperimentalFeatures.LazyCsrfToken.IsEnabledForRoute(context.Route.RouteName)) { context.CsrfToken = CsrfProtector.GenerateToken(context); } @@ -271,6 +278,7 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context) foreach (var f in requestFilters) await f.OnPageRenderedAsync(context); } + catch (CorruptedCsrfTokenException e) { throw; } catch (DotvvmInterruptRequestExecutionException) { throw; } catch (DotvvmHttpException) { throw; } catch (Exception ex) diff --git a/src/DotVVM.Framework/Hosting/HostingConstants.cs b/src/DotVVM.Framework/Hosting/HostingConstants.cs index bf363b440a..6cad221a71 100644 --- a/src/DotVVM.Framework/Hosting/HostingConstants.cs +++ b/src/DotVVM.Framework/Hosting/HostingConstants.cs @@ -11,6 +11,7 @@ public class HostingConstants public const string ResourceHandlerMatchUrl = "dotvvmEmbeddedResource"; public const string FileUploadHandlerMatchUrl = "dotvvmFileUpload"; + public const string CsrfTokenMatchUrl = "___dotvvm-create-csrf-token___"; public const string SpaContentPlaceHolderHeaderName = "X-DotVVM-SpaContentPlaceHolder"; public const string SpaPostBackHeaderName = "X-DotVVM-PostBack"; diff --git a/src/DotVVM.Framework/Hosting/Middlewares/DotvvmCsrfTokenMiddleware.cs b/src/DotVVM.Framework/Hosting/Middlewares/DotvvmCsrfTokenMiddleware.cs new file mode 100644 index 0000000000..5870b9ab36 --- /dev/null +++ b/src/DotVVM.Framework/Hosting/Middlewares/DotvvmCsrfTokenMiddleware.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DotVVM.Framework.Security; + +namespace DotVVM.Framework.Hosting.Middlewares +{ + public class DotvvmCsrfTokenMiddleware : IMiddleware + { + private readonly ICsrfProtector csrfProtector; + public DotvvmCsrfTokenMiddleware(ICsrfProtector csrfProtector) + { + this.csrfProtector = csrfProtector; + } + public async Task Handle(IDotvvmRequestContext cx) + { + if (DotvvmMiddlewareBase.GetCleanRequestUrl(cx.HttpContext) == HostingConstants.CsrfTokenMatchUrl) + { + var token = csrfProtector.GenerateToken(cx); + await cx.HttpContext.Response.WriteAsync(token); + return true; + } + return false; + } + } +} diff --git a/src/DotVVM.Framework/ResourceManagement/DotvvmResourceRepository.cs b/src/DotVVM.Framework/ResourceManagement/DotvvmResourceRepository.cs index 68d3adc701..39cfec4d7a 100644 --- a/src/DotVVM.Framework/ResourceManagement/DotvvmResourceRepository.cs +++ b/src/DotVVM.Framework/ResourceManagement/DotvvmResourceRepository.cs @@ -9,6 +9,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using DotVVM.Framework.Routing; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.ResourceManagement { @@ -103,7 +104,7 @@ public void RegisterNamedParent(string name, IDotvvmResourceRepository parent) protected virtual void ValidateResourceName(string name) { - if (!Regex.IsMatch(name, @"^[a-zA-Z0-9]+([._-][a-zA-Z0-9]+)*$")) + if (!NamingUtils.IsValidResourceName(name)) { throw new ArgumentException($"The resource name {name} is not valid! Only alphanumeric characters, dots, underscores and dashes are allowed! Also please note that two or more subsequent dots, underscores and dashes are reserved for internal use, and are allowed only in the middle of the resource name."); } diff --git a/src/DotVVM.Framework/ResourceManagement/ResourceUtils.cs b/src/DotVVM.Framework/ResourceManagement/ResourceUtils.cs index 844776cf85..8a2dd3fee8 100644 --- a/src/DotVVM.Framework/ResourceManagement/ResourceUtils.cs +++ b/src/DotVVM.Framework/ResourceManagement/ResourceUtils.cs @@ -31,6 +31,10 @@ public static void AssertAcyclicDependencies(IResource resource, { var currentName = queue.Dequeue(); var current = findResource(currentName); + + if (current is null) + continue; + if (visited.Contains(current)) { // dependency cycle detected @@ -38,12 +42,9 @@ public static void AssertAcyclicDependencies(IResource resource, $"dependency."); } visited.Add(current); - if (current != null) + foreach (var dependency in current.Dependencies) { - foreach (var dependency in current.Dependencies) - { - queue.Enqueue(dependency); - } + queue.Enqueue(dependency); } } } diff --git a/src/DotVVM.Framework/ResourceManagement/ResourcesRenderer.cs b/src/DotVVM.Framework/ResourceManagement/ResourcesRenderer.cs index d58881ff42..c07af773a2 100644 --- a/src/DotVVM.Framework/ResourceManagement/ResourcesRenderer.cs +++ b/src/DotVVM.Framework/ResourceManagement/ResourcesRenderer.cs @@ -17,6 +17,39 @@ public static void RenderResourceCached(this NamedResource resource, IHtmlWriter writer.WriteUnencodedText(resource.GetRenderedTextCached(context)); } + static void WriteResourceInfo(NamedResource resource, IHtmlWriter writer, bool preload) + { + var comment = $"Resource {resource.Name} of type {resource.Resource.GetType().Name}."; + if (resource.Resource is ILinkResource linkResource) + comment += $" Pointing to {string.Join(", ", linkResource.GetLocations().Select(l => l.GetType().Name))}."; + + if (preload) comment = "[preload link] " + comment; + + writer.WriteUnencodedText("\n \n "); + // ^~~~ most likely this info will be written directly in the or , so it should be indented by one level. + // we don't have any better way to know how we should indent + } + + public static void RenderResources(ResourceManager resourceManager, IHtmlWriter writer, IDotvvmRequestContext context, ResourceRenderPosition position) + { + var writeDebugInfo = context.Configuration.Debug; + foreach (var resource in resourceManager.GetNamedResourcesInOrder()) + { + if (resource.Resource.RenderPosition == position) + { + if (writeDebugInfo) WriteResourceInfo(resource, writer, preload: false); + resource.RenderResourceCached(writer, context); + } + else if (position == ResourceRenderPosition.Head && resource.Resource.RenderPosition != ResourceRenderPosition.Head && resource.Resource is IPreloadResource preloadResource) + { + if (writeDebugInfo) WriteResourceInfo(resource, writer, preload: true); + preloadResource.RenderPreloadLink(writer, context, resource.Name); + } + } + } + public static string GetRenderedTextCached(this NamedResource resource, IDotvvmRequestContext context) => // don't use cache when debug, so the resource can be refreshed when file is changed context.Configuration.Debug ? diff --git a/src/DotVVM.Framework/Resources/Scripts/DotVVM.d.ts b/src/DotVVM.Framework/Resources/Scripts/DotVVM.d.ts index a68504447a..3362b790ed 100644 --- a/src/DotVVM.Framework/Resources/Scripts/DotVVM.d.ts +++ b/src/DotVVM.Framework/Resources/Scripts/DotVVM.d.ts @@ -303,6 +303,7 @@ declare class DotVVM { extensions: IDotvvmExtensions; useHistoryApiSpaNavigation: boolean; isPostbackRunning: KnockoutObservable; + updateProgressChangeCounter: KnockoutObservable; init(viewModelName: string, culture: string): void; private handlePopState; private handleHashChangeWithHistory; @@ -310,7 +311,11 @@ declare class DotVVM { private persistViewModel; private backUpPostBackConter; private isPostBackStillActive; - staticCommandPostback(viewModelName: string, sender: HTMLElement, command: string, args: any[], callback?: (_: any) => void, errorCallback?: (xhr: XMLHttpRequest, error?: any) => void): void; + private fetchCsrfToken; + staticCommandPostback(viewModelName: string, sender: HTMLElement, command: string, args: any[], callback?: (_: any) => void, errorCallback?: (errorInfo: { + xhr: XMLHttpRequest; + error?: any; + }) => void): void; private processPassedId; protected getPostbackHandler(name: string): (options: any) => DotvvmPostbackHandler; private isPostbackHandler; diff --git a/src/DotVVM.Framework/Resources/Scripts/DotVVM.js b/src/DotVVM.Framework/Resources/Scripts/DotVVM.js index 14db0720eb..7bf24f3aa9 100644 --- a/src/DotVVM.Framework/Resources/Scripts/DotVVM.js +++ b/src/DotVVM.Framework/Resources/Scripts/DotVVM.js @@ -9,6 +9,41 @@ var __assign = (this && this.__assign) || function () { }; return __assign.apply(this, arguments); }; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || @@ -923,8 +958,10 @@ var DotVVM = /** @class */ (function () { this.commonConcurrencyHandler = function (promise, options, queueName) { var queue = _this.getPostbackQueue(queueName); queue.noRunning++; + dotvvm.updateProgressChangeCounter(dotvvm.updateProgressChangeCounter() + 1); var dispatchNext = function () { queue.noRunning--; + dotvvm.updateProgressChangeCounter(dotvvm.updateProgressChangeCounter() - 1); if (queue.queue.length > 0) { var callback = queue.queue.shift(); window.setTimeout(callback, 0); @@ -970,6 +1007,7 @@ var DotVVM = /** @class */ (function () { this.fileUpload = new DotvvmFileUpload(); this.extensions = {}; this.isPostbackRunning = ko.observable(false); + this.updateProgressChangeCounter = ko.observable(0); } DotVVM.prototype.createWindowSetTimeoutHandler = function (time) { return { @@ -1117,38 +1155,86 @@ var DotVVM = /** @class */ (function () { DotVVM.prototype.isPostBackStillActive = function (currentPostBackCounter) { return this.postBackCounter === currentPostBackCounter; }; + DotVVM.prototype.fetchCsrfToken = function (viewModelName) { + return __awaiter(this, void 0, void 0, function () { + var vm, response, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + vm = this.viewModels[viewModelName].viewModel; + if (!(vm.$csrfToken == null)) return [3 /*break*/, 3]; + return [4 /*yield*/, fetch((this.viewModels[viewModelName].virtualDirectory || "") + "/___dotvvm-create-csrf-token___")]; + case 1: + response = _b.sent(); + if (response.status != 200) + throw new Error("Can't fetch CSRF token: " + response.statusText); + _a = vm; + return [4 /*yield*/, response.text()]; + case 2: + _a.$csrfToken = _b.sent(); + _b.label = 3; + case 3: return [2 /*return*/, vm.$csrfToken]; + } + }); + }); + }; DotVVM.prototype.staticCommandPostback = function (viewModelName, sender, command, args, callback, errorCallback) { var _this = this; if (callback === void 0) { callback = function (_) { }; } - if (errorCallback === void 0) { errorCallback = function (xhr, error) { }; } - var data = this.serialization.serialize({ - "args": args, - "command": command, - "$csrfToken": this.viewModels[viewModelName].viewModel.$csrfToken - }); - dotvvm.events.staticCommandMethodInvoking.trigger(data); - this.postJSON(this.viewModels[viewModelName].url, "POST", ko.toJSON(data), function (response) { - try { - _this.isViewModelUpdating = true; - var result = JSON.parse(response.responseText); - dotvvm.events.staticCommandMethodInvoked.trigger(__assign({}, data, { result: result, xhr: response })); - callback(result); - } - catch (error) { - dotvvm.events.staticCommandMethodFailed.trigger(__assign({}, data, { xhr: response, error: error })); - errorCallback(response, error); - } - finally { - _this.isViewModelUpdating = false; - } - }, function (xhr) { - _this.events.error.trigger(new DotvvmErrorEventArgs(sender, _this.viewModels[viewModelName].viewModel, viewModelName, xhr, null)); - console.warn("StaticCommand postback failed: " + xhr.status + " - " + xhr.statusText, xhr); - errorCallback(xhr); - dotvvm.events.staticCommandMethodFailed.trigger(__assign({}, data, { xhr: xhr })); - }, function (xhr) { - xhr.setRequestHeader("X-PostbackType", "StaticCommand"); - }); + if (errorCallback === void 0) { errorCallback = function (errorInfo) { }; } + (function () { return __awaiter(_this, void 0, void 0, function () { + var data, _a, _b, _c, _d; + var _this = this; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + _b = (_a = this.serialization).serialize; + _c = { + args: args, + command: command + }; + _d = "$csrfToken"; + return [4 /*yield*/, this.fetchCsrfToken(viewModelName)]; + case 1: + data = _b.apply(_a, [(_c[_d] = _e.sent(), + _c)]); + dotvvm.events.staticCommandMethodInvoking.trigger(data); + this.postJSON(this.viewModels[viewModelName].url, "POST", ko.toJSON(data), function (response) { + try { + _this.isViewModelUpdating = true; + var result = JSON.parse(response.responseText); + dotvvm.events.staticCommandMethodInvoked.trigger(__assign({}, data, { result: result, xhr: response })); + callback(result); + } + catch (error) { + dotvvm.events.staticCommandMethodFailed.trigger(__assign({}, data, { xhr: response, error: error })); + errorCallback({ xhr: response, error: error }); + } + finally { + _this.isViewModelUpdating = false; + } + }, function (xhr) { + if (/^application\/json(;|$)/.test(xhr.getResponseHeader("Content-Type"))) { + var errObject = JSON.parse(xhr.responseText); + if (errObject.action === "invalidCsrfToken") { + // ok, renew the token and try again. Do that before any event is triggered + _this.viewModels[viewModelName].viewModel.$csrfToken = null; + console.log("Resending postback due to invalid CSRF token."); // this may loop indefinitely (in some extreme case), we don't currently have any loop detection mechanism, so at least we can log it. + _this.staticCommandPostback(viewModelName, sender, command, args, callback, errorCallback); + return; + } + } + _this.events.error.trigger(new DotvvmErrorEventArgs(sender, _this.viewModels[viewModelName].viewModel, viewModelName, xhr, null)); + console.warn("StaticCommand postback failed: " + xhr.status + " - " + xhr.statusText, xhr); + errorCallback({ xhr: xhr }); + dotvvm.events.staticCommandMethodFailed.trigger(__assign({}, data, { xhr: xhr })); + }, function (xhr) { + xhr.setRequestHeader("X-PostbackType", "StaticCommand"); + }); + return [2 /*return*/]; + } + }); + }); })(); }; DotVVM.prototype.processPassedId = function (id, context) { if (typeof id == "string" || id == null) @@ -1257,86 +1343,107 @@ var DotVVM = /** @class */ (function () { }; DotVVM.prototype.postbackCore = function (options, path, command, controlUniqueId, context, commandArgs) { var _this = this; - return new Promise(function (resolve, reject) { - var viewModelName = options.viewModelName; - var viewModel = _this.viewModels[viewModelName].viewModel; - _this.lastStartedPostack = options.postbackId; - // perform the postback - _this.updateDynamicPathFragments(context, path); - var data = { - viewModel: _this.serialization.serialize(viewModel, { pathMatcher: function (val) { return context && val == context.$data; } }), - currentPath: path, - command: command, - controlUniqueId: _this.processPassedId(controlUniqueId, context), - additionalData: options.additionalPostbackData, - renderedResources: _this.viewModels[viewModelName].renderedResources, - commandArgs: commandArgs - }; - _this.postJSON(_this.viewModels[viewModelName].url, "POST", ko.toJSON(data), function (result) { - dotvvm.events.postbackResponseReceived.trigger({}); - resolve(function () { return new Promise(function (resolve, reject) { - dotvvm.events.postbackCommitInvoked.trigger({}); - var locationHeader = result.getResponseHeader("Location"); - var resultObject = locationHeader != null && locationHeader.length > 0 ? - { action: "redirect", url: locationHeader } : - JSON.parse(result.responseText); - if (!resultObject.viewModel && resultObject.viewModelDiff) { - // TODO: patch (~deserialize) it to ko.observable viewModel - resultObject.viewModel = _this.patch(data.viewModel, resultObject.viewModelDiff); - } - _this.loadResourceList(resultObject.resources, function () { - var isSuccess = false; - if (resultObject.action === "successfulCommand") { - try { - _this.isViewModelUpdating = true; - // remove updated controls - var updatedControls = _this.cleanUpdatedControls(resultObject); - // update the viewmodel - if (resultObject.viewModel) { - ko.delaySync.pause(); - _this.serialization.deserialize(resultObject.viewModel, _this.viewModels[viewModelName].viewModel); - ko.delaySync.resume(); + return new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () { + var viewModelName, viewModel, data; + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + viewModelName = options.viewModelName; + return [4 /*yield*/, this.fetchCsrfToken(viewModelName)]; + case 1: + _a.sent(); + viewModel = this.viewModels[viewModelName].viewModel; + this.lastStartedPostack = options.postbackId; + // perform the postback + this.updateDynamicPathFragments(context, path); + data = { + viewModel: this.serialization.serialize(viewModel, { pathMatcher: function (val) { return context && val == context.$data; } }), + currentPath: path, + command: command, + controlUniqueId: this.processPassedId(controlUniqueId, context), + additionalData: options.additionalPostbackData, + renderedResources: this.viewModels[viewModelName].renderedResources, + commandArgs: commandArgs + }; + this.postJSON(this.viewModels[viewModelName].url, "POST", ko.toJSON(data), function (result) { + dotvvm.events.postbackResponseReceived.trigger({}); + resolve(function () { return new Promise(function (resolve, reject) { + dotvvm.events.postbackCommitInvoked.trigger({}); + var locationHeader = result.getResponseHeader("Location"); + var resultObject = locationHeader != null && locationHeader.length > 0 ? + { action: "redirect", url: locationHeader } : + JSON.parse(result.responseText); + if (!resultObject.viewModel && resultObject.viewModelDiff) { + // TODO: patch (~deserialize) it to ko.observable viewModel + resultObject.viewModel = _this.patch(data.viewModel, resultObject.viewModelDiff); + } + _this.loadResourceList(resultObject.resources, function () { + var isSuccess = false; + if (resultObject.action === "successfulCommand") { + try { + _this.isViewModelUpdating = true; + // remove updated controls + var updatedControls = _this.cleanUpdatedControls(resultObject); + // update the viewmodel + if (resultObject.viewModel) { + ko.delaySync.pause(); + _this.serialization.deserialize(resultObject.viewModel, _this.viewModels[viewModelName].viewModel); + ko.delaySync.resume(); + } + isSuccess = true; + // remove updated controls which were previously hidden + _this.cleanUpdatedControls(resultObject, updatedControls); + // add updated controls + _this.restoreUpdatedControls(resultObject, updatedControls, true); + } + finally { + _this.isViewModelUpdating = false; + } + dotvvm.events.postbackViewModelUpdated.trigger({}); + } + else if (resultObject.action === "redirect") { + // redirect + _this.handleRedirect(resultObject, viewModelName); + return resolve(); + } + var idFragment = resultObject.resultIdFragment; + if (idFragment) { + if (_this.getSpaPlaceHolder() || location.hash == "#" + idFragment) { + var element = document.getElementById(idFragment); + if (element && "function" == typeof element.scrollIntoView) + element.scrollIntoView(true); + } + else + location.hash = idFragment; + } + // trigger afterPostback event + if (!isSuccess) { + reject(new DotvvmErrorEventArgs(options.sender, viewModel, viewModelName, result, options.postbackId, resultObject)); + } + else { + var afterPostBackArgs = new DotvvmAfterPostBackEventArgs(options, resultObject, resultObject.commandResult, result); + resolve(afterPostBackArgs); + } + }); + }); }); + }, function (xhr) { + if (/^application\/json(;|$)/.test(xhr.getResponseHeader("Content-Type"))) { + var errObject = JSON.parse(xhr.responseText); + if (errObject.action === "invalidCsrfToken") { + // ok, renew the token and try again. Do that before any event is triggered + _this.viewModels[viewModelName].viewModel.$csrfToken = null; + console.log("Resending postback due to invalid CSRF token."); // this may loop indefinitely (in some extreme case), we don't currently have any loop detection mechanism, so at least we can log it. + _this.postbackCore(options, path, command, controlUniqueId, context, commandArgs).then(resolve, reject); + return; } - isSuccess = true; - // remove updated controls which were previously hidden - _this.cleanUpdatedControls(resultObject, updatedControls); - // add updated controls - _this.restoreUpdatedControls(resultObject, updatedControls, true); - } - finally { - _this.isViewModelUpdating = false; - } - dotvvm.events.postbackViewModelUpdated.trigger({}); - } - else if (resultObject.action === "redirect") { - // redirect - _this.handleRedirect(resultObject, viewModelName); - return resolve(); - } - var idFragment = resultObject.resultIdFragment; - if (idFragment) { - if (_this.getSpaPlaceHolder() || location.hash == "#" + idFragment) { - var element = document.getElementById(idFragment); - if (element && "function" == typeof element.scrollIntoView) - element.scrollIntoView(true); } - else - location.hash = idFragment; - } - // trigger afterPostback event - if (!isSuccess) { - reject(new DotvvmErrorEventArgs(options.sender, viewModel, viewModelName, result, options.postbackId, resultObject)); - } - else { - var afterPostBackArgs = new DotvvmAfterPostBackEventArgs(options, resultObject, resultObject.commandResult, result); - resolve(afterPostBackArgs); - } - }); - }); }); - }, function (xhr) { - reject({ type: 'network', options: options, args: new DotvvmErrorEventArgs(options.sender, viewModel, viewModelName, xhr, options.postbackId) }); + reject({ type: 'network', options: options, args: new DotvvmErrorEventArgs(options.sender, viewModel, viewModelName, xhr, options.postbackId) }); + }); + return [2 /*return*/]; + } }); - }); + }); }); }; DotVVM.prototype.handleSpaNavigation = function (element) { var target = element.getAttribute('target'); @@ -1869,47 +1976,61 @@ var DotVVM = /** @class */ (function () { return { controlsDescendantBindings: true }; // do not apply binding again } }; + var makeUpdatableChildrenContextHandler = function (makeContextCallback, shouldDisplay) { return function (element, valueAccessor, _allBindings, _viewModel, bindingContext) { + if (!bindingContext) + throw new Error(); + var savedNodes; + ko.computed(function () { + var rawValue = valueAccessor(); + // Save a copy of the inner nodes on the initial update, but only if we have dependencies. + if (!savedNodes && ko.computedContext.getDependenciesCount()) { + savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */); + } + if (shouldDisplay(rawValue)) { + if (savedNodes) { + ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes)); + } + ko.applyBindingsToDescendants(makeContextCallback(bindingContext, rawValue), element); + } + else { + ko.virtualElements.emptyNode(element); + } + }, null, { disposeWhenNodeIsRemoved: element }); + return { controlsDescendantBindings: true }; // do not apply binding again + }; }; var foreachCollectionSymbol = "$foreachCollectionSymbol"; ko.virtualElements.allowedBindings["dotvvm-SSR-foreach"] = true; ko.bindingHandlers["dotvvm-SSR-foreach"] = { - init: function (element, valueAccessor, _allBindings, _viewModel, bindingContext) { + init: makeUpdatableChildrenContextHandler(function (bindingContext, rawValue) { var _a; - if (!bindingContext) - throw new Error(); - var value = valueAccessor(); - var innerBindingContext = bindingContext.extend((_a = {}, _a[foreachCollectionSymbol] = value.data, _a)); - element.innerBindingContext = innerBindingContext; - ko.applyBindingsToDescendants(innerBindingContext, element); - return { controlsDescendantBindings: true }; // do not apply binding again - } + return bindingContext.extend((_a = {}, _a[foreachCollectionSymbol] = rawValue.data, _a)); + }, function (v) { return v.data != null; }) }; ko.virtualElements.allowedBindings["dotvvm-SSR-item"] = true; ko.bindingHandlers["dotvvm-SSR-item"] = { init: function (element, valueAccessor, _allBindings, _viewModel, bindingContext) { if (!bindingContext) throw new Error(); - var index = valueAccessor(); var collection = bindingContext[foreachCollectionSymbol]; - var innerBindingContext = bindingContext.createChildContext(function () { return ko.unwrap((ko.unwrap(collection) || [])[index]); }).extend({ $index: ko.pureComputed(function () { return index; }) }); + var innerBindingContext = bindingContext.createChildContext(function () { + return ko.unwrap((ko.unwrap(collection) || [])[valueAccessor()]); + }).extend({ $index: ko.pureComputed(valueAccessor) }); element.innerBindingContext = innerBindingContext; ko.applyBindingsToDescendants(innerBindingContext, element); return { controlsDescendantBindings: true }; // do not apply binding again + }, + update: function (element) { + if (element.seenUpdate) + console.error("dotvvm-SSR-item binding did not expect to see a update"); + element.seenUpdate = 1; } }; ko.virtualElements.allowedBindings["withGridViewDataSet"] = true; ko.bindingHandlers["withGridViewDataSet"] = { - init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + init: makeUpdatableChildrenContextHandler(function (bindingContext, value) { var _a; - if (!bindingContext) - throw new Error(); - var value = valueAccessor(); - var innerBindingContext = bindingContext.extend((_a = { $gridViewDataSet: value }, _a[foreachCollectionSymbol] = dotvvm.evaluator.getDataSourceItems(value), _a)); - element.innerBindingContext = innerBindingContext; - ko.applyBindingsToDescendants(innerBindingContext, element); - return { controlsDescendantBindings: true }; // do not apply binding again - }, - update: function (element, valueAccessor, allBindings, viewModel, bindingContext) { - } + return bindingContext.extend((_a = { $gridViewDataSet: value }, _a[foreachCollectionSymbol] = dotvvm.evaluator.getDataSourceItems(value), _a)); + }, function (_) { return true; }) }; ko.bindingHandlers['dotvvmEnable'] = { 'update': function (element, valueAccessor) { @@ -1950,6 +2071,8 @@ var DotVVM = /** @class */ (function () { init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { element.style.display = "none"; var delay = element.getAttribute("data-delay"); + var includedQueues = (element.getAttribute("data-included-queues") || "").split(",").filter(function (i) { return i.length > 0; }); + var excludedQueues = (element.getAttribute("data-excluded-queues") || "").split(",").filter(function (i) { return i.length > 0; }); var timeout; var running = false; var show = function () { @@ -1968,8 +2091,20 @@ var DotVVM = /** @class */ (function () { clearTimeout(timeout); element.style.display = "none"; }; - dotvvm.isPostbackRunning.subscribe(function (e) { - if (e) { + dotvvm.updateProgressChangeCounter.subscribe(function (e) { + var shouldRun = false; + if (includedQueues.length === 0) { + for (var queue in dotvvm.postbackQueues) { + if (excludedQueues.indexOf(queue) < 0 && dotvvm.postbackQueues[queue].noRunning > 0) { + shouldRun = true; + break; + } + } + } + else { + shouldRun = includedQueues.some(function (q) { return dotvvm.postbackQueues[q] && dotvvm.postbackQueues[q].noRunning > 0; }); + } + if (shouldRun) { if (!running) { show(); } diff --git a/src/DotVVM.Framework/Resources/Scripts/DotVVM.min.js b/src/DotVVM.Framework/Resources/Scripts/DotVVM.min.js index 1797a32457..66369e0e33 100644 --- a/src/DotVVM.Framework/Resources/Scripts/DotVVM.min.js +++ b/src/DotVVM.Framework/Resources/Scripts/DotVVM.min.js @@ -1 +1 @@ -var __assign=this&&this.__assign||function(){return(__assign=Object.assign||function(e){for(var t,r=1,n=arguments.length;r'");return e},e.prototype.format=function(e){for(var o=this,a=[],t=1;t=e.length)r();else{var o=e[t],a=!1;if("script"==o.tagName.toLowerCase()){var i=document.createElement("script");o.src&&(i.src=o.src,a=!0),o.type&&(i.type=o.type),o.text&&(i.text=o.text),o.id&&(i.id=o.id),o=i}else if("link"==o.tagName.toLowerCase()){var s=document.createElement("link");o.href&&(s.href=o.href),o.rel&&(s.rel=o.rel),o.type&&(s.type=o.type),o=s}a&&(o.onload=function(){return n.loadResourceElements(e,t+1,r)}),document.head.appendChild(o),a||this.loadResourceElements(e,t+1,r)}},e.prototype.getSpaPlaceHolder=function(){var e=document.getElementsByName("__dot_SpaContentPlaceHolder");return 1==e.length?e[0]:null},e.prototype.navigateCore=function(i,e,t){var s=this,r=this.viewModels[i].viewModel,n=this.backUpPostBackConter(),o=new DotvvmSpaNavigatingEventArgs(r,i,e);if(this.events.spaNavigating.trigger(o),!o.cancel){var a=this.viewModels[i].virtualDirectory||"",l="/___dotvvm-spa___"+this.addLeadingSlash(e),d=this.addLeadingSlash(this.concatUrl(a,l)),u=this.getSpaPlaceHolder();if(u){t&&t(this.addLeadingSlash(this.concatUrl(a,this.addLeadingSlash(e))));var v=u.attributes["data-dotvvm-spacontentplaceholder"].value;this.getJSON(d,"GET",v,function(o){if(s.isPostBackStillActive(n)){var a=JSON.parse(o.responseText);s.loadResourceList(a.resources,function(){var e=!1;if("successfulCommand"!==a.action&&a.action){if("redirect"===a.action)return void s.handleRedirect(a,i,!0)}else try{s.isViewModelUpdating=!0;var t=s.cleanUpdatedControls(a);for(var r in s.viewModels[i]={},a)a.hasOwnProperty(r)&&(s.viewModels[i][r]=a[r]);ko.delaySync.pause(),s.serialization.deserialize(a.viewModel,s.viewModels[i].viewModel),ko.delaySync.resume(),e=!0,s.viewModelObservables[i](s.viewModels[i].viewModel),s.restoreUpdatedControls(a,t,!0),s.isSpaReady(!0)}finally{s.isViewModelUpdating=!1}var n=new DotvvmSpaNavigatedEventArgs(s.viewModels[i].viewModel,i,a,o);if(s.events.spaNavigated.trigger(n),!e&&!n.isHandled)throw"Invalid response from server!"})}},function(e){if(s.isPostBackStillActive(n)){var t=new DotvvmErrorEventArgs(void 0,r,i,e,-1,void 0,!0);s.events.error.trigger(t),t.handled||alert(e.responseText)}})}else document.location.href=d}},e.prototype.handleRedirect=function(e,t,r){var n;void 0===r&&(r=!1),null!=e.replace&&(r=e.replace),n=this.getSpaPlaceHolder()&&!this.useHistoryApiSpaNavigation&&e.url.indexOf("//")<0&&e.allowSpa?("#!"===(n="#!"+this.removeVirtualDirectoryFromUrl(e.url,t))&&(n="#!/"),this.fixSpaUrlPrefix(n)):e.url;var o=new DotvvmRedirectEventArgs(dotvvm.viewModels[t],t,n,r);this.events.redirect.trigger(o),this.performRedirect(n,r,e.allowSpa&&this.useHistoryApiSpaNavigation)},e.prototype.performRedirect=function(e,t,r){if(t)location.replace(e);else if(r)this.handleSpaNavigationCore(e);else{var n=this.fakeRedirectAnchor;n||((n=document.createElement("a")).style.display="none",n.setAttribute("data-dotvvm-fake-id","dotvvm_fake_redirect_anchor_87D7145D_8EA8_47BA_9941_82B75EE88CDB"),document.body.appendChild(n),this.fakeRedirectAnchor=n),n.href=e,n.click()}},e.prototype.fixSpaUrlPrefix=function(e){var t=this.getSpaPlaceHolder().attributes["data-dotvvm-spacontentplaceholder-urlprefix"];if(!t)return e;var r=t.value;return r!==document.location.pathname&&(""===r&&(r="/"),e=r+e),e},e.prototype.removeVirtualDirectoryFromUrl=function(e,t){var r="/"+this.viewModels[t].virtualDirectory;return 0==e.indexOf(r)?this.addLeadingSlash(e.substring(r.length)):e},e.prototype.addLeadingSlash=function(e){return 0i[0]&&t[1]'");return e},e.prototype.format=function(e){for(var o=this,a=[],t=1;t=e.length)n();else{var o=e[t],a=!1;if("script"==o.tagName.toLowerCase()){var i=document.createElement("script");o.src&&(i.src=o.src,a=!0),o.type&&(i.type=o.type),o.text&&(i.text=o.text),o.id&&(i.id=o.id),o=i}else if("link"==o.tagName.toLowerCase()){var s=document.createElement("link");o.href&&(s.href=o.href),o.rel&&(s.rel=o.rel),o.type&&(s.type=o.type),o=s}a&&(o.onload=function(){return r.loadResourceElements(e,t+1,n)}),document.head.appendChild(o),a||this.loadResourceElements(e,t+1,n)}},e.prototype.getSpaPlaceHolder=function(){var e=document.getElementsByName("__dot_SpaContentPlaceHolder");return 1==e.length?e[0]:null},e.prototype.navigateCore=function(i,e,t){var s=this,n=this.viewModels[i].viewModel,r=this.backUpPostBackConter(),o=new DotvvmSpaNavigatingEventArgs(n,i,e);if(this.events.spaNavigating.trigger(o),!o.cancel){var a=this.viewModels[i].virtualDirectory||"",l="/___dotvvm-spa___"+this.addLeadingSlash(e),u=this.addLeadingSlash(this.concatUrl(a,l)),d=this.getSpaPlaceHolder();if(d){t&&t(this.addLeadingSlash(this.concatUrl(a,this.addLeadingSlash(e))));var v=d.attributes["data-dotvvm-spacontentplaceholder"].value;this.getJSON(u,"GET",v,function(o){if(s.isPostBackStillActive(r)){var a=JSON.parse(o.responseText);s.loadResourceList(a.resources,function(){var e=!1;if("successfulCommand"!==a.action&&a.action){if("redirect"===a.action)return void s.handleRedirect(a,i,!0)}else try{s.isViewModelUpdating=!0;var t=s.cleanUpdatedControls(a);for(var n in s.viewModels[i]={},a)a.hasOwnProperty(n)&&(s.viewModels[i][n]=a[n]);ko.delaySync.pause(),s.serialization.deserialize(a.viewModel,s.viewModels[i].viewModel),ko.delaySync.resume(),e=!0,s.viewModelObservables[i](s.viewModels[i].viewModel),s.restoreUpdatedControls(a,t,!0),s.isSpaReady(!0)}finally{s.isViewModelUpdating=!1}var r=new DotvvmSpaNavigatedEventArgs(s.viewModels[i].viewModel,i,a,o);if(s.events.spaNavigated.trigger(r),!e&&!r.isHandled)throw"Invalid response from server!"})}},function(e){if(s.isPostBackStillActive(r)){var t=new DotvvmErrorEventArgs(void 0,n,i,e,-1,void 0,!0);s.events.error.trigger(t),t.handled||alert(e.responseText)}})}else document.location.href=u}},e.prototype.handleRedirect=function(e,t,n){var r;void 0===n&&(n=!1),null!=e.replace&&(n=e.replace),r=this.getSpaPlaceHolder()&&!this.useHistoryApiSpaNavigation&&e.url.indexOf("//")<0&&e.allowSpa?("#!"===(r="#!"+this.removeVirtualDirectoryFromUrl(e.url,t))&&(r="#!/"),this.fixSpaUrlPrefix(r)):e.url;var o=new DotvvmRedirectEventArgs(dotvvm.viewModels[t],t,r,n);this.events.redirect.trigger(o),this.performRedirect(r,n,e.allowSpa&&this.useHistoryApiSpaNavigation)},e.prototype.performRedirect=function(e,t,n){if(t)location.replace(e);else if(n)this.handleSpaNavigationCore(e);else{var r=this.fakeRedirectAnchor;r||((r=document.createElement("a")).style.display="none",r.setAttribute("data-dotvvm-fake-id","dotvvm_fake_redirect_anchor_87D7145D_8EA8_47BA_9941_82B75EE88CDB"),document.body.appendChild(r),this.fakeRedirectAnchor=r),r.href=e,r.click()}},e.prototype.fixSpaUrlPrefix=function(e){var t=this.getSpaPlaceHolder().attributes["data-dotvvm-spacontentplaceholder-urlprefix"];if(!t)return e;var n=t.value;return n!==document.location.pathname&&(""===n&&(n="/"),e=n+e),e},e.prototype.removeVirtualDirectoryFromUrl=function(e,t){var n="/"+this.viewModels[t].virtualDirectory;return 0==e.indexOf(n)?this.addLeadingSlash(e.substring(n.length)):e},e.prototype.addLeadingSlash=function(e){return 0(promise: Promise, options: PostbackOptions, queueName: string): Promise => { const queue = this.getPostbackQueue(queueName) queue.noRunning++ + dotvvm.updateProgressChangeCounter(dotvvm.updateProgressChangeCounter() + 1); const dispatchNext = () => { queue.noRunning--; + dotvvm.updateProgressChangeCounter(dotvvm.updateProgressChangeCounter() - 1); if (queue.queue.length > 0) { const callback = queue.queue.shift()! window.setTimeout(callback, 0) @@ -217,6 +219,7 @@ class DotVVM { public useHistoryApiSpaNavigation: boolean; public isPostbackRunning = ko.observable(false); + public updateProgressChangeCounter = ko.observable(0); public init(viewModelName: string, culture: string): void { this.addKnockoutBindingHandlers(); @@ -365,35 +368,59 @@ class DotVVM { return this.postBackCounter === currentPostBackCounter; } - public staticCommandPostback(viewModelName: string, sender: HTMLElement, command: string, args: any[], callback = _ => { }, errorCallback = (xhr: XMLHttpRequest, error?) => { }) { - var data = this.serialization.serialize({ - "args": args, - "command": command, - "$csrfToken": this.viewModels[viewModelName].viewModel.$csrfToken - }); - dotvvm.events.staticCommandMethodInvoking.trigger(data); - - this.postJSON(this.viewModels[viewModelName].url, "POST", ko.toJSON(data), response => { - try { - this.isViewModelUpdating = true; - const result = JSON.parse(response.responseText); - dotvvm.events.staticCommandMethodInvoked.trigger({ ...data, result, xhr: response }); - callback(result); - } catch (error) { - dotvvm.events.staticCommandMethodFailed.trigger({ ...data, xhr: response, error: error }) - errorCallback(response, error); - } finally { - this.isViewModelUpdating = false; - } - }, (xhr) => { - this.events.error.trigger(new DotvvmErrorEventArgs(sender, this.viewModels[viewModelName].viewModel, viewModelName, xhr, null)); - console.warn(`StaticCommand postback failed: ${xhr.status} - ${xhr.statusText}`, xhr); - errorCallback(xhr); - dotvvm.events.staticCommandMethodFailed.trigger({ ...data, xhr }) - }, + private async fetchCsrfToken(viewModelName: string): Promise { + const vm = this.viewModels[viewModelName].viewModel + if (vm.$csrfToken == null) { + const response = await fetch((this.viewModels[viewModelName].virtualDirectory || "") + "/___dotvvm-create-csrf-token___") + if (response.status != 200) + throw new Error(`Can't fetch CSRF token: ${response.statusText}`) + vm.$csrfToken = await response.text() + } + return vm.$csrfToken + } + + public staticCommandPostback(viewModelName: string, sender: HTMLElement, command: string, args: any[], callback = _ => { }, errorCallback = (errorInfo: {xhr: XMLHttpRequest, error?: any}) => { }) { + (async () => { + var data = this.serialization.serialize({ + args, + command, + "$csrfToken": await this.fetchCsrfToken(viewModelName) + }); + dotvvm.events.staticCommandMethodInvoking.trigger(data); + + this.postJSON(this.viewModels[viewModelName].url, "POST", ko.toJSON(data), response => { + try { + this.isViewModelUpdating = true; + const result = JSON.parse(response.responseText); + dotvvm.events.staticCommandMethodInvoked.trigger({ ...data, result, xhr: response }); + callback(result); + } catch (error) { + dotvvm.events.staticCommandMethodFailed.trigger({ ...data, xhr: response, error: error }) + errorCallback({ xhr: response, error }); + } finally { + this.isViewModelUpdating = false; + } + }, (xhr) => { + if (/^application\/json(;|$)/.test(xhr.getResponseHeader("Content-Type")!)) { + const errObject = JSON.parse(xhr.responseText) + + if (errObject.action === "invalidCsrfToken") { + // ok, renew the token and try again. Do that before any event is triggered + this.viewModels[viewModelName].viewModel.$csrfToken = null + console.log("Resending postback due to invalid CSRF token.") // this may loop indefinitely (in some extreme case), we don't currently have any loop detection mechanism, so at least we can log it. + this.staticCommandPostback(viewModelName, sender, command, args, callback, errorCallback) + return; + } + } + this.events.error.trigger(new DotvvmErrorEventArgs(sender, this.viewModels[viewModelName].viewModel, viewModelName, xhr, null)); + console.warn(`StaticCommand postback failed: ${xhr.status} - ${xhr.statusText}`, xhr); + errorCallback({ xhr }); + dotvvm.events.staticCommandMethodFailed.trigger({ ...data, xhr }) + }, xhr => { xhr.setRequestHeader("X-PostbackType", "StaticCommand"); }); + })() } private processPassedId(id: any, context: any): string { @@ -496,8 +523,9 @@ class DotVVM { } public postbackCore(options: PostbackOptions, path: string[], command: string, controlUniqueId: string, context: any, commandArgs?: any[]) { - return new Promise<() => Promise>((resolve, reject) => { + return new Promise<() => Promise>(async (resolve, reject) => { const viewModelName = options.viewModelName!; + await this.fetchCsrfToken(viewModelName) const viewModel = this.viewModels[viewModelName].viewModel; this.lastStartedPostack = options.postbackId @@ -579,6 +607,17 @@ class DotVVM { }); })); }, xhr => { + if (/^application\/json(;|$)/.test(xhr.getResponseHeader("Content-Type")!)) { + const errObject = JSON.parse(xhr.responseText) + + if (errObject.action === "invalidCsrfToken") { + // ok, renew the token and try again. Do that before any event is triggered + this.viewModels[viewModelName].viewModel.$csrfToken = null + console.log("Resending postback due to invalid CSRF token.") // this may loop indefinitely (in some extreme case), we don't currently have any loop detection mechanism, so at least we can log it. + this.postbackCore(options, path, command, controlUniqueId, context, commandArgs).then(resolve, reject) + return; + } + } reject({ type: 'network', options: options, args: new DotvvmErrorEventArgs(options.sender, viewModel, viewModelName, xhr, options.postbackId) }); }); }); @@ -1149,43 +1188,65 @@ class DotVVM { } } + const makeUpdatableChildrenContextHandler = ( + makeContextCallback: (bindingContext: KnockoutBindingContext, value: any) => any, + shouldDisplay: (value: any) => boolean + ) => (element: Node, valueAccessor, _allBindings, _viewModel, bindingContext: KnockoutBindingContext) => { + if (!bindingContext) throw new Error() + + var savedNodes : Node[] | undefined; + ko.computed(function() { + var rawValue = valueAccessor(); + + // Save a copy of the inner nodes on the initial update, but only if we have dependencies. + if (!savedNodes && ko.computedContext.getDependenciesCount()) { + savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */); + } + + if (shouldDisplay(rawValue)) { + if (savedNodes) { + ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes)); + } + ko.applyBindingsToDescendants(makeContextCallback(bindingContext, rawValue), element); + } else { + ko.virtualElements.emptyNode(element); + } + + }, null, { disposeWhenNodeIsRemoved: element }); + return { controlsDescendantBindings: true } // do not apply binding again + } + const foreachCollectionSymbol = "$foreachCollectionSymbol" ko.virtualElements.allowedBindings["dotvvm-SSR-foreach"] = true ko.bindingHandlers["dotvvm-SSR-foreach"] = { - init(element, valueAccessor, _allBindings, _viewModel, bindingContext) { - if (!bindingContext) throw new Error() - var value = valueAccessor() - var innerBindingContext = bindingContext.extend({ [foreachCollectionSymbol]: value.data }) - element.innerBindingContext = innerBindingContext - ko.applyBindingsToDescendants(innerBindingContext, element) - return { controlsDescendantBindings: true } // do not apply binding again - - } + init: makeUpdatableChildrenContextHandler( + (bindingContext, rawValue) => bindingContext.extend({ [foreachCollectionSymbol]: rawValue.data }), + v => v.data != null) } ko.virtualElements.allowedBindings["dotvvm-SSR-item"] = true ko.bindingHandlers["dotvvm-SSR-item"] = { init(element, valueAccessor, _allBindings, _viewModel, bindingContext) { if (!bindingContext) throw new Error() - var index = valueAccessor() var collection = bindingContext[foreachCollectionSymbol] - var innerBindingContext = bindingContext.createChildContext(() => ko.unwrap((ko.unwrap(collection) || [])[index])).extend({$index: ko.pureComputed(() => index)}) + var innerBindingContext = bindingContext.createChildContext(() => { + return ko.unwrap((ko.unwrap(collection) || [])[valueAccessor()]); + }).extend({$index: ko.pureComputed(valueAccessor)}); element.innerBindingContext = innerBindingContext ko.applyBindingsToDescendants(innerBindingContext, element) return { controlsDescendantBindings: true } // do not apply binding again + }, + update(element) { + if (element.seenUpdate) + console.error(`dotvvm-SSR-item binding did not expect to see a update`); + element.seenUpdate = 1; } } ko.virtualElements.allowedBindings["withGridViewDataSet"] = true; ko.bindingHandlers["withGridViewDataSet"] = { - init: (element, valueAccessor, allBindings, viewModel, bindingContext) => { - if (!bindingContext) throw new Error(); - var value = valueAccessor(); - var innerBindingContext = bindingContext.extend({ $gridViewDataSet: value, [foreachCollectionSymbol]: dotvvm.evaluator.getDataSourceItems(value) }); - element.innerBindingContext = innerBindingContext; - ko.applyBindingsToDescendants(innerBindingContext, element); - return { controlsDescendantBindings: true }; // do not apply binding again - }, - update(element, valueAccessor, allBindings, viewModel, bindingContext) { - } + init: makeUpdatableChildrenContextHandler( + (bindingContext, value) => bindingContext.extend({ $gridViewDataSet: value, [foreachCollectionSymbol]: dotvvm.evaluator.getDataSourceItems(value) }), + _ => true + ) }; ko.bindingHandlers['dotvvmEnable'] = { @@ -1226,6 +1287,10 @@ class DotVVM { init(element: any, valueAccessor: () => any, allBindingsAccessor: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) { element.style.display = "none"; var delay = element.getAttribute("data-delay"); + + let includedQueues = (element.getAttribute("data-included-queues") || "").split(",").filter(i => i.length > 0); + let excludedQueues = (element.getAttribute("data-excluded-queues") || "").split(",").filter(i => i.length > 0); + var timeout; var running = false; @@ -1246,8 +1311,21 @@ class DotVVM { element.style.display = "none"; } - dotvvm.isPostbackRunning.subscribe(e => { - if (e) { + dotvvm.updateProgressChangeCounter.subscribe(e => { + let shouldRun = false; + + if (includedQueues.length === 0) { + for (let queue in dotvvm.postbackQueues) { + if (excludedQueues.indexOf(queue) < 0 && dotvvm.postbackQueues[queue].noRunning > 0) { + shouldRun = true; + break; + } + } + } else { + shouldRun = includedQueues.some(q => dotvvm.postbackQueues[q] && dotvvm.postbackQueues[q].noRunning > 0); + } + + if (shouldRun) { if (!running) { show(); } diff --git a/src/DotVVM.Framework/Resources/Scripts/knockout-latest.debug.js b/src/DotVVM.Framework/Resources/Scripts/knockout-latest.debug.js index 56f9cd26f4..579e9e73cb 100644 --- a/src/DotVVM.Framework/Resources/Scripts/knockout-latest.debug.js +++ b/src/DotVVM.Framework/Resources/Scripts/knockout-latest.debug.js @@ -705,6 +705,7 @@ ko.exportSymbol('utils.arrayIndexOf', ko.utils.arrayIndexOf); ko.exportSymbol('utils.arrayMap', ko.utils.arrayMap); ko.exportSymbol('utils.arrayPushAll', ko.utils.arrayPushAll); ko.exportSymbol('utils.arrayRemoveItem', ko.utils.arrayRemoveItem); +ko.exportSymbol('utils.cloneNodes', ko.utils.cloneNodes); ko.exportSymbol('utils.extend', ko.utils.extend); ko.exportSymbol('utils.fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost); ko.exportSymbol('utils.getFormFields', ko.utils.getFormFields); diff --git a/src/DotVVM.Framework/Resources/Scripts/knockout-latest.js b/src/DotVVM.Framework/Resources/Scripts/knockout-latest.js index 7861373097..c2821fc388 100644 --- a/src/DotVVM.Framework/Resources/Scripts/knockout-latest.js +++ b/src/DotVVM.Framework/Resources/Scripts/knockout-latest.js @@ -5,64 +5,64 @@ */ (function() {(function(n){var y=this||(0,eval)("this"),t=y.document,N=y.navigator,w=y.jQuery,I=y.JSON;(function(n){"function"===typeof define&&define.amd?define(["exports","require"],n):"object"===typeof exports&&"object"===typeof module?n(module.exports||exports):n(y.ko={})})(function(O,P){function K(a,c){return null===a||typeof a in T?a===c:!1}function U(b,c){var d;return function(){d||(d=a.a.setTimeout(function(){d=n;b()},c))}}function V(b,c){var d;return function(){clearTimeout(d);d=a.a.setTimeout(b,c)}}function W(a, -c){c&&"change"!==c?"beforeChange"===c?this.Vb(a):this.Ua(a,c):this.Wb(a)}function X(a,c){null!==c&&c.o&&c.o()}function Y(a,c){var d=this.Wc,e=d[u];e.da||(this.Ab&&this.$a[c]?(d.Zb(c,a,this.$a[c]),this.$a[c]=null,--this.Ab):e.F[c]||d.Zb(c,a,e.G?{ta:a}:d.Ic(a)),a.tb&&a.Rc())}function L(b,c,d,e){a.f[b]={init:function(b,h,g,l,k){var m,q;a.u(function(){var l=h(),g=a.a.c(l),g=c?null!=g:!d!==!g,z=!q;if(z||c||g!==m)z&&a.Ja.Na()&&(q=a.a.Ia(a.g.childNodes(b),!0)),g?(z||a.g.oa(b,a.a.Ia(q)),a.Fa(e?e(k,l):k,b)): -a.g.Ka(b),m=g},null,{l:b});return{controlsDescendantBindings:!0}}};a.j.Ha[b]=!1;a.g.$[b]=!0}var a="undefined"!==typeof O?O:{};a.b=function(b,c){for(var d=b.split("."),e=a,f=0;fa.a.A(c,b[d])&&c.push(b[d]);return c},ub:function(a,b){a=a||[];for(var c=[],d=0,e=a.length;dk?d&&b.push(c):d||b.splice(k,1)},va:h,extend:c,setPrototypeOf:d,nb:h?d:c,L:b,Qa:function(a,b){if(!a)return a;var c={},d;for(d in a)f.call(a,d)&&(c[d]=b(a[d],d,a));return c},Db:function(b){for(;b.firstChild;)a.removeNode(b.firstChild)},wc:function(b){b=a.a.fa(b);for(var c=(b[0]&&b[0].ownerDocument||t).createElement("div"),d=0,e=b.length;dk?d&&b.push(c):d||b.splice(k,1)},va:h,extend:c,setPrototypeOf:d,nb:h?d:c,L:b,Qa:function(a,b){if(!a)return a;var c={},d;for(d in a)f.call(a,d)&&(c[d]=b(a[d],d,a));return c},Db:function(b){for(;b.firstChild;)a.removeNode(b.firstChild)},wc:function(b){b=a.a.fa(b);for(var c=(b[0]&&b[0].ownerDocument||t).createElement("div"),d=0,e=b.length;dq?a.setAttribute("selected",b):a.selected=b},pb:function(a){return null===a||a===n?"":a.trim?a.trim():a.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")},Ad:function(a,b){a=a||"";return b.length>a.length?!1:a.substring(0,b.length)===b},ad:function(a,b){if(a===b)return!0;if(11===a.nodeType)return!1;if(b.contains)return b.contains(1!==a.nodeType?a.parentNode:a);if(b.compareDocumentPosition)return 16== (b.compareDocumentPosition(a)&16);for(;a&&a!=b;)a=a.parentNode;return!!a},Cb:function(b){return a.a.ad(b,b.ownerDocument.documentElement)},$b:function(b){return!!a.a.bc(b,a.a.Cb)},K:function(a){return a&&a.tagName&&a.tagName.toLowerCase()},fc:function(b){return a.onError?function(){try{return b.apply(this,arguments)}catch(c){throw a.onError&&a.onError(c),c;}}:b},setTimeout:function(b,c){return setTimeout(a.a.fc(b),c)},kc:function(b){setTimeout(function(){a.onError&&a.onError(b);throw b;},0)},C:function(b, -c,d){var e=a.a.fc(d);d=m[c];if(a.options.useOnlyNativeEvents||d||!w)if(d||"function"!=typeof b.addEventListener)if("undefined"!=typeof b.attachEvent){var k=function(a){e.call(b,a)},f="on"+c;b.attachEvent(f,k);a.a.O.Da(b,function(){b.detachEvent(f,k)})}else throw Error("Browser doesn't support addEventListener or attachEvent");else b.addEventListener(c,e,!1);else p||(p="function"==typeof w(b).on?"on":"bind"),w(b)[p](c,e)},Ra:function(b,c){if(!b||!b.nodeType)throw Error("element must be a DOM node when calling triggerEvent"); +c,d){var e=a.a.fc(d);d=m[c];if(a.options.useOnlyNativeEvents||d||!w)if(d||"function"!=typeof b.addEventListener)if("undefined"!=typeof b.attachEvent){var k=function(a){e.call(b,a)},f="on"+c;b.attachEvent(f,k);a.a.O.Ea(b,function(){b.detachEvent(f,k)})}else throw Error("Browser doesn't support addEventListener or attachEvent");else b.addEventListener(c,e,!1);else p||(p="function"==typeof w(b).on?"on":"bind"),w(b)[p](c,e)},Ra:function(b,c){if(!b||!b.nodeType)throw Error("element must be a DOM node when calling triggerEvent"); var d;"input"===a.a.K(b)&&b.type&&"click"==c.toLowerCase()?(d=b.type,d="checkbox"==d||"radio"==d):d=!1;if(a.options.useOnlyNativeEvents||!w||d)if("function"==typeof t.createEvent)if("function"==typeof b.dispatchEvent)d=t.createEvent(k[c]||"HTMLEvents"),d.initEvent(c,!0,!0,y,0,0,0,0,0,!1,!1,!1,!1,0,b),b.dispatchEvent(d);else throw Error("The supplied element doesn't support dispatchEvent");else if(d&&b.click)b.click();else if("undefined"!=typeof b.fireEvent)b.fireEvent("on"+c);else throw Error("Browser doesn't support triggering events"); -else w(b).trigger(c)},c:function(b){return a.J(b)?b():b},Cd:function(b){return a.J(b)?b:a.T(b)},za:function(b){return a.J(b)?b.B():b},rb:function(b,c,d){var k;c&&("object"===typeof b.classList?(k=b.classList[d?"add":"remove"],a.a.D(c.match(r),function(a){k.call(b.classList,a)})):"string"===typeof b.className.baseVal?e(b.className,"baseVal",c,d):e(b,"className",c,d))},ob:function(b,c){var d=a.a.c(c);if(null===d||d===n)d="";var e=a.g.firstChild(b);!e||3!=e.nodeType||a.g.nextSibling(e)?a.g.oa(b,[b.ownerDocument.createTextNode(d)]): +else w(b).trigger(c)},c:function(b){return a.J(b)?b():b},Cd:function(b){return a.J(b)?b:a.T(b)},Aa:function(b){return a.J(b)?b.B():b},rb:function(b,c,d){var k;c&&("object"===typeof b.classList?(k=b.classList[d?"add":"remove"],a.a.D(c.match(r),function(a){k.call(b.classList,a)})):"string"===typeof b.className.baseVal?e(b.className,"baseVal",c,d):e(b,"className",c,d))},ob:function(b,c){var d=a.a.c(c);if(null===d||d===n)d="";var e=a.g.firstChild(b);!e||3!=e.nodeType||a.g.nextSibling(e)?a.g.oa(b,[b.ownerDocument.createTextNode(d)]): e.data=d;a.a.fd(b)},Fc:function(a,b){a.name=b;if(7>=q)try{a.mergeAttributes(t.createElement(""),!1)}catch(c){}},fd:function(a){9<=q&&(a=1==a.nodeType?a:a.parentNode,a.style&&(a.style.zoom=a.style.zoom))},bd:function(a){if(q){var b=a.style.width;a.style.width=0;a.style.width=b}},vd:function(b,c){b=a.a.c(b);c=a.a.c(c);for(var d=[],e=b;e<=c;e++)d.push(e);return d},fa:function(a){for(var b=[],c=0,d=a.length;c",""],d=[3,"","
"],e=[1,""],f={thead:c,tbody:c, -tfoot:c,tr:[2,"","
"],td:d,th:d,option:e,optgroup:e},h=8>=a.a.ca;a.a.ya=function(c,d){var e;if(w)if(w.parseHTML)e=w.parseHTML(c,d)||[];else{if((e=w.clean([c],d))&&e[0]){for(var m=e[0];m.parentNode&&11!==m.parentNode.nodeType;)m=m.parentNode;m.parentNode&&m.parentNode.removeChild(m)}}else{(e=d)||(e=t);var m=e.parentWindow||e.defaultView||y,q=a.a.pb(c).toLowerCase(),r=e.createElement("div"),p;p=(q=q.match(/^(?:\x3c!--.*?--\x3e\s*?)*?<([a-z]+)[\s>]/))&&f[q[1]]||b;q=p[0]; -p="ignored
"+p[1]+c+p[2]+"
";"function"==typeof m.innerShiv?r.appendChild(m.innerShiv(p)):(h&&e.appendChild(r),r.innerHTML=p,h&&r.parentNode.removeChild(r));for(;q--;)r=r.lastChild;e=a.a.fa(r.lastChild.childNodes)}return e};a.a.Mb=function(b,c){a.a.Db(b);c=a.a.c(c);if(null!==c&&c!==n)if("string"!=typeof c&&(c=c.toString()),w)w(b).html(c);else for(var d=a.a.ya(c,b.ownerDocument),e=0;eb){if(5E3<=++c){g=f;a.a.kc(Error("'Too much recursion' after processing "+c+" task groups."));break}b=f}try{d()}catch(q){a.a.kc(q)}}}function c(){b();g=f=e.length=0}var d,e=[],f=0,h=1,g=0;y.MutationObserver?d=function(a){var b=t.createElement("div");(new MutationObserver(a)).observe(b,{attributes:!0});return function(){b.classList.toggle("foo")}}(c): +c.type="hidden";c.name=a;c.value=b;q.appendChild(c)});t.body.appendChild(q);e.submitter?e.submitter(q):q.submit();setTimeout(function(){q.parentNode.removeChild(q)},0)}}}();a.b("utils",a.a);a.b("utils.arrayForEach",a.a.D);a.b("utils.arrayFirst",a.a.bc);a.b("utils.arrayFilter",a.a.Xa);a.b("utils.arrayGetDistinctValues",a.a.cc);a.b("utils.arrayIndexOf",a.a.A);a.b("utils.arrayMap",a.a.ub);a.b("utils.arrayPushAll",a.a.Ya);a.b("utils.arrayRemoveItem",a.a.Za);a.b("utils.cloneNodes",a.a.wa);a.b("utils.extend", +a.a.extend);a.b("utils.fieldsIncludedWithJsonPost",a.a.nc);a.b("utils.getFormFields",a.a.pc);a.b("utils.peekObservable",a.a.Aa);a.b("utils.postJson",a.a.ud);a.b("utils.parseJson",a.a.sd);a.b("utils.registerEventHandler",a.a.C);a.b("utils.stringifyJson",a.a.Ob);a.b("utils.range",a.a.vd);a.b("utils.toggleDomNodeCssClass",a.a.rb);a.b("utils.triggerEvent",a.a.Ra);a.b("utils.unwrapObservable",a.a.c);a.b("utils.objectForEach",a.a.L);a.b("utils.addOrRemoveItem",a.a.Fa);a.b("utils.setTextContent",a.a.ob); +a.b("unwrap",a.a.c);Function.prototype.bind||(Function.prototype.bind=function(a){var c=this;if(1===arguments.length)return function(){return c.apply(a,arguments)};var d=Array.prototype.slice.call(arguments,1);return function(){var e=d.slice(0);e.push.apply(e,arguments);return c.apply(a,e)}});a.a.h=new function(){function a(b,h){var g=b[d];if(!g||"null"===g||!e[g]){if(!h)return n;g=b[d]="ko"+c++;e[g]={}}return e[g]}var c=0,d="__ko__"+(new Date).getTime(),e={};return{get:function(c,d){var e=a(c,!1); +return e===n?n:e[d]},set:function(c,d,e){if(e!==n||a(c,!1)!==n)a(c,!0)[d]=e},clear:function(a){var b=a[d];return b?(delete e[b],a[d]=null,!0):!1},S:function(){return c++ +d}}};a.b("utils.domData",a.a.h);a.b("utils.domData.clear",a.a.h.clear);a.a.O=new function(){function b(b,c){var e=a.a.h.get(b,d);e===n&&c&&(e=[],a.a.h.set(b,d,e));return e}function c(d){var e=b(d,!1);if(e)for(var e=e.slice(0),l=0;l",""],d=[3,"","
"], +e=[1,""],f={thead:c,tbody:c,tfoot:c,tr:[2,"","
"],td:d,th:d,option:e,optgroup:e},h=8>=a.a.ca;a.a.za=function(c,d){var e;if(w)if(w.parseHTML)e=w.parseHTML(c,d)||[];else{if((e=w.clean([c],d))&&e[0]){for(var m=e[0];m.parentNode&&11!==m.parentNode.nodeType;)m=m.parentNode;m.parentNode&&m.parentNode.removeChild(m)}}else{(e=d)||(e=t);var m=e.parentWindow||e.defaultView||y,q=a.a.pb(c).toLowerCase(),r=e.createElement("div"),p;p=(q=q.match(/^(?:\x3c!--.*?--\x3e\s*?)*?<([a-z]+)[\s>]/))&& +f[q[1]]||b;q=p[0];p="ignored
"+p[1]+c+p[2]+"
";"function"==typeof m.innerShiv?r.appendChild(m.innerShiv(p)):(h&&e.appendChild(r),r.innerHTML=p,h&&r.parentNode.removeChild(r));for(;q--;)r=r.lastChild;e=a.a.fa(r.lastChild.childNodes)}return e};a.a.Mb=function(b,c){a.a.Db(b);c=a.a.c(c);if(null!==c&&c!==n)if("string"!=typeof c&&(c=c.toString()),w)w(b).html(c);else for(var d=a.a.za(c,b.ownerDocument),e=0;eb){if(5E3<=++c){g=f;a.a.kc(Error("'Too much recursion' after processing "+c+" task groups."));break}b=f}try{d()}catch(q){a.a.kc(q)}}}function c(){b();g=f=e.length=0}var d,e=[],f=0,h=1,g=0;y.MutationObserver?d=function(a){var b=t.createElement("div");(new MutationObserver(a)).observe(b,{attributes:!0});return function(){b.classList.toggle("foo")}}(c): d=t&&"onreadystatechange"in t.createElement("script")?function(a){var b=t.createElement("script");b.onreadystatechange=function(){b.onreadystatechange=null;t.documentElement.removeChild(b);b=null;a()};t.documentElement.appendChild(b)}:function(a){setTimeout(a,0)};return{scheduler:d,mb:function(b){f||a.ga.scheduler(c);e[f++]=b;return h++},cancel:function(a){a=a-(h-f);a>=g&&ad[0]?l+d[0]:d[0]), +indexOf:function(b){var c=this();return a.a.A(c,b)},replace:function(a,c){var d=this.indexOf(a);0<=d&&(this.ra(),this.B()[d]=c,this.qa())}};a.a.va&&a.a.setPrototypeOf(a.ya.fn,a.T.fn);a.a.D("pop push reverse shift sort splice unshift".split(" "),function(b){a.ya.fn[b]=function(){var a=this.B();this.ra();this.ec(a,b,arguments);var d=a[b].apply(a,arguments);this.qa();return d===a?this:d}});a.a.D(["slice"],function(b){a.ya.fn[b]=function(){var a=this();return a[b].apply(a,arguments)}});a.b("observableArray", +a.ya);a.La.trackArrayChanges=function(b,c){function d(){if(!e){e=!0;l=b.notifySubscribers;b.notifySubscribers=function(a,b){b&&"change"!==b||++g;return l.apply(this,arguments)};var c=[].concat(b.B()||[]);f=null;h=b.subscribe(function(d){d=[].concat(d||[]);if(b.cb("arrayChange")){var e;if(!f||1d[0]?l+d[0]:d[0]), l);for(var l=1===h?l:Math.min(c+(d[1]||0),l),h=c+h-2,H=Math.max(l,h),n=[],Q=[],v=2;cc;c++)b=b();return b})};a.toJSON=function(b,c,d){b=a.Kc(b);return a.a.Ob(b,c,d)};d.prototype={constructor:d,save:function(b,c){var d=a.a.A(this.keys,b);0<=d?this.values[d]=c:(this.keys.push(b),this.values.push(c))},get:function(b){b=a.a.A(this.keys,b);return 0<=b?this.values[b]:n}}})();a.b("toJS",a.Kc);a.b("toJSON",a.toJSON);(function(){a.m={H:function(b){switch(a.a.K(b)){case "option":return!0===b.__ko__hasDomDataOptionValue__?a.a.h.get(b,a.f.options.Hb):7>=a.a.ca?b.getAttributeNode("value")&& b.getAttributeNode("value").specified?b.value:b.text:b.value;case "select":return 0<=b.selectedIndex?a.m.H(b.options[b.selectedIndex]):n;default:return b.value}},sa:function(b,c,d){switch(a.a.K(b)){case "option":switch(typeof c){case "string":a.a.h.set(b,a.f.options.Hb,n);"__ko__hasDomDataOptionValue__"in b&&delete b.__ko__hasDomDataOptionValue__;b.value=c;break;default:a.a.h.set(b,a.f.options.Hb,c),b.__ko__hasDomDataOptionValue__=!0,b.value="number"===typeof c?c:""}break;case "select":if(""===c|| null===c)c=n;for(var e=-1,f=0,h=b.options.length,g;f=p){c.push(q&&g.length?{key:q,value:g.join("")}:{unknown:q||g.join("")});q=p=0;g=[];continue}}else if(58===x){if(!p&&!q&&1===g.length){q=g.pop();continue}}else if(47===x&&1=a.a.ca&&b.tagName===c))return c};a.i.Yb=function(c,e,f,h){if(1===e.nodeType){var g=a.i.getComponentNameForNode(e);if(g){c=c||{};if(c.component)throw Error('Cannot use the "component" binding on a custom element matching a component'); -var l={name:g,params:b(e,f)};c.component=h?function(){return l}:l}}return c};var c=new a.ba;9>a.a.ca&&(a.i.register=function(a){return function(b){return a.apply(this,arguments)}}(a.i.register),t.createDocumentFragment=function(b){return function(){var c=b(),f=a.i.Oc,h;for(h in f);return c}}(t.createDocumentFragment))})();(function(b){function c(b,c,d){c=c.template;if(!c)throw Error("Component '"+b+"' has no template");b=a.a.Ia(c);a.g.oa(d,b)}function d(a,b,c,d){var e=a.createViewModel;return e?e.call(a, -d,{element:b,templateNodes:c}):d}var e=0;a.f.component={init:function(f,h,g,l,k){function m(){var a=q&&q.dispose;"function"===typeof a&&a.call(q);r=q=null}var q,r,p=a.a.fa(a.g.childNodes(f));a.a.O.Da(f,m);a.u(function(){var g=a.a.c(h()),l,n;"string"===typeof g?l=g:(l=a.a.c(g.name),n=a.a.c(g.params));if(!l)throw Error("No component name specified");var G=r=++e;a.i.get(l,function(e){if(r===G){m();if(!e)throw Error("Unknown component '"+l+"'");c(l,e,f);var g=d(e,f,p,n);e=k.createChildContext(g,b,function(a){a.$component= -g;a.$componentTemplateNodes=p});q=g;a.Fa(e,f)}})},null,{l:f});return{controlsDescendantBindings:!0}}};a.g.$.component=!0})();var S={"class":"className","for":"htmlFor"};a.f.attr={update:function(b,c){var d=a.a.c(c())||{};a.a.L(d,function(c,d){d=a.a.c(d);var h=!1===d||null===d||d===n;h&&b.removeAttribute(c);8>=a.a.ca&&c in S?(c=S[c],h?b.removeAttribute(c):b[c]=d):h||b.setAttribute(c,d.toString());"name"===c&&a.a.Fc(b,h?"":d.toString())})}};(function(){a.f.checked={after:["value","attr"],init:function(b, -c,d){function e(){var e=b.checked,f=z?h():e;if(!a.Ja.hb()&&(!k||e)){var l=a.s.I(c);if(q){var m=r?l.B():l;p!==f?(e&&(a.a.Ea(m,f,!0,g),a.a.Ea(m,p,!1,g)),p=f):a.a.Ea(m,f,e,g);r&&a.Oa(l)&&l(m)}else a.j.Aa(l,d,"checked",f,!0)}}function f(){var d=a.a.c(c());q?b.checked=0<=a.a.A(d,h(),g):l?b.checked=d:b.checked=h()===d}var h=a.Ac(function(){return d.has("checkedValue")?a.a.c(d.get("checkedValue")):d.has("value")?a.a.c(d.get("value")):b.value}),g=d.has("checkedArrayContainsObservables")&&d.get("checkedArrayContainsObservables"), +var l={name:g,params:b(e,f)};c.component=h?function(){return l}:l}}return c};var c=new a.ba;9>a.a.ca&&(a.i.register=function(a){return function(b){return a.apply(this,arguments)}}(a.i.register),t.createDocumentFragment=function(b){return function(){var c=b(),f=a.i.Oc,h;for(h in f);return c}}(t.createDocumentFragment))})();(function(b){function c(b,c,d){c=c.template;if(!c)throw Error("Component '"+b+"' has no template");b=a.a.wa(c);a.g.oa(d,b)}function d(a,b,c,d){var e=a.createViewModel;return e?e.call(a, +d,{element:b,templateNodes:c}):d}var e=0;a.f.component={init:function(f,h,g,l,k){function m(){var a=q&&q.dispose;"function"===typeof a&&a.call(q);r=q=null}var q,r,p=a.a.fa(a.g.childNodes(f));a.a.O.Ea(f,m);a.u(function(){var g=a.a.c(h()),l,n;"string"===typeof g?l=g:(l=a.a.c(g.name),n=a.a.c(g.params));if(!l)throw Error("No component name specified");var G=r=++e;a.i.get(l,function(e){if(r===G){m();if(!e)throw Error("Unknown component '"+l+"'");c(l,e,f);var g=d(e,f,p,n);e=k.createChildContext(g,b,function(a){a.$component= +g;a.$componentTemplateNodes=p});q=g;a.Ga(e,f)}})},null,{l:f});return{controlsDescendantBindings:!0}}};a.g.$.component=!0})();var S={"class":"className","for":"htmlFor"};a.f.attr={update:function(b,c){var d=a.a.c(c())||{};a.a.L(d,function(c,d){d=a.a.c(d);var h=!1===d||null===d||d===n;h&&b.removeAttribute(c);8>=a.a.ca&&c in S?(c=S[c],h?b.removeAttribute(c):b[c]=d):h||b.setAttribute(c,d.toString());"name"===c&&a.a.Fc(b,h?"":d.toString())})}};(function(){a.f.checked={after:["value","attr"],init:function(b, +c,d){function e(){var e=b.checked,f=z?h():e;if(!a.Ja.hb()&&(!k||e)){var l=a.s.I(c);if(q){var m=r?l.B():l;p!==f?(e&&(a.a.Fa(m,f,!0,g),a.a.Fa(m,p,!1,g)),p=f):a.a.Fa(m,f,e,g);r&&a.Oa(l)&&l(m)}else a.j.Ba(l,d,"checked",f,!0)}}function f(){var d=a.a.c(c());q?b.checked=0<=a.a.A(d,h(),g):l?b.checked=d:b.checked=h()===d}var h=a.Ac(function(){return d.has("checkedValue")?a.a.c(d.get("checkedValue")):d.has("value")?a.a.c(d.get("value")):b.value}),g=d.has("checkedArrayContainsObservables")&&d.get("checkedArrayContainsObservables"), l="checkbox"==b.type,k="radio"==b.type;if(l||k){var m=c(),q=l&&a.a.c(m)instanceof Array,r=!(q&&m.push&&m.splice),p=q?h():n,z=k||q;k&&!b.name&&a.f.uniqueName.init(b,function(){return!0});a.u(e,null,{l:b});a.a.C(b,"click",e);a.u(f,null,{l:b});m=n}}};a.j.pa.checked=!0;a.f.checkedValue={update:function(b,c){b.value=a.a.c(c())}}})();a.f["class"]={update:function(b,c){var d=a.a.pb(a.a.c(c()));a.a.rb(b,b.__ko__cssValue,!1);b.__ko__cssValue=d;a.a.rb(b,d,!0)}};a.f.css={update:function(b,c){var d=a.a.c(c()); null!==d&&"object"==typeof d?a.a.L(d,function(c,d){d=a.a.c(d);a.a.rb(b,c,d)}):a.f["class"].update(b,c)}};a.f.enable={update:function(b,c){var d=a.a.c(c());d&&b.disabled?b.removeAttribute("disabled"):d||b.disabled||(b.disabled=!0)}};a.f.disable={update:function(b,c){a.f.enable.update(b,function(){return!a.a.c(c())})}};a.f.event={init:function(b,c,d,e,f){var h=c()||{};a.a.L(h,function(g){"string"==typeof g&&a.a.C(b,g,function(b){var h,m=c()[g];if(m){try{var q=a.a.fa(arguments);e=f.$data;q.unshift(e); -h=m.apply(e,q)}finally{!0!==h&&(b.preventDefault?b.preventDefault():b.returnValue=!1)}!1===d.get(g+"Bubble")&&(b.cancelBubble=!0,b.stopPropagation&&b.stopPropagation())}})})}};a.f.foreach={vc:function(b){return function(){var c=b(),d=a.a.za(c);if(!d||"number"==typeof d.length)return{foreach:c,templateEngine:a.X.Ca};a.a.c(c);return{foreach:d.data,separatorTemplate:d.separatorTemplate,as:d.as,includeDestroyed:d.includeDestroyed,afterAdd:d.afterAdd,beforeRemove:d.beforeRemove,afterRender:d.afterRender, -beforeMove:d.beforeMove,afterMove:d.afterMove,templateEngine:a.X.Ca}}},init:function(b,c){return a.f.template.init(b,a.f.foreach.vc(c))},update:function(b,c,d,e,f){return a.f.template.update(b,a.f.foreach.vc(c),d,e,f)}};a.j.Ha.foreach=!1;a.g.$.foreach=!0;a.f.hasfocus={init:function(b,c,d){function e(e){b.__ko_hasfocusUpdating=!0;var f=b.ownerDocument;if("activeElement"in f){var h;try{h=f.activeElement}catch(m){h=f.body}e=h===b}f=c();a.j.Aa(f,d,"hasfocus",e,!0);b.__ko_hasfocusLastValue=e;b.__ko_hasfocusUpdating= +h=m.apply(e,q)}finally{!0!==h&&(b.preventDefault?b.preventDefault():b.returnValue=!1)}!1===d.get(g+"Bubble")&&(b.cancelBubble=!0,b.stopPropagation&&b.stopPropagation())}})})}};a.f.foreach={vc:function(b){return function(){var c=b(),d=a.a.Aa(c);if(!d||"number"==typeof d.length)return{foreach:c,templateEngine:a.X.Da};a.a.c(c);return{foreach:d.data,separatorTemplate:d.separatorTemplate,as:d.as,includeDestroyed:d.includeDestroyed,afterAdd:d.afterAdd,beforeRemove:d.beforeRemove,afterRender:d.afterRender, +beforeMove:d.beforeMove,afterMove:d.afterMove,templateEngine:a.X.Da}}},init:function(b,c){return a.f.template.init(b,a.f.foreach.vc(c))},update:function(b,c,d,e,f){return a.f.template.update(b,a.f.foreach.vc(c),d,e,f)}};a.j.Ia.foreach=!1;a.g.$.foreach=!0;a.f.hasfocus={init:function(b,c,d){function e(e){b.__ko_hasfocusUpdating=!0;var f=b.ownerDocument;if("activeElement"in f){var h;try{h=f.activeElement}catch(m){h=f.body}e=h===b}f=c();a.j.Ba(f,d,"hasfocus",e,!0);b.__ko_hasfocusLastValue=e;b.__ko_hasfocusUpdating= !1}var f=e.bind(null,!0),h=e.bind(null,!1);a.a.C(b,"focus",f);a.a.C(b,"focusin",f);a.a.C(b,"blur",h);a.a.C(b,"focusout",h)},update:function(b,c){var d=!!a.a.c(c());b.__ko_hasfocusUpdating||b.__ko_hasfocusLastValue===d||(d?b.focus():b.blur(),!d&&b.__ko_hasfocusLastValue&&b.ownerDocument.body.focus(),a.s.I(a.a.Ra,null,[b,d?"focusin":"focusout"]))}};a.j.pa.hasfocus=!0;a.f.hasFocus=a.f.hasfocus;a.j.pa.hasFocus=!0;a.f.html={init:function(){return{controlsDescendantBindings:!0}},update:function(b,c){a.a.Mb(b, -c())}};L("if");L("ifnot",!1,!0);L("with",!0,!1,function(a,c){return a.hc(c)});a.f.let={init:function(b,c,d,e,f){c=f.extend(c);a.Fa(c,b);return{controlsDescendantBindings:!0}}};a.g.$.let=!0;var M={};a.f.options={init:function(b){if("select"!==a.a.K(b))throw Error("options binding applies only to SELECT elements");for(;0h)var g=a.a.h.S(),l=a.a.h.S(),k=function(b){var c=this.activeElement;(c=c&&a.a.h.get(c,l))&& -c(b)},m=function(b,c){var d=b.ownerDocument;a.a.h.get(d,g)||(a.a.h.set(d,g,!0),a.a.C(d,"selectionchange",k));a.a.h.set(b,l,c)};a.f.textInput={init:function(b,c,g){function l(c,d){a.a.C(b,c,d)}function k(){var d=a.a.c(c());if(null===d||d===n)d="";t!==n&&d===t?a.a.setTimeout(k,4):b.value!==d&&(H=d,b.value=d)}function x(){u||(t=b.value,u=a.a.setTimeout(G,4))}function G(){clearTimeout(u);t=u=n;var d=b.value;H!==d&&(H=d,a.j.Aa(c(),g,"textInput",d))}var H=b.value,u,t,v=9==a.a.ca?x:G;h&&l("keypress",G); +c(b)},m=function(b,c){var d=b.ownerDocument;a.a.h.get(d,g)||(a.a.h.set(d,g,!0),a.a.C(d,"selectionchange",k));a.a.h.set(b,l,c)};a.f.textInput={init:function(b,c,g){function l(c,d){a.a.C(b,c,d)}function k(){var d=a.a.c(c());if(null===d||d===n)d="";t!==n&&d===t?a.a.setTimeout(k,4):b.value!==d&&(H=d,b.value=d)}function x(){u||(t=b.value,u=a.a.setTimeout(G,4))}function G(){clearTimeout(u);t=u=n;var d=b.value;H!==d&&(H=d,a.j.Ba(c(),g,"textInput",d))}var H=b.value,u,t,v=9==a.a.ca?x:G;h&&l("keypress",G); 11>h&&l("propertychange",function(a){"value"===a.propertyName&&v(a)});8==h&&(l("keyup",G),l("keydown",G));m&&(m(b,v),l("dragend",x));(!h||9<=h)&&l("input",v);5>e&&"textarea"===a.a.K(b)?(l("keydown",x),l("paste",x),l("cut",x)):11>d?l("keydown",x):4>f&&(l("DOMAutoComplete",G),l("dragdrop",G),l("drop",G));l("change",G);l("blur",G);a.u(k,null,{l:b})}};a.j.pa.textInput=!0;a.f.textinput={preprocess:function(a,b,c){c("textInput",a)}}})();a.f.uniqueName={init:function(b,c){if(c()){var d="ko_unique_"+ ++a.f.uniqueName.Xc; -a.a.Fc(b,d)}}};a.f.uniqueName.Xc=0;a.f.using={init:function(b,c,d,e,f){c=f.createChildContext(c);a.Fa(c,b);return{controlsDescendantBindings:!0}}};a.g.$.using=!0;a.f.value={after:["options","foreach"],init:function(b,c,d){var e=a.a.K(b),f="input"==e;if(!f||"checkbox"!=b.type&&"radio"!=b.type){var h=["change"],g=d.get("valueUpdate"),l=!1,k=null;g&&("string"==typeof g&&(g=[g]),a.a.Ya(h,g),h=a.a.cc(h));var m=function(){a.ja.Kb(function(){k=null;l=!1;var e=c(),f=a.m.H(b);a.j.Aa(e,d,"value",f)})};!a.a.ca|| +a.a.Fc(b,d)}}};a.f.uniqueName.Xc=0;a.f.using={init:function(b,c,d,e,f){c=f.createChildContext(c);a.Ga(c,b);return{controlsDescendantBindings:!0}}};a.g.$.using=!0;a.f.value={after:["options","foreach"],init:function(b,c,d){var e=a.a.K(b),f="input"==e;if(!f||"checkbox"!=b.type&&"radio"!=b.type){var h=["change"],g=d.get("valueUpdate"),l=!1,k=null;g&&("string"==typeof g&&(g=[g]),a.a.Ya(h,g),h=a.a.cc(h));var m=function(){a.ja.Kb(function(){k=null;l=!1;var e=c(),f=a.m.H(b);a.j.Ba(e,d,"value",f)})};!a.a.ca|| !f||"text"!=b.type||"off"==b.autocomplete||b.form&&"off"==b.form.autocomplete||-1!=a.a.A(h,"propertychange")||(a.a.C(b,"propertychange",function(){l=!0}),a.a.C(b,"focus",function(){l=!1}),a.a.C(b,"blur",function(){l&&m()}));a.a.D(h,function(c){var d=m;a.a.Ad(c,"after")&&(d=function(){k=a.m.H(b);a.a.setTimeout(m,0)},c=c.substring(5));a.a.C(b,c,d)});var q;q=f&&"file"==b.type?function(){var d=a.a.c(c());null===d||d===n||""===d?b.value="":m()}:function(){var f=a.a.c(c()),g=a.m.H(b);if(null!==k&&f===k)a.a.setTimeout(q, -0);else if(f!==g)if("select"===e){var l=d.get("valueAllowUnset"),h=function(){a.m.sa(b,f,l);a.j.Aa(c(),d,"value",f)};a.ja.Kb(function(){h();l||f===a.m.H(b)?a.a.setTimeout(h,0):a.s.I(a.a.Ra,null,[b,"change"])})}else a.m.sa(b,f)};a.u(q,null,{l:b})}else a.Wa(b,{checkedValue:c})},update:function(){}};a.j.pa.value=!0;a.f.visible={update:function(b,c){var d=a.a.c(c()),e="none"!=b.style.display;d&&!e?b.style.display="":!d&&e&&(b.style.display="none")}};(function(b){a.f[b]={init:function(c,d,e,f,h){return a.f.event.init.call(this, +0);else if(f!==g)if("select"===e){var l=d.get("valueAllowUnset"),h=function(){a.m.sa(b,f,l);a.j.Ba(c(),d,"value",f)};a.ja.Kb(function(){h();l||f===a.m.H(b)?a.a.setTimeout(h,0):a.s.I(a.a.Ra,null,[b,"change"])})}else a.m.sa(b,f)};a.u(q,null,{l:b})}else a.Wa(b,{checkedValue:c})},update:function(){}};a.j.pa.value=!0;a.f.visible={update:function(b,c){var d=a.a.c(c()),e="none"!=b.style.display;d&&!e?b.style.display="":!d&&e&&(b.style.display="none")}};(function(b){a.f[b]={init:function(c,d,e,f,h){return a.f.event.init.call(this, c,function(){var a={};a[b]=d();return a},e,f,h)}}})("click");a.Y=function(){};a.Y.prototype.renderTemplateSource=function(){throw Error("Override renderTemplateSource");};a.Y.prototype.createJavaScriptEvaluatorBlock=function(){throw Error("Override createJavaScriptEvaluatorBlock");};a.Y.prototype.makeTemplateSource=function(b,c){if("string"==typeof b){c=c||t;var d=c.getElementById(b);if(!d)throw Error("Cannot find template with ID "+b);return new a.w.v(d)}if(1==b.nodeType||8==b.nodeType)return new a.w.ha(b); throw Error("Unknown template type: "+b);};a.Y.prototype.renderTemplate=function(a,c,d,e){a=this.makeTemplateSource(a,e);return this.renderTemplateSource(a,c,d,e)};a.Y.prototype.isTemplateRewritten=function(a,c){return!1===this.allowTemplateRewriting?!0:this.makeTemplateSource(a,c).data("isRewritten")};a.Y.prototype.rewriteTemplate=function(a,c,d){a=this.makeTemplateSource(a,d);c=c(a.text());a.text(c);a.data("isRewritten",!0)};a.b("templateEngine",a.Y);a.Qb=function(){function b(b,c,d,g){b=a.j.Ib(b); -for(var l=a.j.Ha,k=0;k]*))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi, +for(var l=a.j.Ia,k=0;k]*))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi, d=/\x3c!--\s*ko\b\s*([\s\S]*?)\s*--\x3e/g;return{cd:function(b,c,d){c.isTemplateRewritten(b,d)||c.rewriteTemplate(b,function(b){return a.Qb.rd(b,c)},d)},rd:function(a,f){return a.replace(c,function(a,c,d,e,m){return b(m,c,d,f)}).replace(d,function(a,c){return b(c,"\x3c!-- ko --\x3e","#comment",f)})},Tc:function(b,c){return a.W.Gb(function(d,g){var l=d.nextSibling;l&&l.nodeName.toLowerCase()===c&&a.Wa(l,b,g)})}}}();a.b("__tr_ambtns",a.Qb.Tc);(function(){a.w={};a.w.v=function(b){if(this.v=b){var c= a.a.K(b);this.qb="script"===c?1:"textarea"===c?2:"template"==c&&b.content&&11===b.content.nodeType?3:4}};a.w.v.prototype.text=function(){var b=1===this.qb?"text":2===this.qb?"value":"innerHTML";if(0==arguments.length)return this.v[b];var c=arguments[0];"innerHTML"===b?a.a.Mb(this.v,c):this.v[b]=c};var b=a.a.h.S()+"_";a.w.v.prototype.data=function(c){if(1===arguments.length)return a.a.h.get(this.v,b+c);a.a.h.set(this.v,b+c,arguments[1])};var c=a.a.h.S();a.w.v.prototype.nodes=function(){var b=this.v; if(0==arguments.length)return(a.a.h.get(b,c)||{}).yb||(3===this.qb?b.content:4===this.qb?b:n);a.a.h.set(b,c,{yb:arguments[0]})};a.w.ha=function(a){this.v=a};a.w.ha.prototype=new a.w.v;a.w.ha.prototype.constructor=a.w.ha;a.w.ha.prototype.text=function(){if(0==arguments.length){var b=a.a.h.get(this.v,c)||{};b.Rb===n&&b.yb&&(b.Rb=b.yb.innerHTML);return b.Rb}a.a.h.set(this.v,c,{Rb:arguments[0]})};a.b("templateSources",a.w);a.b("templateSources.domElement",a.w.v);a.b("templateSources.anonymousTemplate", a.w.ha)})();(function(){function b(b,c,d){var e;for(c=a.g.nextSibling(c);b&&(e=b)!==c;)b=a.g.nextSibling(e),d(e,b)}function c(c,d){if(c.length){var e=c[0],f=c[c.length-1],g=e.parentNode,h=a.ba.instance,n=h.preprocessNode;if(n){b(e,f,function(a,b){var c=a.previousSibling,d=n.call(h,a);d&&(a===e&&(e=d[0]||b),a===f&&(f=d[d.length-1]||c))});c.length=0;if(!e)return;e===f?c.push(e):(c.push(e,f),a.a.Ma(c,g))}b(e,f,function(b){1!==b.nodeType&&8!==b.nodeType||a.ac(d,b)});b(e,f,function(b){1!==b.nodeType&& 8!==b.nodeType||a.W.Mc(b,[d])});a.a.Ma(c,g)}}function d(a){return a.nodeType?a:0a.a.ca?0:b.nodes)? -b.nodes():null)return a.a.fa(c.cloneNode(!0).childNodes);b=b.text();return a.a.ya(b,e)};a.X.Ca=new a.X;a.Nb(a.X.Ca);a.b("nativeTemplateEngine",a.X);(function(){a.Pa=function(){var a=this.nd=function(){if(!w||!w.tmpl)return 0;try{if(0<=w.tmpl.tag.tmpl.open.toString().indexOf("__"))return 2}catch(a){}return 1}();this.renderTemplateSource=function(b,e,f,h){h=h||t;f=f||{};if(2>a)throw Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");var g=b.data("precompiled"); +b.nodes():null)return a.a.fa(c.cloneNode(!0).childNodes);b=b.text();return a.a.za(b,e)};a.X.Da=new a.X;a.Nb(a.X.Da);a.b("nativeTemplateEngine",a.X);(function(){a.Pa=function(){var a=this.nd=function(){if(!w||!w.tmpl)return 0;try{if(0<=w.tmpl.tag.tmpl.open.toString().indexOf("__"))return 2}catch(a){}return 1}();this.renderTemplateSource=function(b,e,f,h){h=h||t;f=f||{};if(2>a)throw Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");var g=b.data("precompiled"); g||(g=b.text()||"",g=w.template(null,"{{ko_with $item.koBindingContext}}"+g+"{{/ko_with}}"),b.data("precompiled",g));b=[e.$data];e=w.extend({koBindingContext:e},f.templateOptions);e=w.tmpl(g,b,e);e.appendTo(h.createElement("div"));w.fragments={};return e};this.createJavaScriptEvaluatorBlock=function(a){return"{{ko_code ((function() { return "+a+" })()) }}"};this.addTemplate=function(a,b){t.write("