20 | Microsoft Graph Notifications Sample
21 | ├── .github ├── CODEOWNERS ├── dependabot.yml ├── workflows │ ├── dotnetcore.yml │ ├── auto-merge-dependabot.yml │ └── codeql.yml ├── ISSUE_TEMPLATE │ ├── ask-a-question.md │ └── bug_report.md └── policies │ └── resourceManagement.yml ├── images ├── add-access-policy.png ├── copy-secret-value.png ├── create-a-resource.png ├── ngrok-https-url.png ├── register-an-app.png ├── user-inbox-notifications.png ├── teams-channel-notifications.png └── remove-configured-permission.png ├── src └── GraphWebhooks │ ├── wwwroot │ ├── css │ │ └── site.css │ ├── img │ │ └── g-raph.png │ ├── js │ │ └── site.js │ └── lib │ │ ├── jquery-validation-unobtrusive │ │ ├── LICENSE.txt │ │ ├── jquery.validate.unobtrusive.min.js │ │ └── jquery.validate.unobtrusive.js │ │ ├── jquery-validation │ │ ├── LICENSE.md │ │ └── dist │ │ │ └── additional-methods.min.js │ │ ├── bootstrap │ │ ├── LICENSE │ │ └── dist │ │ │ └── css │ │ │ ├── bootstrap-reboot.min.css │ │ │ ├── bootstrap-reboot.rtl.min.css │ │ │ ├── bootstrap-reboot.rtl.css │ │ │ └── bootstrap-reboot.css │ │ └── jquery │ │ └── LICENSE.txt │ ├── Views │ ├── _ViewStart.cshtml │ ├── _ViewImports.cshtml │ ├── Shared │ │ ├── _ValidationScriptsPartial.cshtml │ │ ├── _AlertPartial.cshtml │ │ ├── Error.cshtml │ │ └── _Layout.cshtml │ ├── Home │ │ └── Index.cshtml │ └── Watch │ │ ├── AppOnly.cshtml │ │ └── Delegated.cshtml │ ├── appsettings.Development.json │ ├── Areas │ └── MicrosoftIdentity │ │ └── Pages │ │ └── Account │ │ ├── SignedOut.cshtml │ │ └── SignedOut.cshtml.cs │ ├── SignalR │ └── NotificationHub.cs │ ├── Models │ ├── ClientNotification.cs │ ├── ErrorViewModel.cs │ └── SubscriptionRecord.cs │ ├── stylecop.json │ ├── Graph │ ├── GraphClaimTypes.cs │ └── GraphClaimsPrincipalExtensions.cs │ ├── appsettings.json │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Controllers │ ├── HomeController.cs │ ├── LifecycleController.cs │ ├── ListenController.cs │ └── WatchController.cs │ ├── GraphWebhooks.csproj │ ├── Services │ ├── SubscriptionStore.cs │ └── CertificateService.cs │ ├── Alerts │ ├── WithAlertResult.cs │ └── AlertExtensions.cs │ └── Startup.cs ├── .editorconfig ├── .markdownlint.json ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── aspnetcore-webhooks-sample.sln ├── KEYVAULT.md ├── .gitattributes ├── SECURITY.md ├── TROUBLESHOOTING.md ├── .gitignore └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @microsoftgraph/msgraph-devx-samples-write -------------------------------------------------------------------------------- /images/add-access-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/aspnetcore-webhooks-sample/HEAD/images/add-access-policy.png -------------------------------------------------------------------------------- /images/copy-secret-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/aspnetcore-webhooks-sample/HEAD/images/copy-secret-value.png -------------------------------------------------------------------------------- /images/create-a-resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/aspnetcore-webhooks-sample/HEAD/images/create-a-resource.png -------------------------------------------------------------------------------- /images/ngrok-https-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/aspnetcore-webhooks-sample/HEAD/images/ngrok-https-url.png -------------------------------------------------------------------------------- /images/register-an-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/aspnetcore-webhooks-sample/HEAD/images/register-an-app.png -------------------------------------------------------------------------------- /images/user-inbox-notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/aspnetcore-webhooks-sample/HEAD/images/user-inbox-notifications.png -------------------------------------------------------------------------------- /images/teams-channel-notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/aspnetcore-webhooks-sample/HEAD/images/teams-channel-notifications.png -------------------------------------------------------------------------------- /src/GraphWebhooks/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | .wrapped-pre { 2 | word-wrap: break-word; 3 | word-break: break-all; 4 | white-space: pre-wrap; 5 | } 6 | -------------------------------------------------------------------------------- /images/remove-configured-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/aspnetcore-webhooks-sample/HEAD/images/remove-configured-permission.png -------------------------------------------------------------------------------- /src/GraphWebhooks/wwwroot/img/g-raph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/aspnetcore-webhooks-sample/HEAD/src/GraphWebhooks/wwwroot/img/g-raph.png -------------------------------------------------------------------------------- /src/GraphWebhooks/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | 3 | 4 | @{ 5 | Layout = "_Layout"; 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | insert_final_newline = true 5 | 6 | [*.{cs,cshtml}] 7 | indent_size = 4 8 | dotnet_diagnostic.SA1101.severity = silent 9 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/master/schema/markdownlint-config-schema.json", 4 | "no-trailing-punctuation": false, 5 | "MD013": false 6 | } 7 | -------------------------------------------------------------------------------- /src/GraphWebhooks/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | 3 | 4 | @using GraphWebhooks 5 | @using GraphWebhooks.Models 6 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 7 | -------------------------------------------------------------------------------- /src/GraphWebhooks/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/GraphWebhooks/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your JavaScript code. 5 | -------------------------------------------------------------------------------- /src/GraphWebhooks/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: /src/GraphWebhooks/ 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /src/GraphWebhooks/Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 4 | 5 | @model GraphWebhooks.Areas.MicrosoftIdentity.Pages.Account.SignedOut 6 | @{ 7 | ViewData["Title"] = "Signed out"; 8 | } 9 | 10 |
12 | You have successfully signed out. 13 |
14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.ignoreWords": [ 3 | "signin-oidc" 4 | ], 5 | "cSpell.words": [ 6 | "apponly", 7 | "appsettings", 8 | "Decryptable", 9 | "Decryptor", 10 | "Encryptable", 11 | "entra", 12 | "HMACSHA", 13 | "HSTS", 14 | "mailfolders", 15 | "Msal", 16 | "Multitenant", 17 | "Oaep", 18 | "PKCS", 19 | "signin", 20 | "signout", 21 | "SYSLIB", 22 | "utid" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/GraphWebhooks/SignalR/NotificationHub.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | using Microsoft.AspNetCore.SignalR; 5 | 6 | namespace GraphWebhooks.SignalR; 7 | 8 | ///@message
16 | } 17 | else 18 | { 19 |@message
20 |@debugInfo
21 | }
22 |
23 |
15 | Request ID: @Model.RequestId
16 |
21 | Swapping to Development environment will display more detailed information about the error that occurred. 22 |
23 |24 | The Development environment shouldn't be enabled for deployed applications. 25 | It can result in displaying sensitive information from exceptions to end users. 26 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 27 | and restarting the app. 28 |
29 | -------------------------------------------------------------------------------- /src/GraphWebhooks/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | using System.Diagnostics; 5 | using GraphWebhooks.Models; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace GraphWebhooks.Controllers; 10 | 11 | ///Choose one of the options below to create a subscription
11 |Choose this option to sign in as a user and receive notifications when items are created in the user's Exchange Online mailbox
17 | Sign in and subscribe 18 |Choose this option to have the application receive notifications when messages are sent to any Teams channel
24 | Subscribe 25 |
20 | Microsoft Graph Notifications Sample
21 | Notifications should appear below when new messages sent to any Teams channel in your organization.
19 |
25 | @jsonSubscription
26 |
27 | | Sender | 33 |Message | 34 |
|---|
Notifications should appear below when new messages are delivered to @User.GetUserGraphDisplayName()'s inbox.
19 |
26 | @jsonSubscription
27 |
28 | | Subject | 34 |ID | 35 |
|---|
9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/)},"Please specify a valid mobile number"),a.validator.addMethod("netmask",function(a,b){return this.optional(b)||/^(254|252|248|240|224|192|128)\.0\.0\.0|255\.(254|252|248|240|224|192|128|0)\.0\.0|255\.255\.(254|252|248|240|224|192|128|0)\.0|255\.255\.255\.(254|252|248|240|224|192|128|0)/i.test(a)},"Please enter a valid netmask."),a.validator.addMethod("nieES",function(a,b){"use strict";if(this.optional(b))return!0;var c,d=new RegExp(/^[MXYZ]{1}[0-9]{7,8}[TRWAGMYFPDXBNJZSQVHLCKET]{1}$/gi),e="TRWAGMYFPDXBNJZSQVHLCKET",f=a.substr(a.length-1).toUpperCase();return a=a.toString().toUpperCase(),!(a.length>10||a.length<9||!d.test(a))&&(a=a.replace(/^[X]/,"0").replace(/^[Y]/,"1").replace(/^[Z]/,"2"),c=9===a.length?a.substr(0,8):a.substr(0,9),e.charAt(parseInt(c,10)%23)===f)},"Please specify a valid NIE number."),a.validator.addMethod("nifES",function(a,b){"use strict";return!!this.optional(b)||(a=a.toUpperCase(),!!a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")&&(/^[0-9]{8}[A-Z]{1}$/.test(a)?"TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,0)%23)===a.charAt(8):!!/^[KLM]{1}/.test(a)&&a[8]==="TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,1)%23)))},"Please specify a valid NIF number."),a.validator.addMethod("nipPL",function(a){"use strict";if(a=a.replace(/[^0-9]/g,""),10!==a.length)return!1;for(var b=[6,5,7,2,3,4,5,6,7],c=0,d=0;d<9;d++)c+=b[d]*a[d];var e=c%11,f=10===e?0:e;return f===parseInt(a[9],10)},"Please specify a valid NIP number."),a.validator.addMethod("notEqualTo",function(b,c,d){return this.optional(c)||!a.validator.methods.equalTo.call(this,b,c,d)},"Please enter a different value, values must not be the same."),a.validator.addMethod("nowhitespace",function(a,b){return this.optional(b)||/^\S+$/i.test(a)},"No white space please"),a.validator.addMethod("pattern",function(a,b,c){return!!this.optional(b)||("string"==typeof c&&(c=new RegExp("^(?:"+c+")$")),c.test(a))},"Invalid format."),a.validator.addMethod("phoneNL",function(a,b){return this.optional(b)||/^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test(a)},"Please specify a valid phone number."),a.validator.addMethod("phonesUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/)},"Please specify a valid uk phone number"),a.validator.addMethod("phoneUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/)},"Please specify a valid phone number"),a.validator.addMethod("phoneUS",function(a,b){return a=a.replace(/\s+/g,""),this.optional(b)||a.length>9&&a.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/)},"Please specify a valid phone number"),a.validator.addMethod("postalcodeBR",function(a,b){return this.optional(b)||/^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test(a)},"Informe um CEP válido."),a.validator.addMethod("postalCodeCA",function(a,b){return this.optional(b)||/^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ] *\d[ABCEGHJKLMNPRSTVWXYZ]\d$/i.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeIT",function(a,b){return this.optional(b)||/^\d{5}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeNL",function(a,b){return this.optional(b)||/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postcodeUK",function(a,b){return this.optional(b)||/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(a)},"Please specify a valid UK postcode"),a.validator.addMethod("require_from_group",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_req_grp")?f.data("valid_req_grp"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length>=d[0];return f.data("valid_req_grp",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),h},a.validator.format("Please fill at least {0} of these fields.")),a.validator.addMethod("skip_or_fill_minimum",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_skip")?f.data("valid_skip"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length,i=0===h||h>=d[0];return f.data("valid_skip",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),i},a.validator.format("Please either skip these fields or fill at least {0} of them.")),a.validator.addMethod("stateUS",function(a,b,c){var d,e="undefined"==typeof c,f=!e&&"undefined"!=typeof c.caseSensitive&&c.caseSensitive,g=!e&&"undefined"!=typeof c.includeTerritories&&c.includeTerritories,h=!e&&"undefined"!=typeof c.includeMilitary&&c.includeMilitary;return d=g||h?g&&h?"^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":g?"^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":"^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$":"^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$",d=f?new RegExp(d):new RegExp(d,"i"),this.optional(b)||d.test(a)},"Please specify a valid state"),a.validator.addMethod("strippedminlength",function(b,c,d){return a(b).text().length>=d},a.validator.format("Please enter at least {0} characters")),a.validator.addMethod("time",function(a,b){return this.optional(b)||/^([01]\d|2[0-3]|[0-9])(:[0-5]\d){1,2}$/.test(a)},"Please enter a valid time, between 00:00 and 23:59"),a.validator.addMethod("time12h",function(a,b){return this.optional(b)||/^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(a)},"Please enter a valid time in 12-hour am/pm format"),a.validator.addMethod("url2",function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},a.validator.messages.url),a.validator.addMethod("vinUS",function(a){if(17!==a.length)return!1;var b,c,d,e,f,g,h=["A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z"],i=[1,2,3,4,5,6,7,8,1,2,3,4,5,7,9,2,3,4,5,6,7,8,9],j=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2],k=0;for(b=0;b<17;b++){if(e=j[b],d=a.slice(b,b+1),8===b&&(g=d),isNaN(d)){for(c=0;c?@\[\\\]^`{|}~])/g, "\\$1"); 39 | } 40 | 41 | function getModelPrefix(fieldName) { 42 | return fieldName.substr(0, fieldName.lastIndexOf(".") + 1); 43 | } 44 | 45 | function appendModelPrefix(value, prefix) { 46 | if (value.indexOf("*.") === 0) { 47 | value = value.replace("*.", prefix); 48 | } 49 | return value; 50 | } 51 | 52 | function onError(error, inputElement) { // 'this' is the form element 53 | var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"), 54 | replaceAttrValue = container.attr("data-valmsg-replace"), 55 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null; 56 | 57 | container.removeClass("field-validation-valid").addClass("field-validation-error"); 58 | error.data("unobtrusiveContainer", container); 59 | 60 | if (replace) { 61 | container.empty(); 62 | error.removeClass("input-validation-error").appendTo(container); 63 | } 64 | else { 65 | error.hide(); 66 | } 67 | } 68 | 69 | function onErrors(event, validator) { // 'this' is the form element 70 | var container = $(this).find("[data-valmsg-summary=true]"), 71 | list = container.find("ul"); 72 | 73 | if (list && list.length && validator.errorList.length) { 74 | list.empty(); 75 | container.addClass("validation-summary-errors").removeClass("validation-summary-valid"); 76 | 77 | $.each(validator.errorList, function () { 78 | $("").html(this.message).appendTo(list); 79 | }); 80 | } 81 | } 82 | 83 | function onSuccess(error) { // 'this' is the form element 84 | var container = error.data("unobtrusiveContainer"); 85 | 86 | if (container) { 87 | var replaceAttrValue = container.attr("data-valmsg-replace"), 88 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null; 89 | 90 | container.addClass("field-validation-valid").removeClass("field-validation-error"); 91 | error.removeData("unobtrusiveContainer"); 92 | 93 | if (replace) { 94 | container.empty(); 95 | } 96 | } 97 | } 98 | 99 | function onReset(event) { // 'this' is the form element 100 | var $form = $(this), 101 | key = '__jquery_unobtrusive_validation_form_reset'; 102 | if ($form.data(key)) { 103 | return; 104 | } 105 | // Set a flag that indicates we're currently resetting the form. 106 | $form.data(key, true); 107 | try { 108 | $form.data("validator").resetForm(); 109 | } finally { 110 | $form.removeData(key); 111 | } 112 | 113 | $form.find(".validation-summary-errors") 114 | .addClass("validation-summary-valid") 115 | .removeClass("validation-summary-errors"); 116 | $form.find(".field-validation-error") 117 | .addClass("field-validation-valid") 118 | .removeClass("field-validation-error") 119 | .removeData("unobtrusiveContainer") 120 | .find(">*") // If we were using valmsg-replace, get the underlying error 121 | .removeData("unobtrusiveContainer"); 122 | } 123 | 124 | function validationInfo(form) { 125 | var $form = $(form), 126 | result = $form.data(data_validation), 127 | onResetProxy = $.proxy(onReset, form), 128 | defaultOptions = $jQval.unobtrusive.options || {}, 129 | execInContext = function (name, args) { 130 | var func = defaultOptions[name]; 131 | func && $.isFunction(func) && func.apply(form, args); 132 | }; 133 | 134 | if (!result) { 135 | result = { 136 | options: { // options structure passed to jQuery Validate's validate() method 137 | errorClass: defaultOptions.errorClass || "input-validation-error", 138 | errorElement: defaultOptions.errorElement || "span", 139 | errorPlacement: function () { 140 | onError.apply(form, arguments); 141 | execInContext("errorPlacement", arguments); 142 | }, 143 | invalidHandler: function () { 144 | onErrors.apply(form, arguments); 145 | execInContext("invalidHandler", arguments); 146 | }, 147 | messages: {}, 148 | rules: {}, 149 | success: function () { 150 | onSuccess.apply(form, arguments); 151 | execInContext("success", arguments); 152 | } 153 | }, 154 | attachValidation: function () { 155 | $form 156 | .off("reset." + data_validation, onResetProxy) 157 | .on("reset." + data_validation, onResetProxy) 158 | .validate(this.options); 159 | }, 160 | validate: function () { // a validation function that is called by unobtrusive Ajax 161 | $form.validate(); 162 | return $form.valid(); 163 | } 164 | }; 165 | $form.data(data_validation, result); 166 | } 167 | 168 | return result; 169 | } 170 | 171 | $jQval.unobtrusive = { 172 | adapters: [], 173 | 174 | parseElement: function (element, skipAttach) { 175 | /// 176 | /// Parses a single HTML element for unobtrusive validation attributes. 177 | /// 178 | /// The HTML element to be parsed. 179 | /// [Optional] true to skip attaching the 180 | /// validation to the form. If parsing just this single element, you should specify true. 181 | /// If parsing several elements, you should specify false, and manually attach the validation 182 | /// to the form when you are finished. The default is false. 183 | var $element = $(element), 184 | form = $element.parents("form")[0], 185 | valInfo, rules, messages; 186 | 187 | if (!form) { // Cannot do client-side validation without a form 188 | return; 189 | } 190 | 191 | valInfo = validationInfo(form); 192 | valInfo.options.rules[element.name] = rules = {}; 193 | valInfo.options.messages[element.name] = messages = {}; 194 | 195 | $.each(this.adapters, function () { 196 | var prefix = "data-val-" + this.name, 197 | message = $element.attr(prefix), 198 | paramValues = {}; 199 | 200 | if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy) 201 | prefix += "-"; 202 | 203 | $.each(this.params, function () { 204 | paramValues[this] = $element.attr(prefix + this); 205 | }); 206 | 207 | this.adapt({ 208 | element: element, 209 | form: form, 210 | message: message, 211 | params: paramValues, 212 | rules: rules, 213 | messages: messages 214 | }); 215 | } 216 | }); 217 | 218 | $.extend(rules, { "__dummy__": true }); 219 | 220 | if (!skipAttach) { 221 | valInfo.attachValidation(); 222 | } 223 | }, 224 | 225 | parse: function (selector) { 226 | ///227 | /// Parses all the HTML elements in the specified selector. It looks for input elements decorated 228 | /// with the [data-val=true] attribute value and enables validation according to the data-val-* 229 | /// attribute values. 230 | /// 231 | /// Any valid jQuery selector. 232 | 233 | // $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one 234 | // element with data-val=true 235 | var $selector = $(selector), 236 | $forms = $selector.parents() 237 | .addBack() 238 | .filter("form") 239 | .add($selector.find("form")) 240 | .has("[data-val=true]"); 241 | 242 | $selector.find("[data-val=true]").each(function () { 243 | $jQval.unobtrusive.parseElement(this, true); 244 | }); 245 | 246 | $forms.each(function () { 247 | var info = validationInfo(this); 248 | if (info) { 249 | info.attachValidation(); 250 | } 251 | }); 252 | } 253 | }; 254 | 255 | adapters = $jQval.unobtrusive.adapters; 256 | 257 | adapters.add = function (adapterName, params, fn) { 258 | ///Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation. 259 | /// The name of the adapter to be added. This matches the name used 260 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 261 | /// [Optional] An array of parameter names (strings) that will 262 | /// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and 263 | /// mmmm is the parameter name). 264 | /// The function to call, which adapts the values from the HTML 265 | /// attributes into jQuery Validate rules and/or messages. 266 | ///267 | if (!fn) { // Called with no params, just a function 268 | fn = params; 269 | params = []; 270 | } 271 | this.push({ name: adapterName, params: params, adapt: fn }); 272 | return this; 273 | }; 274 | 275 | adapters.addBool = function (adapterName, ruleName) { 276 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 277 | /// the jQuery Validate validation rule has no parameter values. 278 | /// The name of the adapter to be added. This matches the name used 279 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 280 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 281 | /// of adapterName will be used instead. 282 | ///283 | return this.add(adapterName, function (options) { 284 | setValidationValues(options, ruleName || adapterName, true); 285 | }); 286 | }; 287 | 288 | adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) { 289 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 290 | /// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and 291 | /// one for min-and-max). The HTML parameters are expected to be named -min and -max. 292 | /// The name of the adapter to be added. This matches the name used 293 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 294 | /// The name of the jQuery Validate rule to be used when you only 295 | /// have a minimum value. 296 | /// The name of the jQuery Validate rule to be used when you only 297 | /// have a maximum value. 298 | /// The name of the jQuery Validate rule to be used when you 299 | /// have both a minimum and maximum value. 300 | /// [Optional] The name of the HTML attribute that 301 | /// contains the minimum value. The default is "min". 302 | /// [Optional] The name of the HTML attribute that 303 | /// contains the maximum value. The default is "max". 304 | ///305 | return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) { 306 | var min = options.params.min, 307 | max = options.params.max; 308 | 309 | if (min && max) { 310 | setValidationValues(options, minMaxRuleName, [min, max]); 311 | } 312 | else if (min) { 313 | setValidationValues(options, minRuleName, min); 314 | } 315 | else if (max) { 316 | setValidationValues(options, maxRuleName, max); 317 | } 318 | }); 319 | }; 320 | 321 | adapters.addSingleVal = function (adapterName, attribute, ruleName) { 322 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 323 | /// the jQuery Validate validation rule has a single value. 324 | /// The name of the adapter to be added. This matches the name used 325 | /// in the data-val-nnnn HTML attribute(where nnnn is the adapter name). 326 | /// [Optional] The name of the HTML attribute that contains the value. 327 | /// The default is "val". 328 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 329 | /// of adapterName will be used instead. 330 | ///331 | return this.add(adapterName, [attribute || "val"], function (options) { 332 | setValidationValues(options, ruleName || adapterName, options.params[attribute]); 333 | }); 334 | }; 335 | 336 | $jQval.addMethod("__dummy__", function (value, element, params) { 337 | return true; 338 | }); 339 | 340 | $jQval.addMethod("regex", function (value, element, params) { 341 | var match; 342 | if (this.optional(element)) { 343 | return true; 344 | } 345 | 346 | match = new RegExp(params).exec(value); 347 | return (match && (match.index === 0) && (match[0].length === value.length)); 348 | }); 349 | 350 | $jQval.addMethod("nonalphamin", function (value, element, nonalphamin) { 351 | var match; 352 | if (nonalphamin) { 353 | match = value.match(/\W/g); 354 | match = match && match.length >= nonalphamin; 355 | } 356 | return match; 357 | }); 358 | 359 | if ($jQval.methods.extension) { 360 | adapters.addSingleVal("accept", "mimtype"); 361 | adapters.addSingleVal("extension", "extension"); 362 | } else { 363 | // for backward compatibility, when the 'extension' validation method does not exist, such as with versions 364 | // of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for 365 | // validating the extension, and ignore mime-type validations as they are not supported. 366 | adapters.addSingleVal("extension", "extension", "accept"); 367 | } 368 | 369 | adapters.addSingleVal("regex", "pattern"); 370 | adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"); 371 | adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range"); 372 | adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength"); 373 | adapters.add("equalto", ["other"], function (options) { 374 | var prefix = getModelPrefix(options.element.name), 375 | other = options.params.other, 376 | fullOtherName = appendModelPrefix(other, prefix), 377 | element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0]; 378 | 379 | setValidationValues(options, "equalTo", element); 380 | }); 381 | adapters.add("required", function (options) { 382 | // jQuery Validate equates "required" with "mandatory" for checkbox elements 383 | if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") { 384 | setValidationValues(options, "required", true); 385 | } 386 | }); 387 | adapters.add("remote", ["url", "type", "additionalfields"], function (options) { 388 | var value = { 389 | url: options.params.url, 390 | type: options.params.type || "GET", 391 | data: {} 392 | }, 393 | prefix = getModelPrefix(options.element.name); 394 | 395 | $.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) { 396 | var paramName = appendModelPrefix(fieldName, prefix); 397 | value.data[paramName] = function () { 398 | var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']"); 399 | // For checkboxes and radio buttons, only pick up values from checked fields. 400 | if (field.is(":checkbox")) { 401 | return field.filter(":checked").val() || field.filter(":hidden").val() || ''; 402 | } 403 | else if (field.is(":radio")) { 404 | return field.filter(":checked").val() || ''; 405 | } 406 | return field.val(); 407 | }; 408 | }); 409 | 410 | setValidationValues(options, "remote", value); 411 | }); 412 | adapters.add("password", ["min", "nonalphamin", "regex"], function (options) { 413 | if (options.params.min) { 414 | setValidationValues(options, "minlength", options.params.min); 415 | } 416 | if (options.params.nonalphamin) { 417 | setValidationValues(options, "nonalphamin", options.params.nonalphamin); 418 | } 419 | if (options.params.regex) { 420 | setValidationValues(options, "regex", options.params.regex); 421 | } 422 | }); 423 | adapters.add("fileextensions", ["extensions"], function (options) { 424 | setValidationValues(options, "extension", options.params.extensions); 425 | }); 426 | 427 | $(function () { 428 | $jQval.unobtrusive.parse(document); 429 | }); 430 | 431 | return $jQval.unobtrusive; 432 | })); 433 | --------------------------------------------------------------------------------