diff --git a/.gitignore b/.gitignore index fe8940c0e..5be95a706 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ target/ .nuget/ .vs/ .build/ +.dotnet/ # User-specific files *.suo diff --git a/OpenIddict.Samples.sln b/OpenIddict.Samples.sln index de0e8b792..35bcb7d02 100644 --- a/OpenIddict.Samples.sln +++ b/OpenIddict.Samples.sln @@ -86,7 +86,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Weytta", "Weytta", "{374481 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weytta.Client", "samples\Weytta\Weytta.Client\Weytta.Client.csproj", "{20C5B39E-CEBA-4CCD-979A-5499B4030043}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Weytta.Server", "samples\Weytta\Weytta.Server\Weytta.Server.csproj", "{02D5D1C5-675D-482D-9F74-F06FC522D2CC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weytta.Server", "samples\Weytta\Weytta.Server\Weytta.Server.csproj", "{02D5D1C5-675D-482D-9F74-F06FC522D2CC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rentor", "Rentor", "{F549911A-B043-453F-938C-06882ADEA987}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rentor.Client", "samples\Rentor\Rentor.Client\Rentor.Client.csproj", "{A55B0597-7E77-4FBA-8C9E-48842EC3B48A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rentor.Server", "samples\Rentor\Rentor.Server\Rentor.Server.csproj", "{373EB5EA-375B-4343-A3E0-FD5609795F90}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -170,6 +176,14 @@ Global {02D5D1C5-675D-482D-9F74-F06FC522D2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {02D5D1C5-675D-482D-9F74-F06FC522D2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {02D5D1C5-675D-482D-9F74-F06FC522D2CC}.Release|Any CPU.Build.0 = Release|Any CPU + {A55B0597-7E77-4FBA-8C9E-48842EC3B48A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A55B0597-7E77-4FBA-8C9E-48842EC3B48A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A55B0597-7E77-4FBA-8C9E-48842EC3B48A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A55B0597-7E77-4FBA-8C9E-48842EC3B48A}.Release|Any CPU.Build.0 = Release|Any CPU + {373EB5EA-375B-4343-A3E0-FD5609795F90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {373EB5EA-375B-4343-A3E0-FD5609795F90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {373EB5EA-375B-4343-A3E0-FD5609795F90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {373EB5EA-375B-4343-A3E0-FD5609795F90}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -202,6 +216,9 @@ Global {37448199-E002-4FA0-A712-B6DB22DA98EC} = {8B467944-153B-4C90-BAB1-8F1B34C3075A} {20C5B39E-CEBA-4CCD-979A-5499B4030043} = {37448199-E002-4FA0-A712-B6DB22DA98EC} {02D5D1C5-675D-482D-9F74-F06FC522D2CC} = {37448199-E002-4FA0-A712-B6DB22DA98EC} + {F549911A-B043-453F-938C-06882ADEA987} = {8B467944-153B-4C90-BAB1-8F1B34C3075A} + {A55B0597-7E77-4FBA-8C9E-48842EC3B48A} = {F549911A-B043-453F-938C-06882ADEA987} + {373EB5EA-375B-4343-A3E0-FD5609795F90} = {F549911A-B043-453F-938C-06882ADEA987} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F3ECDD26-F40D-4AB4-BC48-8DF143F98FAE} diff --git a/samples/Rentor/Rentor.Client/Controllers/AuthenticationController.cs b/samples/Rentor/Rentor.Client/Controllers/AuthenticationController.cs new file mode 100644 index 000000000..da2981cdd --- /dev/null +++ b/samples/Rentor/Rentor.Client/Controllers/AuthenticationController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Mvc; + +namespace Rentor.Client.Controllers +{ + public class AuthenticationController : Controller + { + [HttpGet("~/login")] + public ActionResult LogIn() + { + return Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme); + } + + [HttpGet("~/logout"), HttpPost("~/logout")] + public ActionResult LogOut() + { + // is redirected from the identity provider after a successful authorization flow and + // to redirect the user agent to the identity provider to sign out. + return SignOut(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme); + } + } +} \ No newline at end of file diff --git a/samples/Rentor/Rentor.Client/Controllers/HomeController.cs b/samples/Rentor/Rentor.Client/Controllers/HomeController.cs new file mode 100644 index 000000000..0b2a28420 --- /dev/null +++ b/samples/Rentor/Rentor.Client/Controllers/HomeController.cs @@ -0,0 +1,45 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Rentor.Client.Controllers +{ + public class HomeController : Controller + { + private readonly IHttpClientFactory _httpClientFactory; + + public HomeController(IHttpClientFactory httpClientFactory) + => _httpClientFactory = httpClientFactory; + + [HttpGet("~/")] + public ActionResult Index() => View("Home"); + + [Authorize, HttpPost("~/")] + public async Task Index(CancellationToken cancellationToken) + { + var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectParameterNames.AccessToken); + if (string.IsNullOrEmpty(token)) + { + throw new InvalidOperationException("The access token cannot be found in the authentication ticket. " + + "Make sure that SaveTokens is set to true in the OIDC options."); + } + + using var client = _httpClientFactory.CreateClient(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:44213/api/message"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + + using var response = await client.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + + return View("Home", model: await response.Content.ReadAsStringAsync()); + } + } +} \ No newline at end of file diff --git a/samples/Rentor/Rentor.Client/Program.cs b/samples/Rentor/Rentor.Client/Program.cs new file mode 100644 index 000000000..20a107d80 --- /dev/null +++ b/samples/Rentor/Rentor.Client/Program.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Rentor.Client +{ + public static class Program + { + public static void Main(string[] args) => + CreateHostBuilder(args).Build().Run(); + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(builder => builder.UseStartup()); + } +} diff --git a/samples/Rentor/Rentor.Client/Properties/launchSettings.json b/samples/Rentor/Rentor.Client/Properties/launchSettings.json new file mode 100644 index 000000000..66c365efe --- /dev/null +++ b/samples/Rentor/Rentor.Client/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53507/", + "sslPort": 44238 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Rentor.Client": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:44238/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/Rentor/Rentor.Client/Rentor.Client.csproj b/samples/Rentor/Rentor.Client/Rentor.Client.csproj new file mode 100644 index 000000000..9a2108039 --- /dev/null +++ b/samples/Rentor/Rentor.Client/Rentor.Client.csproj @@ -0,0 +1,11 @@ + + + + net5.0 + + + + + + + diff --git a/samples/Rentor/Rentor.Client/Startup.cs b/samples/Rentor/Rentor.Client/Startup.cs new file mode 100644 index 000000000..1680a53ab --- /dev/null +++ b/samples/Rentor/Rentor.Client/Startup.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Rentor.Client +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + + .AddCookie(options => + { + options.LoginPath = "/login"; + }) + + .AddOpenIdConnect(options => + { + // Note: these settings must match the application details + // inserted in the database at the server level. + options.ClientId = "mvc"; + options.ClientSecret = "C59AFBC5-3D72-4292-A94E-79B893E9F9C4"; + + options.RequireHttpsMetadata = false; + options.GetClaimsFromUserInfoEndpoint = true; + options.SaveTokens = true; + + // Use the authorization code flow. + options.ResponseType = OpenIdConnectResponseType.Code; + options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet; + + // Note: setting the Authority allows the OIDC client middleware to automatically + // retrieve the identity provider's configuration and spare you from setting + // the different endpoints URIs or the token validation parameters explicitly. + options.Authority = "https://localhost:44213/"; + + options.Scope.Add("email"); + options.Scope.Add("roles"); + + options.SecurityTokenValidator = new JwtSecurityTokenHandler + { + // Disable the built-in JWT claims mapping feature. + InboundClaimTypeMap = new Dictionary() + }; + + options.TokenValidationParameters.NameClaimType = "name"; + options.TokenValidationParameters.RoleClaimType = "role"; + }); + + services.AddControllersWithViews(); + + services.AddHttpClient(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(options => + { + options.MapControllers(); + options.MapDefaultControllerRoute(); + }); + } + } +} \ No newline at end of file diff --git a/samples/Rentor/Rentor.Client/Views/Shared/Home.cshtml b/samples/Rentor/Rentor.Client/Views/Shared/Home.cshtml new file mode 100644 index 000000000..682f1468c --- /dev/null +++ b/samples/Rentor/Rentor.Client/Views/Shared/Home.cshtml @@ -0,0 +1,28 @@ +@model string + +
+ @if (User?.Identity?.IsAuthenticated ?? false) { +

Welcome, @User.Identity.Name

+ +

+ @foreach (var claim in Context.User.Claims) { +

@claim.Type: @claim.Value
+ } +

+ + if (!string.IsNullOrEmpty(Model)) { +

Message received from the resource controller: @Model

+ } + +
+ +
+ + Sign out + } + + else { +

Welcome, anonymous

+ Sign in + } +
\ No newline at end of file diff --git a/samples/Rentor/Rentor.Client/Views/Shared/_Layout.cshtml b/samples/Rentor/Rentor.Client/Views/Shared/_Layout.cshtml new file mode 100644 index 000000000..a574d588b --- /dev/null +++ b/samples/Rentor/Rentor.Client/Views/Shared/_Layout.cshtml @@ -0,0 +1,30 @@ + + + + + + + + + + Rentor.Client (custom store sample) + + + + + + +
+
+

Your application (Rentor.Client)

+
+ + @RenderBody() + + + +
+ + \ No newline at end of file diff --git a/samples/Rentor/Rentor.Client/Views/_ViewStart.cshtml b/samples/Rentor/Rentor.Client/Views/_ViewStart.cshtml new file mode 100644 index 000000000..1af6e4946 --- /dev/null +++ b/samples/Rentor/Rentor.Client/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} \ No newline at end of file diff --git a/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.eot b/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 000000000..4a4ca865d Binary files /dev/null and b/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.eot differ diff --git a/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.svg b/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 000000000..e3e2dc739 --- /dev/null +++ b/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.ttf b/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 000000000..67fa00bf8 Binary files /dev/null and b/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.ttf differ diff --git a/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.woff b/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 000000000..8c54182aa Binary files /dev/null and b/samples/Rentor/Rentor.Client/wwwroot/fonts/glyphicons-halflings-regular.woff differ diff --git a/samples/Rentor/Rentor.Client/wwwroot/scripts/bootstrap.js b/samples/Rentor/Rentor.Client/wwwroot/scripts/bootstrap.js new file mode 100644 index 000000000..53da1c77c --- /dev/null +++ b/samples/Rentor/Rentor.Client/wwwroot/scripts/bootstrap.js @@ -0,0 +1,2114 @@ +/*! + * Bootstrap v3.2.0 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') } + +/* ======================================================================== + * Bootstrap: transition.js v3.2.0 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false + var $el = this + $(this).one('bsTransitionEnd', function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + + if (!$.support.transition) return + + $.event.special.bsTransitionEnd = { + bindType: $.support.transition.end, + delegateType: $.support.transition.end, + handle: function (e) { + if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) + } + } + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: alert.js v3.2.0 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // ALERT CLASS DEFINITION + // ====================== + + var dismiss = '[data-dismiss="alert"]' + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.VERSION = '3.2.0' + + Alert.prototype.close = function (e) { + var $this = $(this) + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = $(selector) + + if (e) e.preventDefault() + + if (!$parent.length) { + $parent = $this.hasClass('alert') ? $this : $this.parent() + } + + $parent.trigger(e = $.Event('close.bs.alert')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + // detach from parent, fire event then clean up data + $parent.detach().trigger('closed.bs.alert').remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent + .one('bsTransitionEnd', removeElement) + .emulateTransitionEnd(150) : + removeElement() + } + + + // ALERT PLUGIN DEFINITION + // ======================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.alert + + $.fn.alert = Plugin + $.fn.alert.Constructor = Alert + + + // ALERT NO CONFLICT + // ================= + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + // ALERT DATA-API + // ============== + + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: button.js v3.2.0 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.VERSION = '3.2.0' + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state = state + 'Text' + + if (data.resetText == null) $el.data('resetText', $el[val]()) + + $el[val](data[state] == null ? this.options[state] : data[state]) + + // push to event loop to allow forms to submit + setTimeout($.proxy(function () { + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d) + } else if (this.isLoading) { + this.isLoading = false + $el.removeClass(d).removeAttr(d) + } + }, this), 0) + } + + Button.prototype.toggle = function () { + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked') && this.$element.hasClass('active')) changed = false + else $parent.find('.active').removeClass('active') + } + if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') + } + + if (changed) this.$element.toggleClass('active') + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + var old = $.fn.button + + $.fn.button = Plugin + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document).on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + Plugin.call($btn, 'toggle') + e.preventDefault() + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: carousel.js v3.2.0 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element).on('keydown.bs.carousel', $.proxy(this.keydown, this)) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = + this.sliding = + this.interval = + this.$active = + this.$items = null + + this.options.pause == 'hover' && this.$element + .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) + .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) + } + + Carousel.VERSION = '3.2.0' + + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true + } + + Carousel.prototype.keydown = function (e) { + switch (e.which) { + case 37: this.prev(); break + case 39: this.next(); break + default: return + } + + e.preventDefault() + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getItemIndex = function (item) { + this.$items = item.parent().children('.item') + return this.$items.index(item || this.$active) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || $active[type]() + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var fallback = type == 'next' ? 'first' : 'last' + var that = this + + if (!$next.length) { + if (!this.options.wrap) return + $next = this.$element.find('.item')[fallback]() + } + + if ($next.hasClass('active')) return (this.sliding = false) + + var relatedTarget = $next[0] + var slideEvent = $.Event('slide.bs.carousel', { + relatedTarget: relatedTarget, + direction: direction + }) + this.$element.trigger(slideEvent) + if (slideEvent.isDefaultPrevented()) return + + this.sliding = true + + isCycling && this.pause() + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) + $nextIndicator && $nextIndicator.addClass('active') + } + + var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one('bsTransitionEnd', function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { + that.$element.trigger(slidEvent) + }, 0) + }) + .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger(slidEvent) + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + var old = $.fn.carousel + + $.fn.carousel = Plugin + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var href + var $this = $(this) + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + if (!$target.hasClass('carousel')) return + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + Plugin.call($target, options) + + if (slideIndex) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + }) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + Plugin.call($carousel, $carousel.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: collapse.js v3.2.0 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.transitioning = null + + if (this.options.parent) this.$parent = $(this.options.parent) + if (this.options.toggle) this.toggle() + } + + Collapse.VERSION = '3.2.0' + + Collapse.DEFAULTS = { + toggle: true + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var actives = this.$parent && this.$parent.find('> .panel > .in') + + if (actives && actives.length) { + var hasData = actives.data('bs.collapse') + if (hasData && hasData.transitioning) return + Plugin.call(actives, 'hide') + hasData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing')[dimension](0) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('collapse in')[dimension]('') + this.transitioning = 0 + this.$element + .trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(350)[dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element[dimension](this.$element[dimension]())[0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse') + .removeClass('in') + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .trigger('hidden.bs.collapse') + .removeClass('collapsing') + .addClass('collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(350) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && option == 'show') option = !option + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.collapse + + $.fn.collapse = Plugin + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { + var href + var $this = $(this) + var target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + var $target = $(target) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + var parent = $this.attr('data-parent') + var $parent = parent && $(parent) + + if (!data || !data.transitioning) { + if ($parent) $parent.find('[data-toggle="collapse"][data-parent="' + parent + '"]').not($this).addClass('collapsed') + $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + } + + Plugin.call($target, option) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.2.0 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.2.0' + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $('