From da799b18c3e5d42ff347305f6d53b3605bbf6595 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Tue, 20 May 2014 19:58:08 -0400 Subject: [PATCH 01/15] Make UndertowUserSessionManagement class usable without the Servlet API. --- .../adapters/undertow/ServletRequestAuthenticator.java | 2 +- .../adapters/undertow/UndertowUserSessionManagement.java | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java index f73227f287..139683abc2 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java @@ -66,7 +66,7 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator { HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); HttpSession session = req.getSession(true); session.setAttribute(KeycloakUndertowAccount.class.getName(), account); - userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager(), session, account.getPrincipal().getName(), account.getKeycloakSecurityContext().getToken().getSessionState()); + userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager(), session.getId(), account.getPrincipal().getName(), account.getKeycloakSecurityContext().getToken().getSessionState()); } } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java index 1c11d37517..f9df4b05c3 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java @@ -8,7 +8,6 @@ import io.undertow.server.session.SessionManager; import io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler; import org.jboss.logging.Logger; -import javax.servlet.http.HttpSession; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -62,9 +61,7 @@ public class UndertowUserSessionManagement implements SessionListener { return set; } - public synchronized void login(SessionManager manager, HttpSession session, String username, String keycloakSessionId) { - String sessionId = session.getId(); - + public synchronized void login(SessionManager manager, String sessionId, String username, String keycloakSessionId) { UserSessions sessions = userSessionMap.get(username); if (sessions == null) { sessions = new UserSessions(); From c2075af7f74ef19075ea4823d39ff1702d5a9f33 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 08:47:58 +0100 Subject: [PATCH 02/15] Fix Twitter for localhost --- docbook/reference/en/en-US/modules/social-twitter.xml | 2 -- .../resources/theme/login/patternfly/resources/css/login.css | 4 ++++ .../java/org/keycloak/social/twitter/TwitterProvider.java | 5 +---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docbook/reference/en/en-US/modules/social-twitter.xml b/docbook/reference/en/en-US/modules/social-twitter.xml index 7f7f3fb603..66f0d8307b 100644 --- a/docbook/reference/en/en-US/modules/social-twitter.xml +++ b/docbook/reference/en/en-US/modules/social-twitter.xml @@ -33,8 +33,6 @@ Twitter doesn't allow localhost in the redirect URI. To test on a local server replace localhost with 127.0.0.1. - Twitter also restricts connection to TLS/SSL connections only, so you are required to use HTTPS to access - Keycloak to enable log in with Twitter. \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css b/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css index 354525d6fa..a3fc592458 100644 --- a/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css +++ b/forms/common-themes/src/main/resources/theme/login/patternfly/resources/css/login.css @@ -186,6 +186,10 @@ ol#kc-totp-settings li:first-of-type { width: 125px; } +.zocial:hover { + color: #fff !important; +} + .zocial.facebook, .zocial.github, .zocial.google, diff --git a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java index 10df5cda46..f07eb7df59 100755 --- a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java +++ b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterProvider.java @@ -48,10 +48,7 @@ public class TwitterProvider implements SocialProvider { Twitter twitter = new TwitterFactory().getInstance(); twitter.setOAuthConsumer(config.getKey(), config.getSecret()); - String redirectUri = config.getCallbackUrl(); - redirectUri = redirectUri.replace("//localhost", "//127.0.0.1"); - - RequestToken requestToken = twitter.getOAuthRequestToken(redirectUri); + RequestToken requestToken = twitter.getOAuthRequestToken(config.getCallbackUrl()); return AuthRequest.create(requestToken.getToken(), requestToken.getAuthenticationURL()) .setAttribute("token", requestToken.getToken()).setAttribute("tokenSecret", requestToken.getTokenSecret()) From 59440840fea687c6198480f0f2af018e236cb52a Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 09:35:01 +0100 Subject: [PATCH 03/15] Read keycloak-sever.json from /standalone/configuration if it exists --- distribution/appliance-dist/assembly.xml | 7 +++++++ distribution/war-zip/assembly.xml | 7 +++++++ .../services/DefaultProviderSessionFactory.java | 2 -- .../services/resources/KeycloakApplication.java | 16 +++++++++++++++- .../services/util/JsonConfigProvider.java | 8 ++++---- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/distribution/appliance-dist/assembly.xml b/distribution/appliance-dist/assembly.xml index 957cc368db..049062a4cc 100755 --- a/distribution/appliance-dist/assembly.xml +++ b/distribution/appliance-dist/assembly.xml @@ -38,6 +38,13 @@ keycloak-ds.xml + + ${project.build.directory}/unpacked/deployments/auth-server.war/WEB-INF/classes/META-INF + keycloak/standalone/configuration + + keycloak-server.json + + ${project.build.directory}/unpacked/themes keycloak/standalone/configuration/themes diff --git a/distribution/war-zip/assembly.xml b/distribution/war-zip/assembly.xml index 89ed1aae8b..124b455846 100755 --- a/distribution/war-zip/assembly.xml +++ b/distribution/war-zip/assembly.xml @@ -19,5 +19,12 @@ deployments + + ${project.build.directory}/unpacked/deployments/auth-server.war/WEB-INF/classes/META-INF + configuration + + keycloak-server.json + + diff --git a/services/src/main/java/org/keycloak/services/DefaultProviderSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultProviderSessionFactory.java index 128c27200c..1dcf5fe303 100755 --- a/services/src/main/java/org/keycloak/services/DefaultProviderSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultProviderSessionFactory.java @@ -33,7 +33,6 @@ public class DefaultProviderSessionFactory implements ProviderSessionFactory { ProviderFactory factory = loadProviderFactory(spi, provider); Config.Scope scope = Config.scope(spi.getName(), provider); factory.init(scope); - log.debug("Initialized " + factory.getClass().getName() + " (config = " + scope + ")"); factories.put(factory.getId(), factory); @@ -42,7 +41,6 @@ public class DefaultProviderSessionFactory implements ProviderSessionFactory { for (ProviderFactory factory : ServiceLoader.load(spi.getProviderFactoryClass())) { Config.Scope scope = Config.scope(spi.getName(), factory.getId()); factory.init(scope); - log.debug("Initialized " + factory.getClass().getName() + " (config = " + scope + ")"); factories.put(factory.getId(), factory); } diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index c677aa868c..24d7df8011 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -108,7 +108,19 @@ public class KeycloakApplication extends Application { protected void loadConfig() { try { - URL config = Thread.currentThread().getContextClassLoader().getResource("META-INF/keycloak-server.json"); + URL config = null; + + String configDir = System.getProperty("jboss.server.config.dir"); + if (configDir != null) { + File f = new File(configDir + File.separator + "keycloak-server.json"); + if (f.isFile()) { + config = f.toURI().toURL(); + } + } + + if (config == null) { + config = Thread.currentThread().getContextClassLoader().getResource("META-INF/keycloak-server.json"); + } if (config != null) { JsonNode node = new ObjectMapper().readTree(config); @@ -116,6 +128,8 @@ public class KeycloakApplication extends Application { log.info("Loaded config from " + config); return; + } else { + log.warn("Config 'keycloak-server.json' not found"); } } catch (IOException e) { throw new RuntimeException("Failed to load config", e); diff --git a/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java b/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java index 714e4dbe1d..0eb1cef453 100644 --- a/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java +++ b/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java @@ -74,11 +74,11 @@ public class JsonConfigProvider implements Config.ConfigProvider { if (n == null) { return null; } else if (n.isArray()) { - ArrayList l = new ArrayList(); - for (JsonNode e : n) { - l.add(StringPropertyReplacer.replaceProperties(e.getTextValue())); + String[] a = new String[n.size()]; + for (int i = 0; i < a.length; i++) { + a[i] = StringPropertyReplacer.replaceProperties(n.get(i).getTextValue()); } - return (String[]) l.toArray(); + return a; } else { return new String[] { StringPropertyReplacer.replaceProperties(n.getTextValue()) }; } From 11fc6d5321f1d3aa9a80182d7a82f76b7fff4bb3 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 09:39:02 +0100 Subject: [PATCH 04/15] KEYCLOAK-477 Remove keycloak.js debug statements --- integration/js/src/main/resources/keycloak.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/integration/js/src/main/resources/keycloak.js b/integration/js/src/main/resources/keycloak.js index 6dc108fc63..b05761d850 100755 --- a/integration/js/src/main/resources/keycloak.js +++ b/integration/js/src/main/resources/keycloak.js @@ -532,7 +532,6 @@ var Keycloak = function (config) { } var src = getRealmUrl() + '/login-status-iframe.html?client_id=' + encodeURIComponent(kc.clientId); - console.log('iframe src='+ src); iframe.setAttribute('src', src ); iframe.style.display = 'none'; document.body.appendChild(iframe); @@ -571,7 +570,6 @@ var Keycloak = function (config) { msg.callbackId = createCallbackId(); loginIframe.callbackMap[msg.callbackId] = promise; var origin = loginIframe.iframeOrigin; - console.log('*** origin: ' + origin); loginIframe.iframe.contentWindow.postMessage(msg, origin); } else { promise.setSuccess(); @@ -612,8 +610,6 @@ var Keycloak = function (config) { } if (type == 'cordova') { - console.debug('Enabling Cordova support'); - return { login: function(options) { var promise = createPromise(); From fd9317a295fcad8a2a1cdff9a66a415fb6f40108 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 09:52:02 +0100 Subject: [PATCH 05/15] KEYCLOAK-474 Code not visible for urn:ietf:wg:oauth:2.0:oob --- .../common-themes/src/main/resources/theme/login/base/code.ftl | 2 +- .../src/main/resources/theme/login/patternfly/theme.properties | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/forms/common-themes/src/main/resources/theme/login/base/code.ftl b/forms/common-themes/src/main/resources/theme/login/base/code.ftl index 43fdbe593c..84a98a5bcc 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/code.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/code.ftl @@ -10,7 +10,7 @@
<#if code.success>

Please copy this code and paste it into your application:

- + <#else>

${code.error}

diff --git a/forms/common-themes/src/main/resources/theme/login/patternfly/theme.properties b/forms/common-themes/src/main/resources/theme/login/patternfly/theme.properties index c6ae40258a..4b8566542a 100644 --- a/forms/common-themes/src/main/resources/theme/login/patternfly/theme.properties +++ b/forms/common-themes/src/main/resources/theme/login/patternfly/theme.properties @@ -23,5 +23,6 @@ kcInputClass=form-control kcInputWrapperClass=col-xs-12 col-sm-12 col-md-8 col-lg-9 kcFormOptionsClass=col-xs-4 col-sm-5 col-md-offset-4 col-md-4 col-lg-offset-3 col-lg-5 kcFormButtonsClass=col-xs-8 col-sm-7 col-md-4 col-lg-4 submit +kcTextareaClass=form-control kcInfoAreaClass=col-xs-12 col-sm-4 col-md-4 col-lg-6 details \ No newline at end of file From eb47d43497a7809e9e5aece71b398590105c3a42 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 10:32:03 +0100 Subject: [PATCH 06/15] For installed app urn redirect to a page instead of returning the html, this is to prevent NPE if page is refreshed --- .../main/resources/theme/login/base/code.ftl | 2 +- .../services/resources/AccountService.java | 4 +- .../services/resources/SocialResource.java | 6 ++- .../services/resources/TokenService.java | 35 +++++++++++++---- .../services/resources/flows/OAuthFlows.java | 39 +++++++------------ .../services/resources/flows/Urls.java | 4 ++ .../testsuite/util => }/MailUtil.java | 2 +- .../RequiredActionEmailVerificationTest.java | 4 +- .../testsuite/forms/ResetPasswordTest.java | 2 +- .../oauth/AuthorizationCodeTest.java | 29 ++++++++++++++ 10 files changed, 86 insertions(+), 41 deletions(-) rename testsuite/integration/src/test/java/org/keycloak/testsuite/{org/keycloak/testsuite/util => }/MailUtil.java (89%) diff --git a/forms/common-themes/src/main/resources/theme/login/base/code.ftl b/forms/common-themes/src/main/resources/theme/login/base/code.ftl index 84a98a5bcc..23ca6a8fca 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/code.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/code.ftl @@ -12,7 +12,7 @@

Please copy this code and paste it into your application:

<#else> -

${code.error}

+

${code.error}

diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 5ff6ca373c..47be8e9815 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -560,7 +560,7 @@ public class AccountService { ApplicationModel application = realm.getApplicationByName(referrer); if (application != null) { if (referrerUri != null) { - referrerUri = TokenService.verifyRedirectUri(uriInfo, referrerUri, application); + referrerUri = TokenService.verifyRedirectUri(uriInfo, referrerUri, realm, application); } else { referrerUri = ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), application.getBaseUrl()); } @@ -571,7 +571,7 @@ public class AccountService { } else if (referrerUri != null) { ClientModel client = realm.getOAuthClient(referrer); if (client != null) { - referrerUri = TokenService.verifyRedirectUri(uriInfo, referrerUri, application); + referrerUri = TokenService.verifyRedirectUri(uriInfo, referrerUri, realm, application); if (referrerUri != null) { return new String[]{referrer, referrerUri}; diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java index c7c5d23211..8129ff19f1 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -122,6 +122,10 @@ public class SocialResource { Map queryParams = getQueryParams(); RequestDetails requestData = getRequestDetails(queryParams); + if (requestData == null) { + Flows.forms(providerSession, null, uriInfo).setError("Unexpected callback").createErrorPage(); + } + SocialProvider provider = SocialLoader.load(requestData.getProviderId()); String realmName = requestData.getClientAttribute("realm"); @@ -296,7 +300,7 @@ public class SocialResource { logger.warn("Login requester not enabled."); return Flows.forms(providerSession, realm, uriInfo).setError("Login requester not enabled.").createErrorPage(); } - redirectUri = TokenService.verifyRedirectUri(uriInfo, redirectUri, client); + redirectUri = TokenService.verifyRedirectUri(uriInfo, redirectUri, realm, client); if (redirectUri == null) { audit.error(Errors.INVALID_REDIRECT_URI); return Flows.forms(providerSession, realm, uriInfo).setError("Invalid redirect_uri.").createErrorPage(); diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index f7feddcc68..0a5b33c76a 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -18,6 +18,7 @@ import org.keycloak.authentication.AuthenticationProviderException; import org.keycloak.authentication.AuthenticationProviderManager; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -42,6 +43,7 @@ import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.OAuthFlows; +import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.validation.Validation; import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.Time; @@ -363,7 +365,7 @@ public class TokenService { return oauth.forwardToSecurityFailure("Login requester not enabled."); } - redirect = verifyRedirectUri(uriInfo, redirect, client); + redirect = verifyRedirectUri(uriInfo, redirect, realm, client); if (redirect == null) { audit.error(Errors.INVALID_REDIRECT_URI); return oauth.forwardToSecurityFailure("Invalid redirect_uri."); @@ -457,7 +459,7 @@ public class TokenService { return oauth.forwardToSecurityFailure("Login requester not enabled."); } - redirect = verifyRedirectUri(uriInfo, redirect, client); + redirect = verifyRedirectUri(uriInfo, redirect, realm, client); if (redirect == null) { audit.error(Errors.INVALID_REDIRECT_URI); return oauth.forwardToSecurityFailure("Invalid redirect_uri."); @@ -754,7 +756,7 @@ public class TokenService { audit.error(Errors.NOT_ALLOWED); return oauth.forwardToSecurityFailure("Bearer-only applications are not allowed to initiate login"); } - redirect = verifyRedirectUri(uriInfo, redirect, client); + redirect = verifyRedirectUri(uriInfo, redirect, realm, client); if (redirect == null) { audit.error(Errors.INVALID_REDIRECT_URI); return oauth.forwardToSecurityFailure("Invalid redirect_uri."); @@ -811,7 +813,7 @@ public class TokenService { return oauth.forwardToSecurityFailure("Login requester not enabled."); } - redirect = verifyRedirectUri(uriInfo, redirect, client); + redirect = verifyRedirectUri(uriInfo, redirect, realm, client); if (redirect == null) { audit.error(Errors.INVALID_REDIRECT_URI); return oauth.forwardToSecurityFailure("Invalid redirect_uri."); @@ -937,6 +939,17 @@ public class TokenService { return oauth.redirectAccessCode(accessCodeEntry, session, state, redirect); } + @Path("oauth/oob") + @GET + public Response installedAppUrnCallback(final @QueryParam("code") String code, final @QueryParam("error") String error, final @QueryParam("error_description") String errorDescription) { + LoginFormsProvider forms = Flows.forms(providerSession, realm, uriInfo); + if (code != null) { + return forms.setAccessCode(null, code).createCode(); + } else { + return forms.setError(error).createCode(); + } + } + protected Response redirectAccessDenied(String redirect, String state) { UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied"); if (state != null) @@ -961,7 +974,7 @@ public class TokenService { return false; } - public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, ClientModel client) { + public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) { Set validRedirects = client.getRedirectUris(); if (redirectUri == null) { if (validRedirects.size() != 1) return null; @@ -970,10 +983,10 @@ public class TokenService { if (idx > -1) { validRedirect = validRedirect.substring(0, idx); } - return validRedirect; + redirectUri = validRedirect; } else if (validRedirects.isEmpty()) { logger.error("Redirect URI is required for client: " + client.getClientId()); - return null; + redirectUri = null; } else { String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri; Set resolveValidRedirects = resolveValidRedirects(uriInfo, validRedirects); @@ -996,7 +1009,13 @@ public class TokenService { valid = matchesRedirects(resolveValidRedirects, r); } - return valid ? redirectUri : null; + redirectUri = valid ? redirectUri : null; + } + + if (Constants.INSTALLED_APP_URN.equals(redirectUri)) { + return Urls.realmInstalledAppUrnCallback(uriInfo.getBaseUri(), realm.getName()).toString(); + } else { + return redirectUri; } } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index 7bf80fa195..65e1bb05d2 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -86,34 +86,25 @@ public class OAuthFlows { public Response redirectAccessCode(AccessCodeEntry accessCode, UserSessionModel session, String state, String redirect, boolean rememberMe) { String code = accessCode.getCode(); - - if (Constants.INSTALLED_APP_URN.equals(redirect)) { - return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), code).createCode(); - } else { - UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code); - log.debugv("redirectAccessCode: state: {0}", state); - if (state != null) - redirectUri.queryParam(OAuth2Constants.STATE, state); - Response.ResponseBuilder location = Response.status(302).location(redirectUri.build()); - Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME); - rememberMe = rememberMe || remember != null; - // refresh the cookies! - authManager.createLoginCookie(realm, accessCode.getUser(), session, uriInfo, rememberMe); - if (rememberMe) authManager.createRememberMeCookie(realm, uriInfo); - return location.build(); - } + UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code); + log.debugv("redirectAccessCode: state: {0}", state); + if (state != null) + redirectUri.queryParam(OAuth2Constants.STATE, state); + Response.ResponseBuilder location = Response.status(302).location(redirectUri.build()); + Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME); + rememberMe = rememberMe || remember != null; + // refresh the cookies! + authManager.createLoginCookie(realm, accessCode.getUser(), session, uriInfo, rememberMe); + if (rememberMe) authManager.createRememberMeCookie(realm, uriInfo); + return location.build(); } public Response redirectError(ClientModel client, String error, String state, String redirect) { - if (Constants.INSTALLED_APP_URN.equals(redirect)) { - return Flows.forms(providerSession, realm, uriInfo).setError(error).createCode(); - } else { - UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, error); - if (state != null) { - redirectUri.queryParam(OAuth2Constants.STATE, state); - } - return Response.status(302).location(redirectUri.build()).build(); + UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, error); + if (state != null) { + redirectUri.queryParam(OAuth2Constants.STATE, state); } + return Response.status(302).location(redirectUri.build()).build(); } public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, UserSessionModel session, String username, boolean rememberMe, String authMethod, Audit audit) { diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java index 961fbcb65f..fa311faebe 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java @@ -148,6 +148,10 @@ public class Urls { return tokenBase(baseUri).path(TokenService.class, "registerPage").build(realmId); } + public static URI realmInstalledAppUrnCallback(URI baseUri, String realmId) { + return tokenBase(baseUri).path(TokenService.class, "installedAppUrnCallback").build(realmId); + } + public static URI realmOauthAction(URI baseUri, String realmId) { return tokenBase(baseUri).path(TokenService.class, "processOAuth").build(realmId); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/MailUtil.java similarity index 89% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/MailUtil.java index 440fdb823e..9691301076 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/MailUtil.java @@ -1,4 +1,4 @@ -package org.keycloak.testsuite.org.keycloak.testsuite.util; +package org.keycloak.testsuite; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java index 73f185f52c..3d04ffcaba 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java @@ -34,7 +34,7 @@ import org.keycloak.models.UserModel; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; -import org.keycloak.testsuite.org.keycloak.testsuite.util.MailUtil; +import org.keycloak.testsuite.MailUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; @@ -50,8 +50,6 @@ import org.openqa.selenium.WebDriver; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * @author Stian Thorgersen diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 719b98c5a2..ae5cfce6d9 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -36,7 +36,7 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; -import org.keycloak.testsuite.org.keycloak.testsuite.util.MailUtil; +import org.keycloak.testsuite.MailUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java index dd11d41800..214ff5b45e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java @@ -112,6 +112,35 @@ public class AuthorizationCodeTest { }); } + @Test + public void authorizationRequestInstalledAppCancel() throws IOException { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.getApplicationNameMap().get("test-app").addRedirectUri(Constants.INSTALLED_APP_URN); + } + }); + oauth.redirectUri(Constants.INSTALLED_APP_URN); + + oauth.openLoginForm(); + driver.findElement(By.name("cancel")).click(); + + String title = driver.getTitle(); + Assert.assertTrue(title.equals("Error error=access_denied")); + + String error = driver.findElement(By.id(OAuth2Constants.ERROR)).getText(); + Assert.assertEquals("access_denied", error); + + events.expectLogin().error("rejected_by_user").user((String) null).session((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).detail(Details.REDIRECT_URI, Constants.INSTALLED_APP_URN).assertEvent().getDetails().get(Details.CODE_ID); + + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.getApplicationNameMap().get("test-app").removeRedirectUri(Constants.INSTALLED_APP_URN); + } + }); + } + @Test public void authorizationValidRedirectUri() throws IOException { keycloakRule.configure(new KeycloakRule.KeycloakSetup() { From 271292dbd474023bf206a5ccdb5880bd19204b12 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 20 May 2014 18:10:26 +0200 Subject: [PATCH 07/15] Mongo related fixes --- audit/mongo/pom.xml | 8 +- .../mongo/MongoAuditProviderFactory.java | 5 ++ .../models/mongo/impl/MongoStoreImpl.java | 2 +- .../keycloak/adapters/ApplicationAdapter.java | 2 +- .../keycloak/adapters/ClientAdapter.java | 18 ++--- .../adapters/MongoKeycloakSessionFactory.java | 4 +- .../keycloak/adapters/OAuthClientAdapter.java | 2 +- .../mongo/keycloak/adapters/RoleAdapter.java | 7 +- .../keycloak/adapters/UserSessionAdapter.java | 77 ++++++++----------- .../entities/MongoApplicationEntity.java | 12 ++- ...ngoClientUserSessionAssociationEntity.java | 37 --------- .../entities/MongoOAuthClientEntity.java | 12 ++- .../entities/MongoUserSessionEntity.java | 20 +++-- .../org/keycloak/model/test/AdapterTest.java | 45 +++++++++++ testsuite/integration/pom.xml | 2 +- .../resources/META-INF/keycloak-server.json | 16 +++- 16 files changed, 149 insertions(+), 120 deletions(-) delete mode 100755 model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientUserSessionAssociationEntity.java diff --git a/audit/mongo/pom.xml b/audit/mongo/pom.xml index 69a5a577c1..c62abf8cae 100755 --- a/audit/mongo/pom.xml +++ b/audit/mongo/pom.xml @@ -37,6 +37,11 @@ ${project.version} test + + org.jboss.logging + jboss-logging + provided + org.mongodb mongo-java-driver @@ -85,9 +90,6 @@ ${keycloak.audit.mongo.db} ${keycloak.audit.mongo.clearOnStartup} - - org.keycloak:keycloak-model-tests - diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java index b81afc5303..5a3d95a6d3 100644 --- a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java +++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java @@ -5,6 +5,7 @@ import com.mongodb.MongoClient; import com.mongodb.MongoCredential; import com.mongodb.ServerAddress; import com.mongodb.WriteConcern; +import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.audit.AuditProvider; import org.keycloak.audit.AuditProviderFactory; @@ -21,6 +22,8 @@ import java.util.Set; */ public class MongoAuditProviderFactory implements AuditProviderFactory { + protected static final Logger logger = Logger.getLogger(MongoAuditProviderFactory.class); + public static final String ID = "mongo"; private MongoClient client; private DB db; @@ -55,6 +58,8 @@ public class MongoAuditProviderFactory implements AuditProviderFactory { if (clearOnStartup) { db.getCollection("audit").drop(); } + + logger.infof("Initialized mongo audit. host: %s, port: %d, db: %s, clearOnStartup: %b", host, port, dbName, clearOnStartup); } catch (UnknownHostException e) { throw new RuntimeException(e); } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoStoreImpl.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoStoreImpl.java index 79b0e95610..a9c1d969e5 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoStoreImpl.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoStoreImpl.java @@ -237,7 +237,7 @@ public class MongoStoreImpl implements MongoStore { public T loadEntity(Class type, String id, MongoStoreInvocationContext context) { // First look if we already read the object with this oid and type during this transaction. If yes, use it instead of DB lookup T cached = context.getLoadedEntity(type, id); - if (cached != null) return cached; + if (cached != null && type.isAssignableFrom(cached.getClass())) return cached; DBCollection dbCollection = getDBCollectionForType(type); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java index c30db32d1f..b8dbd0a94e 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java @@ -22,7 +22,7 @@ import java.util.Set; */ public class ApplicationAdapter extends ClientAdapter implements ApplicationModel { - public ApplicationAdapter(RealmModel realm, MongoApplicationEntity applicationEntity, MongoStoreInvocationContext invContext) { + public ApplicationAdapter(RealmAdapter realm, MongoApplicationEntity applicationEntity, MongoStoreInvocationContext invContext) { super(realm, applicationEntity, invContext); } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java index b7404fde7f..55c2bd9ab8 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java @@ -9,12 +9,11 @@ import com.mongodb.DBObject; import com.mongodb.QueryBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.entities.ClientEntity; import org.keycloak.models.mongo.api.MongoIdentifiableEntity; import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; -import org.keycloak.models.mongo.keycloak.entities.MongoClientUserSessionAssociationEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity; /** * @author Marek Posolda @@ -22,9 +21,9 @@ import org.keycloak.models.mongo.keycloak.entities.MongoClientUserSessionAssocia public class ClientAdapter extends AbstractMongoAdapter implements ClientModel { protected final T clientEntity; - private final RealmModel realm; + private final RealmAdapter realm; - public ClientAdapter(RealmModel realm, T clientEntity, MongoStoreInvocationContext invContext) { + public ClientAdapter(RealmAdapter realm, T clientEntity, MongoStoreInvocationContext invContext) { super(invContext); this.clientEntity = clientEntity; this.realm = realm; @@ -154,7 +153,7 @@ public class ClientAdapter extends AbstractMo } @Override - public RealmModel getRealm() { + public RealmAdapter getRealm() { return realm; } @@ -172,14 +171,13 @@ public class ClientAdapter extends AbstractMo @Override public Set getUserSessions() { DBObject query = new QueryBuilder() - .and("clientId").is(getId()) + .and("associatedClientIds").is(getId()) .get(); - List associations = getMongoStore().loadEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext); + List sessions = getMongoStore().loadEntities(MongoUserSessionEntity.class, query, invocationContext); Set result = new HashSet(); - for (MongoClientUserSessionAssociationEntity association : associations) { - UserSessionModel session = realm.getUserSession(association.getSessionId()); - result.add(session); + for (MongoUserSessionEntity session : sessions) { + result.add(new UserSessionAdapter(session, realm, invocationContext)); } return result; diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java index 142cdbe356..fa390986ad 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java @@ -64,7 +64,7 @@ public class MongoKeycloakSessionFactory implements KeycloakSessionFactory { try { String host = config.get("host", ServerAddress.defaultHost()); int port = config.getInt("port", ServerAddress.defaultPort()); - String dbName = config.get("db", "keycloak-audit"); + String dbName = config.get("db", "keycloak"); boolean clearOnStartup = config.getBoolean("clearOnStartup", false); String user = config.get("user"); @@ -79,6 +79,8 @@ public class MongoKeycloakSessionFactory implements KeycloakSessionFactory { DB db = client.getDB(dbName); this.mongoStore = new MongoStoreImpl(db, clearOnStartup, MANAGED_ENTITY_TYPES); + + logger.infof("Initialized mongo model. host: %s, port: %d, db: %s, clearOnStartup: %b", host, port, dbName, clearOnStartup); } catch (UnknownHostException e) { throw new RuntimeException(e); } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/OAuthClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/OAuthClientAdapter.java index fbb2b3e5d2..d5d653211c 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/OAuthClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/OAuthClientAdapter.java @@ -10,7 +10,7 @@ import org.keycloak.models.mongo.keycloak.entities.MongoOAuthClientEntity; */ public class OAuthClientAdapter extends ClientAdapter implements OAuthClientModel { - public OAuthClientAdapter(RealmModel realm, MongoOAuthClientEntity oauthClientEntity, MongoStoreInvocationContext invContext) { + public OAuthClientAdapter(RealmAdapter realm, MongoOAuthClientEntity oauthClientEntity, MongoStoreInvocationContext invContext) { super(realm, oauthClientEntity, invContext); } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java index 3d8ef1afc3..41318bb613 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java @@ -7,7 +7,6 @@ import java.util.Set; import com.mongodb.DBObject; import com.mongodb.QueryBuilder; -import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; @@ -25,13 +24,13 @@ public class RoleAdapter extends AbstractMongoAdapter implement private final MongoRoleEntity role; private RoleContainerModel roleContainer; - private RealmModel realm; + private RealmAdapter realm; - public RoleAdapter(RealmModel realm, MongoRoleEntity roleEntity, MongoStoreInvocationContext invContext) { + public RoleAdapter(RealmAdapter realm, MongoRoleEntity roleEntity, MongoStoreInvocationContext invContext) { this(realm, roleEntity, null, invContext); } - public RoleAdapter(RealmModel realm, MongoRoleEntity roleEntity, RoleContainerModel roleContainer, MongoStoreInvocationContext invContext) { + public RoleAdapter(RealmAdapter realm, MongoRoleEntity roleEntity, RoleContainerModel roleContainer, MongoStoreInvocationContext invContext) { super(invContext); this.role = roleEntity; this.roleContainer = roleContainer; diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java index 78dfcb8801..bc23df2e9b 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserSessionAdapter.java @@ -2,27 +2,27 @@ package org.keycloak.models.mongo.keycloak.adapters; import com.mongodb.DBObject; import com.mongodb.QueryBuilder; +import org.jboss.logging.Logger; +import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.entities.ClientEntity; import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; -import org.keycloak.models.mongo.keycloak.entities.MongoClientUserSessionAssociationEntity; -import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity; -import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; -import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoApplicationEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoOAuthClientEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * @author Stian Thorgersen */ public class UserSessionAdapter extends AbstractMongoAdapter implements UserSessionModel { + private static final Logger logger = Logger.getLogger(RealmAdapter.class); + private MongoUserSessionEntity entity; private RealmAdapter realm; @@ -46,6 +46,7 @@ public class UserSessionAdapter extends AbstractMongoAdapter clients = getClientAssociations(); - for (ClientModel ass : clients) { - if (ass.getId().equals(client.getId())) return; - } - - MongoClientUserSessionAssociationEntity association = new MongoClientUserSessionAssociationEntity(); - association.setClientId(client.getId()); - association.setSessionId(getId()); - - getMongoStore().insertEntity(association, invocationContext); + getMongoStore().pushItemToList(entity, "associatedClientIds", client.getId(), true, invocationContext); } @Override public List getClientAssociations() { - DBObject query = new QueryBuilder() - .and("sessionId").is(getId()) - .get(); - List associations = getMongoStore().loadEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext); + List associatedClientIds = getMongoEntity().getAssociatedClientIds(); - List result = new ArrayList(); - for (MongoClientUserSessionAssociationEntity association : associations) { - ClientModel client = realm.findClientById(association.getClientId()); - result.add(client); + List clients = new ArrayList(); + for (String clientId : associatedClientIds) { + // Try application first + ClientModel client = realm.getApplicationById(clientId); + + // And then OAuthClient + if (client == null) { + client = realm.getOAuthClientById(clientId); + } + + if (client != null) { + clients.add(client); + } else { + logger.warnf("Not found associated client with Id: %s", clientId); + } } - return result; + return clients; } @Override public void removeAssociatedClient(ClientModel client) { - DBObject query = new QueryBuilder() - .and("sessionId").is(getId()) - .and("clientId").is(client.getId()) - .get(); - getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - - UserSessionAdapter that = (UserSessionAdapter) o; - return getId().equals(that.getId()); - } - - @Override - public int hashCode() { - return getId().hashCode(); + getMongoStore().pullItemFromList(entity, "associatedClientIds", client.getId(), invocationContext); } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoApplicationEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoApplicationEntity.java index bb83e0ee83..c8dd6daac0 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoApplicationEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoApplicationEntity.java @@ -1,7 +1,10 @@ package org.keycloak.models.mongo.keycloak.entities; +import java.util.List; + import com.mongodb.DBObject; import com.mongodb.QueryBuilder; +import org.jboss.logging.Logger; import org.keycloak.models.entities.ApplicationEntity; import org.keycloak.models.mongo.api.MongoCollection; import org.keycloak.models.mongo.api.MongoIdentifiableEntity; @@ -23,10 +26,13 @@ public class MongoApplicationEntity extends ApplicationEntity implements MongoId .get(); context.getMongoStore().removeEntities(MongoRoleEntity.class, query, context); + // Remove all session associations query = new QueryBuilder() - .and("clientId").is(getId()) + .and("associatedClientIds").is(getId()) .get(); - context.getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, context); - + List sessions = context.getMongoStore().loadEntities(MongoUserSessionEntity.class, query, context); + for (MongoUserSessionEntity session : sessions) { + context.getMongoStore().pullItemFromList(session, "associatedClientIds", getId(), context); + } } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientUserSessionAssociationEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientUserSessionAssociationEntity.java deleted file mode 100755 index d28e561999..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientUserSessionAssociationEntity.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.keycloak.models.mongo.keycloak.entities; - -import org.keycloak.models.entities.AbstractIdentifiableEntity; -import org.keycloak.models.mongo.api.MongoCollection; -import org.keycloak.models.mongo.api.MongoIdentifiableEntity; -import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -@MongoCollection(collectionName = "session-client-associations") -public class MongoClientUserSessionAssociationEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity { - private String clientId; - private String sessionId; - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getSessionId() { - return sessionId; - } - - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } - - @Override - public void afterRemove(MongoStoreInvocationContext invocationContext) { - } - -} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOAuthClientEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOAuthClientEntity.java index a499e51fb0..8950292411 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOAuthClientEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOAuthClientEntity.java @@ -1,5 +1,7 @@ package org.keycloak.models.mongo.keycloak.entities; +import java.util.List; + import com.mongodb.DBObject; import com.mongodb.QueryBuilder; import org.keycloak.models.entities.OAuthClientEntity; @@ -16,10 +18,14 @@ import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; public class MongoOAuthClientEntity extends OAuthClientEntity implements MongoIdentifiableEntity { @Override - public void afterRemove(MongoStoreInvocationContext invocationContext) { + public void afterRemove(MongoStoreInvocationContext context) { + // Remove all session associations DBObject query = new QueryBuilder() - .and("clientId").is(getId()) + .and("associatedClientIds").is(getId()) .get(); - invocationContext.getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, invocationContext); + List sessions = context.getMongoStore().loadEntities(MongoUserSessionEntity.class, query, context); + for (MongoUserSessionEntity session : sessions) { + context.getMongoStore().pullItemFromList(session, "associatedClientIds", getId(), context); + } } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java index 238984085d..c728b277a2 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java @@ -1,7 +1,8 @@ package org.keycloak.models.mongo.keycloak.entities; -import com.mongodb.DBObject; -import com.mongodb.QueryBuilder; +import java.util.ArrayList; +import java.util.List; + import org.keycloak.models.entities.AbstractIdentifiableEntity; import org.keycloak.models.mongo.api.MongoCollection; import org.keycloak.models.mongo.api.MongoIdentifiableEntity; @@ -23,6 +24,8 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement private int lastSessionRefresh; + private List associatedClientIds = new ArrayList(); + public String getRealmId() { return realmId; } @@ -63,13 +66,16 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement this.lastSessionRefresh = lastSessionRefresh; } + public List getAssociatedClientIds() { + return associatedClientIds; + } + + public void setAssociatedClientIds(List associatedClientIds) { + this.associatedClientIds = associatedClientIds; + } + @Override public void afterRemove(MongoStoreInvocationContext context) { - // Remove all roles, which belongs to this application - DBObject query = new QueryBuilder() - .and("sessionId").is(getId()) - .get(); - context.getMongoStore().removeEntities(MongoClientUserSessionAssociationEntity.class, query, context); } } diff --git a/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java index be26f44eb0..84d4586d89 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java @@ -762,4 +762,49 @@ public class AdapterTest extends AbstractModelTest { assertNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId())); } + @Test + public void userSessionAssociations() { + RealmModel realm = realmManager.createRealm("userSessions"); + UserModel user = realm.addUser("userSessions1"); + UserSessionModel userSession = realm.createUserSession(user, "127.0.0.1"); + + ApplicationModel app1 = realm.addApplication("app1"); + ApplicationModel app2 = realm.addApplication("app2"); + OAuthClientModel client1 = realm.addOAuthClient("client1"); + + Assert.assertEquals(0, userSession.getClientAssociations().size()); + + userSession.associateClient(app1); + userSession.associateClient(client1); + + Assert.assertEquals(2, userSession.getClientAssociations().size()); + Assert.assertTrue(app1.getUserSessions().contains(userSession)); + Assert.assertFalse(app2.getUserSessions().contains(userSession)); + Assert.assertTrue(client1.getUserSessions().contains(userSession)); + + commit(); + + // Refresh all + realm = realmManager.getRealm("userSessions"); + userSession = realm.getUserSession(userSession.getId()); + app1 = realm.getApplicationByName("app1"); + client1 = realm.getOAuthClient("client1"); + + userSession.removeAssociatedClient(app1); + Assert.assertEquals(1, userSession.getClientAssociations().size()); + Assert.assertEquals(client1, userSession.getClientAssociations().get(0)); + Assert.assertFalse(app1.getUserSessions().contains(userSession)); + + commit(); + + // Refresh all + realm = realmManager.getRealm("userSessions"); + userSession = realm.getUserSession(userSession.getId()); + client1 = realm.getOAuthClient("client1"); + + userSession.removeAssociatedClient(client1); + Assert.assertEquals(0, userSession.getClientAssociations().size()); + Assert.assertFalse(client1.getUserSessions().contains(userSession)); + } + } diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 74bac64faa..423b421577 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -439,7 +439,7 @@ mongo - keycloak.model + keycloak.model.provider mongo diff --git a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json index f0fd04673e..29172c23fa 100644 --- a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json @@ -4,11 +4,23 @@ }, "audit": { - "provider": "${keycloak.audit.provider:jpa}" + "provider": "${keycloak.audit.provider,keycloak.model.provider:jpa}", + "mongo": { + "host": "${keycloak.audit.mongo.host:127.0.0.1}", + "port": "${keycloak.audit.mongo.port:27017}", + "db": "${keycloak.audit.mongo.db:keycloak-audit}", + "clearOnStartup": "${keycloak.model.mongo.clearOnStartup:false}" + } }, "model": { - "provider": "${keycloak.model.provider:jpa}" + "provider": "${keycloak.model.provider:jpa}", + "mongo": { + "host": "${keycloak.model.mongo.host:127.0.0.1}", + "port": "${keycloak.model.mongo.port:27017}", + "db": "${keycloak.model.mongo.db:keycloak}", + "clearOnStartup": "${keycloak.model.mongo.clearOnStartup:false}" + } }, "timer": { From 7232535729b18ef41fc9071c41cc9a68ae0316c3 Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 21 May 2014 11:34:12 +0200 Subject: [PATCH 08/15] More mongo fixes --- .../keycloak/models/mongo/keycloak/adapters/RealmAdapter.java | 2 ++ testsuite/integration/README.md | 2 ++ testsuite/integration/pom.xml | 4 ++-- .../src/main/resources/META-INF/keycloak-server.json | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index da010222f2..da20338ebc 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -299,6 +299,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme @Override public void setSsoSessionIdleTimeout(int seconds) { realm.setSsoSessionIdleTimeout(seconds); + updateRealm(); } @Override @@ -309,6 +310,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme @Override public void setSsoSessionMaxLifespan(int seconds) { realm.setSsoSessionMaxLifespan(seconds); + updateRealm(); } @Override diff --git a/testsuite/integration/README.md b/testsuite/integration/README.md index 003c2151bf..a2033eb399 100644 --- a/testsuite/integration/README.md +++ b/testsuite/integration/README.md @@ -58,6 +58,8 @@ By default it's using database `keycloak` on localhost/27017 and it uses already mvn exec:java -Pkeycloak-server -Dkeycloak.model.provider=mongo -Dkeycloak.model.mongo.host=localhost -Dkeycloak.model.mongo.port=27017 -Dkeycloak.model.mongo.db=keycloak -Dkeycloak.model.mongo.clearOnStartup=false +Note that if you are using Mongo model, it would mean that Mongo will be used for audit as well. You may need to use audit related properties for configuration of Mongo if you want to override default ones (For example keycloak.audit.mongo.host, keycloak.audit.mongo.port etc) + TOTP codes ---------- diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 423b421577..513666f617 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -471,13 +471,13 @@ ${keycloak.model.mongo.host} ${keycloak.model.mongo.port} ${keycloak.model.mongo.db} + ${keycloak.model.mongo.clearOnStartup} mongo ${keycloak.model.mongo.host} ${keycloak.model.mongo.port} ${keycloak.model.mongo.db} - - ${keycloak.model.mongo.clearOnStartup} + ${keycloak.model.mongo.clearOnStartup} diff --git a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json index 29172c23fa..251dfa48b4 100644 --- a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json @@ -9,7 +9,7 @@ "host": "${keycloak.audit.mongo.host:127.0.0.1}", "port": "${keycloak.audit.mongo.port:27017}", "db": "${keycloak.audit.mongo.db:keycloak-audit}", - "clearOnStartup": "${keycloak.model.mongo.clearOnStartup:false}" + "clearOnStartup": "${keycloak.audit.mongo.clearOnStartup:false}" } }, From a5a28482510ec49163f23935fba1bcf959de23ed Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 10:40:15 +0100 Subject: [PATCH 09/15] KEYCLOAK-476 theme/account/patternfly/img/icon-sidebar-active.svg not found --- .../patternfly/resources/css/account.css | 2 +- .../resources/img/icon-sidebar-active.png | Bin 0 -> 202 bytes .../keycloak/resources/css/admin-console.css | 4 +- .../resources/img/icon-sidebar-active.svg | 7 - .../resources/img/sprite-arrow-down.svg | 8 - .../keycloak/resources/img/sprites-white.svg | 1328 ----------------- .../admin/patternfly/resources/css/styles.css | 6 +- 7 files changed, 4 insertions(+), 1351 deletions(-) create mode 100644 forms/common-themes/src/main/resources/theme/account/patternfly/resources/img/icon-sidebar-active.png delete mode 100644 forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/icon-sidebar-active.svg delete mode 100644 forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/sprite-arrow-down.svg delete mode 100755 forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/sprites-white.svg diff --git a/forms/common-themes/src/main/resources/theme/account/patternfly/resources/css/account.css b/forms/common-themes/src/main/resources/theme/account/patternfly/resources/css/account.css index bfd13a11f6..07762b07be 100644 --- a/forms/common-themes/src/main/resources/theme/account/patternfly/resources/css/account.css +++ b/forms/common-themes/src/main/resources/theme/account/patternfly/resources/css/account.css @@ -91,7 +91,7 @@ header .navbar { background-color: #c7e5f0; border-color: #56bae0; font-weight: bold; - background-image: url(../img/icon-sidebar-active.svg); + background-image: url(../img/icon-sidebar-active.png); background-repeat: no-repeat; background-position: right center; } diff --git a/forms/common-themes/src/main/resources/theme/account/patternfly/resources/img/icon-sidebar-active.png b/forms/common-themes/src/main/resources/theme/account/patternfly/resources/img/icon-sidebar-active.png new file mode 100644 index 0000000000000000000000000000000000000000..e7b9b082836b728286e1962f7d2efc81ddbe0b71 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^;y}#D!3HFcd~W{?q$EpRBT9nv(@M${i&7aJQ}UBi z6+Ckj(^G>|6H_V+Po~-c73FxkIEGZ*N=jIun2<7~^e}_RcjiI`=UK zVCeYHtShu&-DyXLq!)E7W+-p1Ilz&A@c@&Xh}=}&gd+kA7qR+CC9YLuAP#kTzP zOOabP0l+XkKxNbyD literal 0 HcmV?d00001 diff --git a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/admin-console.css b/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/admin-console.css index d567eedb97..978fe0998b 100644 --- a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/admin-console.css +++ b/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/admin-console.css @@ -310,7 +310,7 @@ form .btn + .btn { background-color: #c7e5f0; border-color: #56bae0; font-weight: bold; - background-image: url(../img/icon-sidebar-active.svg); + background-image: url(../img/icon-sidebar-active.png); background-repeat: no-repeat; background-position: right center; } @@ -522,7 +522,7 @@ legend .kc-icon-collapse { .header .navbar-primary li > .select-kc { background-color: #555A5E; - background-image: url("../img/sprite-arrow-down.svg"); + background-image: url("../img/sprite-arrow-down.png"); background-position: right -26px; background-repeat: no-repeat; border: 1px solid #676C6E; diff --git a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/icon-sidebar-active.svg b/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/icon-sidebar-active.svg deleted file mode 100644 index 3b7539db51..0000000000 --- a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/icon-sidebar-active.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/sprite-arrow-down.svg b/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/sprite-arrow-down.svg deleted file mode 100644 index 87c4cda27f..0000000000 --- a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/sprite-arrow-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/sprites-white.svg b/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/sprites-white.svg deleted file mode 100755 index 294c7c54bc..0000000000 --- a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/img/sprites-white.svg +++ /dev/null @@ -1,1328 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/forms/common-themes/src/main/resources/theme/admin/patternfly/resources/css/styles.css b/forms/common-themes/src/main/resources/theme/admin/patternfly/resources/css/styles.css index 726012a200..df98e10e71 100644 --- a/forms/common-themes/src/main/resources/theme/admin/patternfly/resources/css/styles.css +++ b/forms/common-themes/src/main/resources/theme/admin/patternfly/resources/css/styles.css @@ -1,6 +1,2 @@ @import url("../lib/patternfly/css/patternfly.css"); -@import url("../lib/select2-3.4.1/select2.css"); - -@import url("admin-console.css"); -@import url("tables.css"); -@import url("sprites.css"); +@import url("../lib/select2-3.4.1/select2.css"); \ No newline at end of file From f08477ea66c5ad9fa6094255b2d84c82185ba872 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Tue, 20 May 2014 17:38:59 +0100 Subject: [PATCH 10/15] Run import before creating default realm --- .../org/keycloak/services/resources/KeycloakApplication.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 24d7df8011..2571824611 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -84,12 +84,12 @@ public class KeycloakApplication extends Application { classes.add(JsResource.class); classes.add(WelcomeResource.class); + checkExportImportProvider(); + setupDefaultRealm(context.getContextPath()); setupScheduledTasks(providerSessionFactory); importRealms(context); - - checkExportImportProvider(); } public String getContextPath() { From 952f09844035200426d1b528b5c0fdc529ea1c19 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 11:16:27 +0100 Subject: [PATCH 11/15] KEYCLOAK-483 Remove 'index.html' from console url --- .../keycloak/services/resources/admin/AdminConsole.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index bd897fa340..38bd390e77 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -250,9 +250,11 @@ public class AdminConsole { @GET public Response getMainPage() throws URISyntaxException { - return Response.status(302).location( - AdminRoot.adminConsoleUrl(uriInfo).path("index.html").build(realm.getName()) - ).build(); + if (!uriInfo.getRequestUri().getPath().endsWith("/")) { + return Response.status(302).location(uriInfo.getRequestUriBuilder().path("/").build()).build(); + } else { + return getResource("index.html"); + } } @GET From d68131ac00d694e0b988e8015cb650460aeadf2c Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 12:05:32 +0100 Subject: [PATCH 12/15] KEYCLOAK-475 Log is shown in menu when audit is disabled for realm --- .../main/resources/theme/account/base/log.ftl | 2 +- .../resources/theme/account/base/sessions.ftl | 18 +++++++----------- .../services/resources/AccountService.java | 12 +++++++++++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/account/base/log.ftl b/forms/common-themes/src/main/resources/theme/account/base/log.ftl index d61a98a901..8f6473656c 100644 --- a/forms/common-themes/src/main/resources/theme/account/base/log.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/log.ftl @@ -24,7 +24,7 @@ ${event.date?datetime} ${event.event} ${event.ipAddress} - ${event.client} + ${event.client!} <#list event.details as detail>${detail.key} = ${detail.value} <#if detail_has_next>, diff --git a/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl b/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl index ce5ed96393..2fa5400cfe 100755 --- a/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl @@ -7,7 +7,7 @@ - +
@@ -27,18 +27,14 @@ diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 47be8e9815..8f864ac10d 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -160,7 +160,9 @@ public class AccountService { } } - account.setFeatures(realm.isSocial(), auditProvider != null, passwordUpdateSupported); + boolean auditEnabled = auditProvider != null && realm.isAuditEnabled(); + + account.setFeatures(realm.isSocial(), auditEnabled, passwordUpdateSupported); } public static UriBuilder accountServiceBaseUrl(UriInfo uriInfo) { @@ -246,6 +248,14 @@ public class AccountService { public Response logPage() { if (auth != null) { List events = auditProvider.createQuery().event(AUDIT_EVENTS).user(auth.getUser().getId()).maxResults(30).getResultList(); + for (Event e : events) { + Iterator> itr = e.getDetails().entrySet().iterator(); + while (itr.hasNext()) { + if (!AUDIT_DETAILS.contains(itr.next().getKey())) { + itr.remove(); + } + } + } account.setEvents(events); } return forwardToPage("log", AccountPages.LOG); From 681423019339058c6c314333a2808d9c12de6973 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 12:13:00 +0100 Subject: [PATCH 13/15] Fix NPE in account service --- .../keycloak/services/resources/AccountService.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 8f864ac10d..d6414412b9 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -249,10 +249,12 @@ public class AccountService { if (auth != null) { List events = auditProvider.createQuery().event(AUDIT_EVENTS).user(auth.getUser().getId()).maxResults(30).getResultList(); for (Event e : events) { - Iterator> itr = e.getDetails().entrySet().iterator(); - while (itr.hasNext()) { - if (!AUDIT_DETAILS.contains(itr.next().getKey())) { - itr.remove(); + if (e.getDetails() != null) { + Iterator> itr = e.getDetails().entrySet().iterator(); + while (itr.hasNext()) { + if (!AUDIT_DETAILS.contains(itr.next().getKey())) { + itr.remove(); + } } } } From 73f59417bd972f20bf484ff44021ff9abcca188e Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 12:47:07 +0100 Subject: [PATCH 14/15] Remove index.html from redirect on '/auth/admin/' --- .../java/org/keycloak/services/resources/admin/AdminRoot.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index 8b623a63fc..1c01d84852 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -64,7 +64,7 @@ public class AdminRoot { public Response masterRealmAdminConsoleRedirect() { RealmModel master = new RealmManager(session).getKeycloakAdminstrationRealm(); return Response.status(302).location( - uriInfo.getBaseUriBuilder().path(AdminRoot.class).path(AdminRoot.class, "getAdminConsole").path("index.html").build(master.getName()) + uriInfo.getBaseUriBuilder().path(AdminRoot.class).path(AdminRoot.class, "getAdminConsole").path("/").build(master.getName()) ).build(); } From 10ae4572371f76bf8f4551c890a96e190b3ad337 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Wed, 21 May 2014 14:10:59 +0100 Subject: [PATCH 15/15] Fixes to admin console --- .../theme/admin/base/resources/index.html | 3 +- .../theme/admin/base/resources/js/services.js | 114 +- .../resources/partials/realm-auth-detail.html | 6 +- .../resources/partials/realm-auth-list.html | 9 +- .../base/resources/templates/kc-modal.html | 23 +- .../resources/templates/kc-navigation.html | 2 +- .../lib/angular/angular-bootstrap-prettify.js | 1835 -------- .../lib/angular/angular-bootstrap.js | 175 - .../lib/angular/ui-bootstrap-tpls-0.11.0.js | 4116 +++++++++++++++++ .../lib/angular/ui-bootstrap-tpls-0.4.0.js | 3170 ------------- 10 files changed, 4205 insertions(+), 5248 deletions(-) delete mode 100755 forms/common-themes/src/main/resources/theme/common/keycloak/resources/lib/angular/angular-bootstrap-prettify.js delete mode 100755 forms/common-themes/src/main/resources/theme/common/keycloak/resources/lib/angular/angular-bootstrap.js create mode 100644 forms/common-themes/src/main/resources/theme/common/keycloak/resources/lib/angular/ui-bootstrap-tpls-0.11.0.js delete mode 100755 forms/common-themes/src/main/resources/theme/common/keycloak/resources/lib/angular/ui-bootstrap-tpls-0.4.0.js diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/index.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/index.html index 2384e1b841..eca11704ff 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/index.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/index.html @@ -15,7 +15,7 @@ - + @@ -39,6 +39,7 @@ +
You will be logged off in seconds due to inactivity. Click here to continue using this web page. diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js index 0294169311..b827960997 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js @@ -2,9 +2,40 @@ var module = angular.module('keycloak.services', [ 'ngResource', 'ngRoute' ]); -module.service('Dialog', function($dialog) { +module.service('Dialog', function($modal) { var dialog = {}; + var openDialog = function(title, message, btns) { + var controller = function($scope, $modalInstance, title, message, btns) { + $scope.title = title; + $scope.message = message; + $scope.btns = btns; + + $scope.ok = function () { + $modalInstance.close(); + }; + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + }; + + return $modal.open({ + templateUrl: 'templates/kc-modal.html', + controller: controller, + resolve: { + title: function() { + return title; + }, + message: function() { + return message; + }, + btns: function() { + return btns; + } + } + }).result; + } + var escapeHtml = function(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); @@ -13,64 +44,53 @@ module.service('Dialog', function($dialog) { dialog.confirmDelete = function(name, type, success) { var title = 'Delete ' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)); - var msg = 'Are you sure you want to permanently delete the ' + escapeHtml(type) + ' ' + escapeHtml(name) + '?'; - var btns = [ { - result : 'cancel', - label : 'Cancel', - cssClass : 'btn btn-default' - }, { - result : 'ok', - label : 'Delete', - cssClass : 'btn btn-danger' - } ]; + var msg = 'Are you sure you want to permanently delete the ' + type + ' ' + name + '?'; + var btns = { + ok: { + label: 'Delete', + cssClass: 'btn btn-danger' + }, + cancel: { + label: 'Cancel', + cssClass: 'btn btn-default' + } + } - $dialog.messageBox(title, msg, btns).open().then(function(result) { - if (result == "ok") { - success(); - } - }); + openDialog(title, msg, btns).then(success); } dialog.confirmGenerateKeys = function(name, type, success) { var title = 'Generate new keys for realm'; - var msg = 'Are you sure you want to permanently generate new keys for ' + name + '?'; - var btns = [ { - result : 'cancel', - label : 'Cancel', - cssClass : 'btn btn-default' - }, { - result : 'ok', - label : 'Generate new keys', - cssClass : 'btn btn-danger' - } ]; - - $dialog.messageBox(title, msg, btns).open().then(function(result) { - if (result == "ok") { - success(); + var msg = 'Are you sure you want to permanently generate new keys for ' + name + '?'; + var btns = { + ok: { + label: 'Generate Keys', + cssClass: 'btn btn-danger' + }, + cancel: { + label: 'Cancel', + cssClass: 'btn btn-default' } - }); + } + + openDialog(title, msg, btns).then(success); } dialog.confirm = function(title, message, success, cancel) { var title = title; var msg = '' + message + '"'; - var btns = [ { - result : 'cancel', - label : 'Cancel', - cssClass : 'btn btn-default' - }, { - result : 'ok', - label : title, - cssClass : 'btn btn-danger' - } ]; - - $dialog.messageBox(title, msg, btns).open().then(function(result) { - if (result == "ok") { - success(); - } else { - cancel && cancel(); + var btns = { + ok: { + label: title, + cssClass: 'btn btn-danger' + }, + cancel: { + label: 'Cancel', + cssClass: 'btn btn-default' } - }); + } + + openDialog(title, msg, btns).then(success).reject(cancel); } return dialog diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-auth-detail.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-auth-detail.html index e9a42846c2..da9282d512 100644 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-auth-detail.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-auth-detail.html @@ -37,17 +37,13 @@
-
+
- - -
- {{authProvider.providerName|capitalize}}'s provider options
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-auth-list.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-auth-list.html index a49df6dcdb..8acd99c324 100644 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-auth-list.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-auth-list.html @@ -28,7 +28,14 @@
- + diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-modal.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-modal.html index ad9b6a7a71..25eee9682f 100644 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-modal.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/templates/kc-modal.html @@ -1,14 +1,11 @@ -
IP${session.lastAccess?datetime} ${session.expires?datetime} -
    - <#list session.applications as app> -
  • ${app}
  • - -
+ <#list session.applications as app> + ${app}
+
-
    - <#list session.clients as client> -
  • ${client}
  • - -
+ <#list session.clients as client> + ${client}
+
{{authProvider.providerName|capitalize}} {{authProvider.passwordUpdateSupported}}{{authProvider.config}} + + + + + +
{{key}}{{value}}
+
No authentication providers available
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
{{label.abbr}}
{{ weekNumbers[$index] }}\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/month.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/popup.html", + "
    \n" + + "
  • \n" + + "
  • \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
  • \n" + + "
\n" + + ""); +}]); + +angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/datepicker/year.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/modal/backdrop.html", + "
\n" + + ""); +}]); + +angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/modal/window.html", + "
\n" + + "
\n" + + "
"); +}]); + +angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/pagination/pager.html", + ""); +}]); + +angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/pagination/pagination.html", + ""); +}]); + +angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tooltip/tooltip-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/popover/popover.html", + "
\n" + + "
\n" + + "\n" + + "
\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/bar.html", + "
"); +}]); + +angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progress.html", + "
"); +}]); + +angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progressbar.html", + "
\n" + + "
\n" + + "
"); +}]); + +angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/rating/rating.html", + "\n" + + " \n" + + " ({{ $index < value ? '*' : ' ' }})\n" + + " \n" + + ""); +}]); + +angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tab.html", + "
  • \n" + + " {{heading}}\n" + + "
  • \n" + + ""); +}]); + +angular.module("template/tabs/tabset-titles.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tabset-titles.html", + "
      \n" + + "
    \n" + + ""); +}]); + +angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tabset.html", + "\n" + + "
    \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + ""); +}]); + +angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/timepicker/timepicker.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
       
      \n" + + " \n" + + " :\n" + + " \n" + + "
       
      \n" + + ""); +}]); + +angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-match.html", + ""); +}]); + +angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-popup.html", + "
        \n" + + "
      • \n" + + "
        \n" + + "
      • \n" + + "
      "); +}]); diff --git a/forms/common-themes/src/main/resources/theme/common/keycloak/resources/lib/angular/ui-bootstrap-tpls-0.4.0.js b/forms/common-themes/src/main/resources/theme/common/keycloak/resources/lib/angular/ui-bootstrap-tpls-0.4.0.js deleted file mode 100755 index 9b62fe7ee4..0000000000 --- a/forms/common-themes/src/main/resources/theme/common/keycloak/resources/lib/angular/ui-bootstrap-tpls-0.4.0.js +++ /dev/null @@ -1,3170 +0,0 @@ -angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.datepicker","ui.bootstrap.dialog","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.position","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); -angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/dialog/message.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead.html"]); -angular.module('ui.bootstrap.transition', []) - -/** - * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. - * @param {DOMElement} element The DOMElement that will be animated. - * @param {string|object|function} trigger The thing that will cause the transition to start: - * - As a string, it represents the css class to be added to the element. - * - As an object, it represents a hash of style attributes to be applied to the element. - * - As a function, it represents a function to be called that will cause the transition to occur. - * @return {Promise} A promise that is resolved when the transition finishes. - */ -.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { - - var $transition = function(element, trigger, options) { - options = options || {}; - var deferred = $q.defer(); - var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"]; - - var transitionEndHandler = function(event) { - $rootScope.$apply(function() { - element.unbind(endEventName, transitionEndHandler); - deferred.resolve(element); - }); - }; - - if (endEventName) { - element.bind(endEventName, transitionEndHandler); - } - - // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur - $timeout(function() { - if ( angular.isString(trigger) ) { - element.addClass(trigger); - } else if ( angular.isFunction(trigger) ) { - trigger(element); - } else if ( angular.isObject(trigger) ) { - element.css(trigger); - } - //If browser does not support transitions, instantly resolve - if ( !endEventName ) { - deferred.resolve(element); - } - }); - - // Add our custom cancel function to the promise that is returned - // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, - // i.e. it will therefore never raise a transitionEnd event for that transition - deferred.promise.cancel = function() { - if ( endEventName ) { - element.unbind(endEventName, transitionEndHandler); - } - deferred.reject('Transition cancelled'); - }; - - return deferred.promise; - }; - - // Work out the name of the transitionEnd event - var transElement = document.createElement('trans'); - var transitionEndEventNames = { - 'WebkitTransition': 'webkitTransitionEnd', - 'MozTransition': 'transitionend', - 'OTransition': 'oTransitionEnd', - 'transition': 'transitionend' - }; - var animationEndEventNames = { - 'WebkitTransition': 'webkitAnimationEnd', - 'MozTransition': 'animationend', - 'OTransition': 'oAnimationEnd', - 'transition': 'animationend' - }; - function findEndEventName(endEventNames) { - for (var name in endEventNames){ - if (transElement.style[name] !== undefined) { - return endEventNames[name]; - } - } - } - $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); - $transition.animationEndEventName = findEndEventName(animationEndEventNames); - return $transition; -}]); - -angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition']) - -// The collapsible directive indicates a block of html that will expand and collapse -.directive('collapse', ['$transition', function($transition) { - // CSS transitions don't work with height: auto, so we have to manually change the height to a - // specific value and then once the animation completes, we can reset the height to auto. - // Unfortunately if you do this while the CSS transitions are specified (i.e. in the CSS class - // "collapse") then you trigger a change to height 0 in between. - // The fix is to remove the "collapse" CSS class while changing the height back to auto - phew! - var fixUpHeight = function(scope, element, height) { - // We remove the collapse CSS class to prevent a transition when we change to height: auto - element.removeClass('collapse'); - element.css({ height: height }); - // It appears that reading offsetWidth makes the browser realise that we have changed the - // height already :-/ - var x = element[0].offsetWidth; - element.addClass('collapse'); - }; - - return { - link: function(scope, element, attrs) { - - var isCollapsed; - var initialAnimSkip = true; - scope.$watch(function (){ return element[0].scrollHeight; }, function (value) { - //The listener is called when scollHeight changes - //It actually does on 2 scenarios: - // 1. Parent is set to display none - // 2. angular bindings inside are resolved - //When we have a change of scrollHeight we are setting again the correct height if the group is opened - if (element[0].scrollHeight !== 0) { - if (!isCollapsed) { - if (initialAnimSkip) { - fixUpHeight(scope, element, element[0].scrollHeight + 'px'); - } else { - fixUpHeight(scope, element, 'auto'); - } - } - } - }); - - scope.$watch(attrs.collapse, function(value) { - if (value) { - collapse(); - } else { - expand(); - } - }); - - - var currentTransition; - var doTransition = function(change) { - if ( currentTransition ) { - currentTransition.cancel(); - } - currentTransition = $transition(element,change); - currentTransition.then( - function() { currentTransition = undefined; }, - function() { currentTransition = undefined; } - ); - return currentTransition; - }; - - var expand = function() { - if (initialAnimSkip) { - initialAnimSkip = false; - if ( !isCollapsed ) { - fixUpHeight(scope, element, 'auto'); - } - } else { - doTransition({ height : element[0].scrollHeight + 'px' }) - .then(function() { - // This check ensures that we don't accidentally update the height if the user has closed - // the group while the animation was still running - if ( !isCollapsed ) { - fixUpHeight(scope, element, 'auto'); - } - }); - } - isCollapsed = false; - }; - - var collapse = function() { - isCollapsed = true; - if (initialAnimSkip) { - initialAnimSkip = false; - fixUpHeight(scope, element, 0); - } else { - fixUpHeight(scope, element, element[0].scrollHeight + 'px'); - doTransition({'height':'0'}); - } - }; - } - }; -}]); - -angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) - -.constant('accordionConfig', { - closeOthers: true -}) - -.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { - - // This array keeps track of the accordion groups - this.groups = []; - - // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to - this.closeOthers = function(openGroup) { - var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; - if ( closeOthers ) { - angular.forEach(this.groups, function (group) { - if ( group !== openGroup ) { - group.isOpen = false; - } - }); - } - }; - - // This is called from the accordion-group directive to add itself to the accordion - this.addGroup = function(groupScope) { - var that = this; - this.groups.push(groupScope); - - groupScope.$on('$destroy', function (event) { - that.removeGroup(groupScope); - }); - }; - - // This is called from the accordion-group directive when to remove itself - this.removeGroup = function(group) { - var index = this.groups.indexOf(group); - if ( index !== -1 ) { - this.groups.splice(this.groups.indexOf(group), 1); - } - }; - -}]) - -// The accordion directive simply sets up the directive controller -// and adds an accordion CSS class to itself element. -.directive('accordion', function () { - return { - restrict:'EA', - controller:'AccordionController', - transclude: true, - replace: false, - templateUrl: 'template/accordion/accordion.html' - }; -}) - -// The accordion-group directive indicates a block of html that will expand and collapse in an accordion -.directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) { - return { - require:'^accordion', // We need this directive to be inside an accordion - restrict:'EA', - transclude:true, // It transcludes the contents of the directive into the template - replace: true, // The element containing the directive will be replaced with the template - templateUrl:'template/accordion/accordion-group.html', - scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope - controller: ['$scope', function($scope) { - this.setHeading = function(element) { - this.heading = element; - }; - }], - link: function(scope, element, attrs, accordionCtrl) { - var getIsOpen, setIsOpen; - - accordionCtrl.addGroup(scope); - - scope.isOpen = false; - - if ( attrs.isOpen ) { - getIsOpen = $parse(attrs.isOpen); - setIsOpen = getIsOpen.assign; - - scope.$watch( - function watchIsOpen() { return getIsOpen(scope.$parent); }, - function updateOpen(value) { scope.isOpen = value; } - ); - - scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false; - } - - scope.$watch('isOpen', function(value) { - if ( value ) { - accordionCtrl.closeOthers(scope); - } - if ( setIsOpen ) { - setIsOpen(scope.$parent, value); - } - }); - } - }; -}]) - -// Use accordion-heading below an accordion-group to provide a heading containing HTML -// -// Heading containing HTML - -// -.directive('accordionHeading', function() { - return { - restrict: 'EA', - transclude: true, // Grab the contents to be used as the heading - template: '', // In effect remove this element! - replace: true, - require: '^accordionGroup', - compile: function(element, attr, transclude) { - return function link(scope, element, attr, accordionGroupCtrl) { - // Pass the heading to the accordion-group controller - // so that it can be transcluded into the right place in the template - // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] - accordionGroupCtrl.setHeading(transclude(scope, function() {})); - }; - } - }; -}) - -// Use in the accordion-group template to indicate where you want the heading to be transcluded -// You must provide the property on the accordion-group controller that will hold the transcluded element -//
      -// -// ... -//
      -.directive('accordionTransclude', function() { - return { - require: '^accordionGroup', - link: function(scope, element, attr, controller) { - scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { - if ( heading ) { - element.html(''); - element.append(heading); - } - }); - } - }; -}); - -angular.module("ui.bootstrap.alert", []).directive('alert', function () { - return { - restrict:'EA', - templateUrl:'template/alert/alert.html', - transclude:true, - replace:true, - scope: { - type: '=', - close: '&' - }, - link: function(scope, iElement, iAttrs, controller) { - scope.closeable = "close" in iAttrs; - } - }; -}); - -angular.module('ui.bootstrap.buttons', []) - - .constant('buttonConfig', { - activeClass:'active', - toggleEvent:'click' - }) - - .directive('btnRadio', ['buttonConfig', function (buttonConfig) { - var activeClass = buttonConfig.activeClass || 'active'; - var toggleEvent = buttonConfig.toggleEvent || 'click'; - - return { - - require:'ngModel', - link:function (scope, element, attrs, ngModelCtrl) { - - //model -> UI - ngModelCtrl.$render = function () { - element.toggleClass(activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); - }; - - //ui->model - element.bind(toggleEvent, function () { - if (!element.hasClass(activeClass)) { - scope.$apply(function () { - ngModelCtrl.$setViewValue(scope.$eval(attrs.btnRadio)); - ngModelCtrl.$render(); - }); - } - }); - } - }; -}]) - - .directive('btnCheckbox', ['buttonConfig', function (buttonConfig) { - - var activeClass = buttonConfig.activeClass || 'active'; - var toggleEvent = buttonConfig.toggleEvent || 'click'; - - return { - require:'ngModel', - link:function (scope, element, attrs, ngModelCtrl) { - - var trueValue = scope.$eval(attrs.btnCheckboxTrue); - var falseValue = scope.$eval(attrs.btnCheckboxFalse); - - trueValue = angular.isDefined(trueValue) ? trueValue : true; - falseValue = angular.isDefined(falseValue) ? falseValue : false; - - //model -> UI - ngModelCtrl.$render = function () { - element.toggleClass(activeClass, angular.equals(ngModelCtrl.$modelValue, trueValue)); - }; - - //ui->model - element.bind(toggleEvent, function () { - scope.$apply(function () { - ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue); - ngModelCtrl.$render(); - }); - }); - } - }; -}]); -/** -* @ngdoc overview -* @name ui.bootstrap.carousel -* -* @description -* AngularJS version of an image carousel. -* -*/ -angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) -.controller('CarouselController', ['$scope', '$timeout', '$transition', '$q', function ($scope, $timeout, $transition, $q) { - var self = this, - slides = self.slides = [], - currentIndex = -1, - currentTimeout, isPlaying; - self.currentSlide = null; - - /* direction: "prev" or "next" */ - self.select = function(nextSlide, direction) { - var nextIndex = slides.indexOf(nextSlide); - //Decide direction if it's not given - if (direction === undefined) { - direction = nextIndex > currentIndex ? "next" : "prev"; - } - if (nextSlide && nextSlide !== self.currentSlide) { - if ($scope.$currentTransition) { - $scope.$currentTransition.cancel(); - //Timeout so ng-class in template has time to fix classes for finished slide - $timeout(goNext); - } else { - goNext(); - } - } - function goNext() { - //If we have a slide to transition from and we have a transition type and we're allowed, go - if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { - //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime - nextSlide.$element.addClass(direction); - nextSlide.$element[0].offsetWidth = nextSlide.$element[0].offsetWidth; //force reflow - - //Set all other slides to stop doing their stuff for the new transition - angular.forEach(slides, function(slide) { - angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); - }); - angular.extend(nextSlide, {direction: direction, active: true, entering: true}); - angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); - - $scope.$currentTransition = $transition(nextSlide.$element, {}); - //We have to create new pointers inside a closure since next & current will change - (function(next,current) { - $scope.$currentTransition.then( - function(){ transitionDone(next, current); }, - function(){ transitionDone(next, current); } - ); - }(nextSlide, self.currentSlide)); - } else { - transitionDone(nextSlide, self.currentSlide); - } - self.currentSlide = nextSlide; - currentIndex = nextIndex; - //every time you change slides, reset the timer - restartTimer(); - } - function transitionDone(next, current) { - angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); - angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); - $scope.$currentTransition = null; - } - }; - - /* Allow outside people to call indexOf on slides array */ - self.indexOfSlide = function(slide) { - return slides.indexOf(slide); - }; - - $scope.next = function() { - var newIndex = (currentIndex + 1) % slides.length; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'next'); - } - }; - - $scope.prev = function() { - var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'prev'); - } - }; - - $scope.select = function(slide) { - self.select(slide); - }; - - $scope.isActive = function(slide) { - return self.currentSlide === slide; - }; - - $scope.slides = function() { - return slides; - }; - - $scope.$watch('interval', restartTimer); - function restartTimer() { - if (currentTimeout) { - $timeout.cancel(currentTimeout); - } - function go() { - if (isPlaying) { - $scope.next(); - restartTimer(); - } else { - $scope.pause(); - } - } - var interval = +$scope.interval; - if (!isNaN(interval) && interval>=0) { - currentTimeout = $timeout(go, interval); - } - } - $scope.play = function() { - if (!isPlaying) { - isPlaying = true; - restartTimer(); - } - }; - $scope.pause = function() { - if (!$scope.noPause) { - isPlaying = false; - if (currentTimeout) { - $timeout.cancel(currentTimeout); - } - } - }; - - self.addSlide = function(slide, element) { - slide.$element = element; - slides.push(slide); - //if this is the first slide or the slide is set to active, select it - if(slides.length === 1 || slide.active) { - self.select(slides[slides.length-1]); - if (slides.length == 1) { - $scope.play(); - } - } else { - slide.active = false; - } - }; - - self.removeSlide = function(slide) { - //get the index of the slide inside the carousel - var index = slides.indexOf(slide); - slides.splice(index, 1); - if (slides.length > 0 && slide.active) { - if (index >= slides.length) { - self.select(slides[index-1]); - } else { - self.select(slides[index]); - } - } else if (currentIndex > index) { - currentIndex--; - } - }; -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:carousel - * @restrict EA - * - * @description - * Carousel is the outer container for a set of image 'slides' to showcase. - * - * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. - * @param {boolean=} noTransition Whether to disable transitions on the carousel. - * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). - * - * @example - - - - - - - - - - - - - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - - - */ -.directive('carousel', [function() { - return { - restrict: 'EA', - transclude: true, - replace: true, - controller: 'CarouselController', - require: 'carousel', - templateUrl: 'template/carousel/carousel.html', - scope: { - interval: '=', - noTransition: '=', - noPause: '=' - } - }; -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:slide - * @restrict EA - * - * @description - * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. - * - * @param {boolean=} active Model binding, whether or not this slide is currently active. - * - * @example - - -
      - - - - - - -
      -
      -
        -
      • - - {{$index}}: {{slide.text}} -
      • -
      - Add Slide -
      -
      - Interval, in milliseconds: -
      Enter a negative number to stop the interval. -
      -
      -
      -
      - -function CarouselDemoCtrl($scope) { - $scope.myInterval = 5000; - var slides = $scope.slides = []; - $scope.addSlide = function() { - var newWidth = 200 + ((slides.length + (25 * slides.length)) % 150); - slides.push({ - image: 'http://placekitten.com/' + newWidth + '/200', - text: ['More','Extra','Lots of','Surplus'][slides.length % 4] + ' ' - ['Cats', 'Kittys', 'Felines', 'Cutes'][slides.length % 4] - }); - }; - for (var i=0; i<4; i++) $scope.addSlide(); -} - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - -
      -*/ - -.directive('slide', ['$parse', function($parse) { - return { - require: '^carousel', - restrict: 'EA', - transclude: true, - replace: true, - templateUrl: 'template/carousel/slide.html', - scope: { - }, - link: function (scope, element, attrs, carouselCtrl) { - //Set up optional 'active' = binding - if (attrs.active) { - var getActive = $parse(attrs.active); - var setActive = getActive.assign; - var lastValue = scope.active = getActive(scope.$parent); - scope.$watch(function parentActiveWatch() { - var parentActive = getActive(scope.$parent); - - if (parentActive !== scope.active) { - // we are out of sync and need to copy - if (parentActive !== lastValue) { - // parent changed and it has precedence - lastValue = scope.active = parentActive; - } else { - // if the parent can be assigned then do so - setActive(scope.$parent, parentActive = lastValue = scope.active); - } - } - return parentActive; - }); - } - - carouselCtrl.addSlide(scope, element); - //when the scope is destroyed then remove the slide from the current slides array - scope.$on('$destroy', function() { - carouselCtrl.removeSlide(scope); - }); - - scope.$watch('active', function(active) { - if (active) { - carouselCtrl.select(scope); - } - }); - } - }; -}]); - -angular.module('ui.bootstrap.datepicker', []) - -.constant('datepickerConfig', { - dayFormat: 'dd', - monthFormat: 'MMMM', - yearFormat: 'yyyy', - dayHeaderFormat: 'EEE', - dayTitleFormat: 'MMMM yyyy', - monthTitleFormat: 'yyyy', - showWeeks: true, - startingDay: 0, - yearRange: 20 -}) - -.directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', function (dateFilter, $parse, datepickerConfig) { - return { - restrict: 'EA', - replace: true, - scope: { - model: '=ngModel', - dateDisabled: '&' - }, - templateUrl: 'template/datepicker/datepicker.html', - link: function(scope, element, attrs) { - scope.mode = 'day'; // Initial mode - - // Configuration parameters - var selected = new Date(), showWeeks, minDate, maxDate, format = {}; - format.day = angular.isDefined(attrs.dayFormat) ? scope.$eval(attrs.dayFormat) : datepickerConfig.dayFormat; - format.month = angular.isDefined(attrs.monthFormat) ? scope.$eval(attrs.monthFormat) : datepickerConfig.monthFormat; - format.year = angular.isDefined(attrs.yearFormat) ? scope.$eval(attrs.yearFormat) : datepickerConfig.yearFormat; - format.dayHeader = angular.isDefined(attrs.dayHeaderFormat) ? scope.$eval(attrs.dayHeaderFormat) : datepickerConfig.dayHeaderFormat; - format.dayTitle = angular.isDefined(attrs.dayTitleFormat) ? scope.$eval(attrs.dayTitleFormat) : datepickerConfig.dayTitleFormat; - format.monthTitle = angular.isDefined(attrs.monthTitleFormat) ? scope.$eval(attrs.monthTitleFormat) : datepickerConfig.monthTitleFormat; - var startingDay = angular.isDefined(attrs.startingDay) ? scope.$eval(attrs.startingDay) : datepickerConfig.startingDay; - var yearRange = angular.isDefined(attrs.yearRange) ? scope.$eval(attrs.yearRange) : datepickerConfig.yearRange; - - if (attrs.showWeeks) { - scope.$parent.$watch($parse(attrs.showWeeks), function(value) { - showWeeks = !! value; - updateShowWeekNumbers(); - }); - } else { - showWeeks = datepickerConfig.showWeeks; - updateShowWeekNumbers(); - } - - if (attrs.min) { - scope.$parent.$watch($parse(attrs.min), function(value) { - minDate = new Date(value); - refill(); - }); - } - if (attrs.max) { - scope.$parent.$watch($parse(attrs.max), function(value) { - maxDate = new Date(value); - refill(); - }); - } - - function updateCalendar (rows, labels, title) { - scope.rows = rows; - scope.labels = labels; - scope.title = title; - } - - // Define whether the week number are visible - function updateShowWeekNumbers() { - scope.showWeekNumbers = ( scope.mode === 'day' && showWeeks ); - } - - function compare( date1, date2 ) { - if ( scope.mode === 'year') { - return date2.getFullYear() - date1.getFullYear(); - } else if ( scope.mode === 'month' ) { - return new Date( date2.getFullYear(), date2.getMonth() ) - new Date( date1.getFullYear(), date1.getMonth() ); - } else if ( scope.mode === 'day' ) { - return (new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) - new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) ); - } - } - - function isDisabled(date) { - return ((minDate && compare(date, minDate) > 0) || (maxDate && compare(date, maxDate) < 0) || (scope.dateDisabled && scope.dateDisabled({ date: date, mode: scope.mode }))); - } - - // Split array into smaller arrays - var split = function(a, size) { - var arrays = []; - while (a.length > 0) { - arrays.push(a.splice(0, size)); - } - return arrays; - }; - var getDaysInMonth = function( year, month ) { - return new Date(year, month + 1, 0).getDate(); - }; - - var fill = { - day: function() { - var days = [], labels = [], lastDate = null; - - function addDays( dt, n, isCurrentMonth ) { - for (var i =0; i < n; i ++) { - days.push( {date: new Date(dt), isCurrent: isCurrentMonth, isSelected: isSelected(dt), label: dateFilter(dt, format.day), disabled: isDisabled(dt) } ); - dt.setDate( dt.getDate() + 1 ); - } - lastDate = dt; - } - - var d = new Date(selected); - d.setDate(1); - - var difference = startingDay - d.getDay(); - var numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference; - - if ( numDisplayedFromPreviousMonth > 0 ) { - d.setDate( - numDisplayedFromPreviousMonth + 1 ); - addDays(d, numDisplayedFromPreviousMonth, false); - } - addDays(lastDate || d, getDaysInMonth(selected.getFullYear(), selected.getMonth()), true); - addDays(lastDate, (7 - days.length % 7) % 7, false); - - // Day labels - for (i = 0; i < 7; i++) { - labels.push( dateFilter(days[i].date, format.dayHeader) ); - } - updateCalendar( split( days, 7 ), labels, dateFilter(selected, format.dayTitle) ); - }, - month: function() { - var months = [], i = 0, year = selected.getFullYear(); - while ( i < 12 ) { - var dt = new Date(year, i++, 1); - months.push( {date: dt, isCurrent: true, isSelected: isSelected(dt), label: dateFilter(dt, format.month), disabled: isDisabled(dt)} ); - } - updateCalendar( split( months, 3 ), [], dateFilter(selected, format.monthTitle) ); - }, - year: function() { - var years = [], year = parseInt((selected.getFullYear() - 1) / yearRange, 10) * yearRange + 1; - for ( var i = 0; i < yearRange; i++ ) { - var dt = new Date(year + i, 0, 1); - years.push( {date: dt, isCurrent: true, isSelected: isSelected(dt), label: dateFilter(dt, format.year), disabled: isDisabled(dt)} ); - } - var title = years[0].label + ' - ' + years[years.length - 1].label; - updateCalendar( split( years, 5 ), [], title ); - } - }; - var refill = function() { - fill[scope.mode](); - }; - var isSelected = function( dt ) { - if ( scope.model && scope.model.getFullYear() === dt.getFullYear() ) { - if ( scope.mode === 'year' ) { - return true; - } - if ( scope.model.getMonth() === dt.getMonth() ) { - return ( scope.mode === 'month' || (scope.mode === 'day' && scope.model.getDate() === dt.getDate()) ); - } - } - return false; - }; - - scope.$watch('model', function ( dt, olddt ) { - if ( angular.isDate(dt) ) { - selected = angular.copy(dt); - } - - if ( ! angular.equals(dt, olddt) ) { - refill(); - } - }); - scope.$watch('mode', function() { - updateShowWeekNumbers(); - refill(); - }); - - scope.select = function( dt ) { - selected = new Date(dt); - - if ( scope.mode === 'year' ) { - scope.mode = 'month'; - selected.setFullYear( dt.getFullYear() ); - } else if ( scope.mode === 'month' ) { - scope.mode = 'day'; - selected.setMonth( dt.getMonth() ); - } else if ( scope.mode === 'day' ) { - scope.model = new Date(selected); - } - }; - scope.move = function(step) { - if (scope.mode === 'day') { - selected.setMonth( selected.getMonth() + step ); - } else if (scope.mode === 'month') { - selected.setFullYear( selected.getFullYear() + step ); - } else if (scope.mode === 'year') { - selected.setFullYear( selected.getFullYear() + step * yearRange ); - } - refill(); - }; - scope.toggleMode = function() { - scope.mode = ( scope.mode === 'day' ) ? 'month' : ( scope.mode === 'month' ) ? 'year' : 'day'; - }; - scope.getWeekNumber = function(row) { - if ( scope.mode !== 'day' || ! scope.showWeekNumbers || row.length !== 7 ) { - return; - } - - var index = ( startingDay > 4 ) ? 11 - startingDay : 4 - startingDay; // Thursday - var d = new Date( row[ index ].date ); - d.setHours(0, 0, 0); - return Math.ceil((((d - new Date(d.getFullYear(), 0, 1)) / 86400000) + 1) / 7); // 86400000 = 1000*60*60*24; - }; - } - }; -}]); -// The `$dialogProvider` can be used to configure global defaults for your -// `$dialog` service. -var dialogModule = angular.module('ui.bootstrap.dialog', ['ui.bootstrap.transition']); - -dialogModule.controller('MessageBoxController', ['$scope', 'dialog', 'model', function($scope, dialog, model){ - $scope.title = model.title; - $scope.message = model.message; - $scope.buttons = model.buttons; - $scope.close = function(res){ - dialog.close(res); - }; -}]); - -dialogModule.provider("$dialog", function(){ - - // The default options for all dialogs. - var defaults = { - backdrop: true, - dialogClass: 'modal', - backdropClass: 'modal-backdrop', - transitionClass: 'fade', - triggerClass: 'in', - resolve:{}, - backdropFade: true, - dialogFade: true, - keyboard: true, // close with esc key - backdropClick: true // only in conjunction with backdrop=true - /* other options: template, templateUrl, controller */ - }; - - var globalOptions = {}; - - var activeBackdrops = {value : 0}; - - // The `options({})` allows global configuration of all dialogs in the application. - // - // var app = angular.module('App', ['ui.bootstrap.dialog'], function($dialogProvider){ - // // don't close dialog when backdrop is clicked by default - // $dialogProvider.options({backdropClick: false}); - // }); - this.options = function(value){ - globalOptions = value; - }; - - // Returns the actual `$dialog` service that is injected in controllers - this.$get = ["$http", "$document", "$compile", "$rootScope", "$controller", "$templateCache", "$q", "$transition", "$injector", - function ($http, $document, $compile, $rootScope, $controller, $templateCache, $q, $transition, $injector) { - - var body = $document.find('body'); - - function createElement(clazz) { - var el = angular.element("
      "); - el.addClass(clazz); - return el; - } - - // The `Dialog` class represents a modal dialog. The dialog class can be invoked by providing an options object - // containing at lest template or templateUrl and controller: - // - // var d = new Dialog({templateUrl: 'foo.html', controller: 'BarController'}); - // - // Dialogs can also be created using templateUrl and controller as distinct arguments: - // - // var d = new Dialog('path/to/dialog.html', MyDialogController); - function Dialog(opts) { - - var self = this, options = this.options = angular.extend({}, defaults, globalOptions, opts); - this._open = false; - - this.backdropEl = createElement(options.backdropClass); - if(options.backdropFade){ - this.backdropEl.addClass(options.transitionClass); - this.backdropEl.removeClass(options.triggerClass); - } - - this.modalEl = createElement(options.dialogClass); - if(options.dialogFade){ - this.modalEl.addClass(options.transitionClass); - this.modalEl.removeClass(options.triggerClass); - } - this.modalEl.css("display", "block"); /* FIXME: For BS 3.x support */ - - this.handledEscapeKey = function(e) { - if (e.which === 27) { - self.close(); - e.preventDefault(); - self.$scope.$apply(); - } - }; - - this.handleBackDropClick = function(e) { - self.close(); - e.preventDefault(); - self.$scope.$apply(); - }; - - this.handleLocationChange = function() { - self.close(); - }; - } - - // The `isOpen()` method returns wether the dialog is currently visible. - Dialog.prototype.isOpen = function(){ - return this._open; - }; - - // The `open(templateUrl, controller)` method opens the dialog. - // Use the `templateUrl` and `controller` arguments if specifying them at dialog creation time is not desired. - Dialog.prototype.open = function(templateUrl, controller){ - var self = this, options = this.options; - - if(templateUrl){ - options.templateUrl = templateUrl; - } - if(controller){ - options.controller = controller; - } - - if(!(options.template || options.templateUrl)) { - throw new Error('Dialog.open expected template or templateUrl, neither found. Use options or open method to specify them.'); - } - - this._loadResolves().then(function(locals) { - var $scope = locals.$scope = self.$scope = locals.$scope ? locals.$scope : $rootScope.$new(); - - self.modalEl.html(locals.$template); - - if (self.options.controller) { - var ctrl = $controller(self.options.controller, locals); - self.modalEl.children().data('ngControllerController', ctrl); - } - - $compile(self.modalEl)($scope); - self._addElementsToDom(); - - // trigger tranisitions - setTimeout(function(){ - if(self.options.dialogFade){ self.modalEl.addClass(self.options.triggerClass); } - if(self.options.backdropFade){ self.backdropEl.addClass(self.options.triggerClass); } - }); - - self._bindEvents(); - }); - - this.deferred = $q.defer(); - return this.deferred.promise; - }; - - // closes the dialog and resolves the promise returned by the `open` method with the specified result. - Dialog.prototype.close = function(result){ - var self = this; - var fadingElements = this._getFadingElements(); - - if(fadingElements.length > 0){ - for (var i = fadingElements.length - 1; i >= 0; i--) { - $transition(fadingElements[i], removeTriggerClass).then(onCloseComplete); - } - return; - } - - this._onCloseComplete(result); - - function removeTriggerClass(el){ - el.removeClass(self.options.triggerClass); - } - - function onCloseComplete(){ - if(self._open){ - self._onCloseComplete(result); - } - } - }; - - Dialog.prototype._getFadingElements = function(){ - var elements = []; - if(this.options.dialogFade){ - elements.push(this.modalEl); - } - if(this.options.backdropFade){ - elements.push(this.backdropEl); - } - - return elements; - }; - - Dialog.prototype._bindEvents = function() { - if(this.options.keyboard){ body.bind('keydown', this.handledEscapeKey); } - if(this.options.backdrop && this.options.backdropClick){ this.backdropEl.bind('click', this.handleBackDropClick); } - }; - - Dialog.prototype._unbindEvents = function() { - if(this.options.keyboard){ body.unbind('keydown', this.handledEscapeKey); } - if(this.options.backdrop && this.options.backdropClick){ this.backdropEl.unbind('click', this.handleBackDropClick); } - }; - - Dialog.prototype._onCloseComplete = function(result) { - this._removeElementsFromDom(); - this._unbindEvents(); - - this.deferred.resolve(result); - }; - - Dialog.prototype._addElementsToDom = function(){ - body.append(this.modalEl); - - if(this.options.backdrop) { - if (activeBackdrops.value === 0) { - body.append(this.backdropEl); - } - activeBackdrops.value++; - } - - this._open = true; - }; - - Dialog.prototype._removeElementsFromDom = function(){ - this.modalEl.remove(); - - if(this.options.backdrop) { - activeBackdrops.value--; - if (activeBackdrops.value === 0) { - this.backdropEl.remove(); - } - } - this._open = false; - }; - - // Loads all `options.resolve` members to be used as locals for the controller associated with the dialog. - Dialog.prototype._loadResolves = function(){ - var values = [], keys = [], templatePromise, self = this; - - if (this.options.template) { - templatePromise = $q.when(this.options.template); - } else if (this.options.templateUrl) { - templatePromise = $http.get(this.options.templateUrl, {cache:$templateCache}) - .then(function(response) { return response.data; }); - } - - angular.forEach(this.options.resolve || [], function(value, key) { - keys.push(key); - values.push(angular.isString(value) ? $injector.get(value) : $injector.invoke(value)); - }); - - keys.push('$template'); - values.push(templatePromise); - - return $q.all(values).then(function(values) { - var locals = {}; - angular.forEach(values, function(value, index) { - locals[keys[index]] = value; - }); - locals.dialog = self; - return locals; - }); - }; - - // The actual `$dialog` service that is injected in controllers. - return { - // Creates a new `Dialog` with the specified options. - dialog: function(opts){ - return new Dialog(opts); - }, - // creates a new `Dialog` tied to the default message box template and controller. - // - // Arguments `title` and `message` are rendered in the modal header and body sections respectively. - // The `buttons` array holds an object with the following members for each button to include in the - // modal footer section: - // - // * `result`: the result to pass to the `close` method of the dialog when the button is clicked - // * `label`: the label of the button - // * `cssClass`: additional css class(es) to apply to the button for styling - messageBox: function(title, message, buttons){ - return new Dialog({templateUrl: 'template/dialog/message.html', controller: 'MessageBoxController', resolve: - {model: function() { - return { - title: title, - message: message, - buttons: buttons - }; - } - }}); - } - }; - }]; -}); - -/* - * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js - * @restrict class or attribute - * @example: - - */ - -angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) { - var openElement = null, - closeMenu = angular.noop; - return { - restrict: 'CA', - link: function(scope, element, attrs) { - scope.$watch('$location.path', function() { closeMenu(); }); - element.parent().bind('click', function() { closeMenu(); }); - element.bind('click', function (event) { - - var elementWasOpen = (element === openElement); - - event.preventDefault(); - event.stopPropagation(); - - if (!!openElement) { - closeMenu(); - } - - if (!elementWasOpen) { - element.parent().addClass('open'); - openElement = element; - closeMenu = function (event) { - if (event) { - event.preventDefault(); - event.stopPropagation(); - } - $document.unbind('click', closeMenu); - element.parent().removeClass('open'); - closeMenu = angular.noop; - openElement = null; - }; - $document.bind('click', closeMenu); - } - }); - } - }; -}]); -angular.module('ui.bootstrap.modal', ['ui.bootstrap.dialog']) -.directive('modal', ['$parse', '$dialog', function($parse, $dialog) { - return { - restrict: 'EA', - terminal: true, - link: function(scope, elm, attrs) { - var opts = angular.extend({}, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options)); - var shownExpr = attrs.modal || attrs.show; - var setClosed; - - // Create a dialog with the template as the contents of the directive - // Add the current scope as the resolve in order to make the directive scope as a dialog controller scope - opts = angular.extend(opts, { - template: elm.html(), - resolve: { $scope: function() { return scope; } } - }); - var dialog = $dialog.dialog(opts); - - elm.remove(); - - if (attrs.close) { - setClosed = function() { - $parse(attrs.close)(scope); - }; - } else { - setClosed = function() { - if (angular.isFunction($parse(shownExpr).assign)) { - $parse(shownExpr).assign(scope, false); - } - }; - } - - scope.$watch(shownExpr, function(isShown, oldShown) { - if (isShown) { - dialog.open().then(function(){ - setClosed(); - }); - } else { - //Make sure it is not opened - if (dialog.isOpen()){ - dialog.close(); - } - } - }); - } - }; -}]); -angular.module('ui.bootstrap.pagination', []) - -.controller('PaginationController', ['$scope', function (scope) { - - scope.noPrevious = function() { - return scope.currentPage === 1; - }; - scope.noNext = function() { - return scope.currentPage === scope.numPages; - }; - - scope.isActive = function(page) { - return scope.currentPage === page; - }; - - scope.selectPage = function(page) { - if ( ! scope.isActive(page) && page > 0 && page <= scope.numPages) { - scope.currentPage = page; - scope.onSelectPage({ page: page }); - } - }; -}]) - -.constant('paginationConfig', { - boundaryLinks: false, - directionLinks: true, - firstText: 'First', - previousText: 'Previous', - nextText: 'Next', - lastText: 'Last', - rotate: true -}) - -.directive('pagination', ['paginationConfig', function(paginationConfig) { - return { - restrict: 'EA', - scope: { - numPages: '=', - currentPage: '=', - maxSize: '=', - onSelectPage: '&' - }, - controller: 'PaginationController', - templateUrl: 'template/pagination/pagination.html', - replace: true, - link: function(scope, element, attrs) { - - // Setup configuration parameters - var boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; - var directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$eval(attrs.directionLinks) : paginationConfig.directionLinks; - var firstText = angular.isDefined(attrs.firstText) ? scope.$parent.$eval(attrs.firstText) : paginationConfig.firstText; - var previousText = angular.isDefined(attrs.previousText) ? scope.$parent.$eval(attrs.previousText) : paginationConfig.previousText; - var nextText = angular.isDefined(attrs.nextText) ? scope.$parent.$eval(attrs.nextText) : paginationConfig.nextText; - var lastText = angular.isDefined(attrs.lastText) ? scope.$parent.$eval(attrs.lastText) : paginationConfig.lastText; - var rotate = angular.isDefined(attrs.rotate) ? scope.$eval(attrs.rotate) : paginationConfig.rotate; - - // Create page object used in template - function makePage(number, text, isActive, isDisabled) { - return { - number: number, - text: text, - active: isActive, - disabled: isDisabled - }; - } - - scope.$watch('numPages + currentPage + maxSize', function() { - scope.pages = []; - - // Default page limits - var startPage = 1, endPage = scope.numPages; - var isMaxSized = ( angular.isDefined(scope.maxSize) && scope.maxSize < scope.numPages ); - - // recompute if maxSize - if ( isMaxSized ) { - if ( rotate ) { - // Current page is displayed in the middle of the visible ones - startPage = Math.max(scope.currentPage - Math.floor(scope.maxSize/2), 1); - endPage = startPage + scope.maxSize - 1; - - // Adjust if limit is exceeded - if (endPage > scope.numPages) { - endPage = scope.numPages; - startPage = endPage - scope.maxSize + 1; - } - } else { - // Visible pages are paginated with maxSize - startPage = ((Math.ceil(scope.currentPage / scope.maxSize) - 1) * scope.maxSize) + 1; - - // Adjust last page if limit is exceeded - endPage = Math.min(startPage + scope.maxSize - 1, scope.numPages); - } - } - - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, scope.isActive(number), false); - scope.pages.push(page); - } - - // Add links to move between page sets - if ( isMaxSized && ! rotate ) { - if ( startPage > 1 ) { - var previousPageSet = makePage(startPage - 1, '...', false, false); - scope.pages.unshift(previousPageSet); - } - - if ( endPage < scope.numPages ) { - var nextPageSet = makePage(endPage + 1, '...', false, false); - scope.pages.push(nextPageSet); - } - } - - // Add previous & next links - if (directionLinks) { - var previousPage = makePage(scope.currentPage - 1, previousText, false, scope.noPrevious()); - scope.pages.unshift(previousPage); - - var nextPage = makePage(scope.currentPage + 1, nextText, false, scope.noNext()); - scope.pages.push(nextPage); - } - - // Add first & last links - if (boundaryLinks) { - var firstPage = makePage(1, firstText, false, scope.noPrevious()); - scope.pages.unshift(firstPage); - - var lastPage = makePage(scope.numPages, lastText, false, scope.noNext()); - scope.pages.push(lastPage); - } - - if ( scope.currentPage > scope.numPages ) { - scope.selectPage(scope.numPages); - } - }); - } - }; -}]) - -.constant('pagerConfig', { - previousText: '« Previous', - nextText: 'Next »', - align: true -}) - -.directive('pager', ['pagerConfig', function(config) { - return { - restrict: 'EA', - scope: { - numPages: '=', - currentPage: '=', - onSelectPage: '&' - }, - controller: 'PaginationController', - templateUrl: 'template/pagination/pager.html', - replace: true, - link: function(scope, element, attrs, paginationCtrl) { - - // Setup configuration parameters - var previousText = angular.isDefined(attrs.previousText) ? scope.$parent.$eval(attrs.previousText) : config.previousText; - var nextText = angular.isDefined(attrs.nextText) ? scope.$parent.$eval(attrs.nextText) : config.nextText; - var align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : config.align; - - // Create page object used in template - function makePage(number, text, isDisabled, isPrevious, isNext) { - return { - number: number, - text: text, - disabled: isDisabled, - previous: ( align && isPrevious ), - next: ( align && isNext ) - }; - } - - scope.$watch('numPages + currentPage', function() { - scope.pages = []; - - // Add previous & next links - var previousPage = makePage(scope.currentPage - 1, previousText, scope.noPrevious(), true, false); - scope.pages.unshift(previousPage); - - var nextPage = makePage(scope.currentPage + 1, nextText, scope.noNext(), false, true); - scope.pages.push(nextPage); - - if ( scope.currentPage > scope.numPages ) { - scope.selectPage(scope.numPages); - } - }); - } - }; -}]); - -angular.module('ui.bootstrap.position', []) - -/** - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, - * typeahead suggestions etc.). - */ - .factory('$position', ['$document', '$window', function ($document, $window) { - - var mouseX, mouseY; - - $document.bind('mousemove', function mouseMoved(event) { - mouseX = event.pageX; - mouseY = event.pageY; - }); - - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; - } - // finally try and get inline style - return el.style[cssprop]; - } - - /** - * Checks if a given element is statically positioned - * @param element - raw DOM element - */ - function isStaticPositioned(element) { - return (getStyle(element, "position") || 'static' ) === 'static'; - } - - /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element - */ - var parentOffsetEl = function (element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; - - return { - /** - * Provides read-only equivalent of jQuery's position function: - * http://api.jquery.com/position/ - */ - position: function (element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop; - offsetParentBCR.left += offsetParentEl.clientLeft; - } - - return { - width: element.prop('offsetWidth'), - height: element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; - }, - - /** - * Provides read-only equivalent of jQuery's offset function: - * http://api.jquery.com/offset/ - */ - offset: function (element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: element.prop('offsetWidth'), - height: element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft) - }; - }, - - /** - * Provides the coordinates of the mouse - */ - mouse: function () { - return {x: mouseX, y: mouseY}; - } - }; - }]); - -/** - * The following features are still outstanding: animation as a - * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html tooltips, and selector delegation. - */ -angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position' ] ) - -/** - * The $tooltip service creates tooltip- and popover-like directives as well as - * houses global options for them. - */ -.provider( '$tooltip', function () { - // The default options tooltip and popover. - var defaultOptions = { - placement: 'top', - animation: true, - popupDelay: 0 - }; - - // Default hide triggers for each show trigger - var triggerMap = { - 'mouseenter': 'mouseleave', - 'click': 'click', - 'focus': 'blur' - }; - - // The options specified to the provider globally. - var globalOptions = {}; - - /** - * `options({})` allows global configuration of all tooltips in the - * application. - * - * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { - * // place tooltips left instead of top by default - * $tooltipProvider.options( { placement: 'left' } ); - * }); - */ - this.options = function( value ) { - angular.extend( globalOptions, value ); - }; - - /** - * This allows you to extend the set of trigger mappings available. E.g.: - * - * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); - */ - this.setTriggers = function setTriggers ( triggers ) { - angular.extend( triggerMap, triggers ); - }; - - /** - * This is a helper function for translating camel-case to snake-case. - */ - function snake_case(name){ - var regexp = /[A-Z]/g; - var separator = '-'; - return name.replace(regexp, function(letter, pos) { - return (pos ? separator : '') + letter.toLowerCase(); - }); - } - - /** - * Returns the actual instance of the $tooltip service. - * TODO support multiple triggers - */ - this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) { - return function $tooltip ( type, prefix, defaultTriggerShow ) { - var options = angular.extend( {}, defaultOptions, globalOptions ); - - /** - * Returns an object of show and hide triggers. - * - * If a trigger is supplied, - * it is used to show the tooltip; otherwise, it will use the `trigger` - * option passed to the `$tooltipProvider.options` method; else it will - * default to the trigger supplied to this directive factory. - * - * The hide trigger is based on the show trigger. If the `trigger` option - * was passed to the `$tooltipProvider.options` method, it will use the - * mapped trigger from `triggerMap` or the passed trigger if the map is - * undefined; otherwise, it uses the `triggerMap` value of the show - * trigger; else it will just use the show trigger. - */ - function setTriggers ( trigger ) { - var show, hide; - - show = trigger || options.trigger || defaultTriggerShow; - if ( angular.isDefined ( options.trigger ) ) { - hide = triggerMap[options.trigger] || show; - } else { - hide = triggerMap[show] || show; - } - - return { - show: show, - hide: hide - }; - } - - var directiveName = snake_case( type ); - var triggers = setTriggers( undefined ); - - var startSym = $interpolate.startSymbol(); - var endSym = $interpolate.endSymbol(); - var template = - '<'+ directiveName +'-popup '+ - 'title="'+startSym+'tt_title'+endSym+'" '+ - 'content="'+startSym+'tt_content'+endSym+'" '+ - 'placement="'+startSym+'tt_placement'+endSym+'" '+ - 'animation="tt_animation()" '+ - 'is-open="tt_isOpen"'+ - '>'+ - ''; - - return { - restrict: 'EA', - scope: true, - link: function link ( scope, element, attrs ) { - var tooltip = $compile( template )( scope ); - var transitionTimeout; - var popupTimeout; - var $body; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - - // By default, the tooltip is not open. - // TODO add ability to start tooltip opened - scope.tt_isOpen = false; - - function toggleTooltipBind () { - if ( ! scope.tt_isOpen ) { - showTooltipBind(); - } else { - hideTooltipBind(); - } - } - - // Show the tooltip with delay if specified, otherwise show it immediately - function showTooltipBind() { - if ( scope.tt_popupDelay ) { - popupTimeout = $timeout( show, scope.tt_popupDelay ); - } else { - scope.$apply( show ); - } - } - - function hideTooltipBind () { - scope.$apply(function () { - hide(); - }); - } - - // Show the tooltip popup element. - function show() { - var position, - ttWidth, - ttHeight, - ttPosition; - - // Don't show empty tooltips. - if ( ! scope.tt_content ) { - return; - } - - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - } - - // Set the initial positioning. - tooltip.css({ top: 0, left: 0, display: 'block' }); - - // Now we add it to the DOM because need some info about it. But it's not - // visible yet anyway. - if ( appendToBody ) { - $body = $body || $document.find( 'body' ); - $body.append( tooltip ); - } else { - element.after( tooltip ); - } - - // Get the position of the directive element. - position = options.appendToBody ? $position.offset( element ) : $position.position( element ); - - // Get the height and width of the tooltip so we can center it. - ttWidth = tooltip.prop( 'offsetWidth' ); - ttHeight = tooltip.prop( 'offsetHeight' ); - - // Calculate the tooltip's top and left coordinates to center it with - // this directive. - switch ( scope.tt_placement ) { - case 'mouse': - var mousePos = $position.mouse(); - ttPosition = { - top: mousePos.y, - left: mousePos.x - }; - break; - case 'right': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left + position.width - }; - break; - case 'bottom': - ttPosition = { - top: position.top + position.height, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; - case 'left': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left - ttWidth - }; - break; - default: - ttPosition = { - top: position.top - ttHeight, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; - } - - ttPosition.top += 'px'; - ttPosition.left += 'px'; - - // Now set the calculated positioning. - tooltip.css( ttPosition ); - - // And show the tooltip. - scope.tt_isOpen = true; - } - - // Hide the tooltip popup element. - function hide() { - // First things first: we don't show it anymore. - scope.tt_isOpen = false; - - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - - // And now we remove it from the DOM. However, if we have animation, we - // need to wait for it to expire beforehand. - // FIXME: this is a placeholder for a port of the transitions library. - if ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) { - transitionTimeout = $timeout( function () { tooltip.remove(); }, 500 ); - } else { - tooltip.remove(); - } - } - - /** - * Observe the relevant attributes. - */ - attrs.$observe( type, function ( val ) { - scope.tt_content = val; - }); - - attrs.$observe( prefix+'Title', function ( val ) { - scope.tt_title = val; - }); - - attrs.$observe( prefix+'Placement', function ( val ) { - scope.tt_placement = angular.isDefined( val ) ? val : options.placement; - }); - - attrs.$observe( prefix+'Animation', function ( val ) { - scope.tt_animation = angular.isDefined( val ) ? $parse( val ) : function(){ return options.animation; }; - }); - - attrs.$observe( prefix+'PopupDelay', function ( val ) { - var delay = parseInt( val, 10 ); - scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; - }); - - attrs.$observe( prefix+'Trigger', function ( val ) { - element.unbind( triggers.show ); - element.unbind( triggers.hide ); - - triggers = setTriggers( val ); - - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); - } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); - } - }); - - attrs.$observe( prefix+'AppendToBody', function ( val ) { - appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; - }); - - // if a tooltip is attached to we need to remove it on - // location change as its parent scope will probably not be destroyed - // by the change. - if ( appendToBody ) { - scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( scope.tt_isOpen ) { - hide(); - } - }); - } - - // Make sure tooltip is destroyed and removed. - scope.$on('$destroy', function onDestroyTooltip() { - if ( scope.tt_isOpen ) { - hide(); - } else { - tooltip.remove(); - } - }); - } - }; - }; - }]; -}) - -.directive( 'tooltipPopup', function () { - return { - restrict: 'E', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' - }; -}) - -.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); -}]) - -.directive( 'tooltipHtmlUnsafePopup', function () { - return { - restrict: 'E', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' - }; -}) - -.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); -}]); - -/** - * The following features are still outstanding: popup delay, animation as a - * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html popovers, and selector delegatation. - */ -angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) -.directive( 'popoverPopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/popover/popover.html' - }; -}) -.directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', '$tooltip', function ( $compile, $timeout, $parse, $window, $tooltip ) { - return $tooltip( 'popover', 'popover', 'click' ); -}]); - - -angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition']) - -.constant('progressConfig', { - animate: true, - autoType: false, - stackedTypes: ['success', 'info', 'warning', 'danger'] -}) - -.controller('ProgressBarController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { - - // Whether bar transitions should be animated - var animate = angular.isDefined($attrs.animate) ? $scope.$eval($attrs.animate) : progressConfig.animate; - var autoType = angular.isDefined($attrs.autoType) ? $scope.$eval($attrs.autoType) : progressConfig.autoType; - var stackedTypes = angular.isDefined($attrs.stackedTypes) ? $scope.$eval('[' + $attrs.stackedTypes + ']') : progressConfig.stackedTypes; - - // Create bar object - this.makeBar = function(newBar, oldBar, index) { - var newValue = (angular.isObject(newBar)) ? newBar.value : (newBar || 0); - var oldValue = (angular.isObject(oldBar)) ? oldBar.value : (oldBar || 0); - var type = (angular.isObject(newBar) && angular.isDefined(newBar.type)) ? newBar.type : (autoType) ? getStackedType(index || 0) : null; - - return { - from: oldValue, - to: newValue, - type: type, - animate: animate - }; - }; - - function getStackedType(index) { - return stackedTypes[index]; - } - - this.addBar = function(bar) { - $scope.bars.push(bar); - $scope.totalPercent += bar.to; - }; - - this.clearBars = function() { - $scope.bars = []; - $scope.totalPercent = 0; - }; - this.clearBars(); -}]) - -.directive('progress', function() { - return { - restrict: 'EA', - replace: true, - controller: 'ProgressBarController', - scope: { - value: '=percent', - onFull: '&', - onEmpty: '&' - }, - templateUrl: 'template/progressbar/progress.html', - link: function(scope, element, attrs, controller) { - scope.$watch('value', function(newValue, oldValue) { - controller.clearBars(); - - if (angular.isArray(newValue)) { - // Stacked progress bar - for (var i=0, n=newValue.length; i < n; i++) { - controller.addBar(controller.makeBar(newValue[i], oldValue[i], i)); - } - } else { - // Simple bar - controller.addBar(controller.makeBar(newValue, oldValue)); - } - }, true); - - // Total percent listeners - scope.$watch('totalPercent', function(value) { - if (value >= 100) { - scope.onFull(); - } else if (value <= 0) { - scope.onEmpty(); - } - }, true); - } - }; -}) - -.directive('progressbar', ['$transition', function($transition) { - return { - restrict: 'EA', - replace: true, - scope: { - width: '=', - old: '=', - type: '=', - animate: '=' - }, - templateUrl: 'template/progressbar/bar.html', - link: function(scope, element) { - scope.$watch('width', function(value) { - if (scope.animate) { - element.css('width', scope.old + '%'); - $transition(element, {width: value + '%'}); - } else { - element.css('width', value + '%'); - } - }); - } - }; -}]); -angular.module('ui.bootstrap.rating', []) - -.constant('ratingConfig', { - max: 5 -}) - -.directive('rating', ['ratingConfig', '$parse', function(ratingConfig, $parse) { - return { - restrict: 'EA', - scope: { - value: '=' - }, - templateUrl: 'template/rating/rating.html', - replace: true, - link: function(scope, element, attrs) { - - var maxRange = angular.isDefined(attrs.max) ? scope.$eval(attrs.max) : ratingConfig.max; - - scope.range = []; - for (var i = 1; i <= maxRange; i++) { - scope.range.push(i); - } - - scope.rate = function(value) { - if ( ! scope.readonly ) { - scope.value = value; - } - }; - - scope.enter = function(value) { - if ( ! scope.readonly ) { - scope.val = value; - } - }; - - scope.reset = function() { - scope.val = angular.copy(scope.value); - }; - scope.reset(); - - scope.$watch('value', function(value) { - scope.val = value; - }); - - scope.readonly = false; - if (attrs.readonly) { - scope.$parent.$watch($parse(attrs.readonly), function(value) { - scope.readonly = !!value; - }); - } - } - }; -}]); - -/** - * @ngdoc overview - * @name ui.bootstrap.tabs - * - * @description - * AngularJS version of the tabs directive. - */ - -angular.module('ui.bootstrap.tabs', []) - -.directive('tabs', function() { - return function() { - throw new Error("The `tabs` directive is deprecated, please migrate to `tabset`. Instructions can be found at http://github.com/angular-ui/bootstrap/tree/master/CHANGELOG.md"); - }; -}) - -.controller('TabsetController', ['$scope', '$element', -function TabsetCtrl($scope, $element) { - var ctrl = this, - tabs = ctrl.tabs = $scope.tabs = []; - - ctrl.select = function(tab) { - angular.forEach(tabs, function(tab) { - tab.active = false; - }); - tab.active = true; - }; - - ctrl.addTab = function addTab(tab) { - tabs.push(tab); - if (tabs.length == 1) { - ctrl.select(tab); - } - }; - - ctrl.removeTab = function removeTab(tab) { - var index = tabs.indexOf(tab); - //Select a new tab if the tab to be removed is selected - if (tab.active && tabs.length > 1) { - //If this is the last tab, select the previous tab. else, the next tab. - var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; - ctrl.select(tabs[newActiveIndex]); - } - tabs.splice(index, 1); - }; -}]) - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabset - * @restrict EA - * - * @description - * Tabset is the outer container for the tabs directive - * - * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. - * - * @example - - - - First Content! - Second Content! - -
      - - First Vertical Content! - Second Vertical Content! - -
      -
      - */ -.directive('tabset', function() { - return { - restrict: 'EA', - transclude: true, - scope: {}, - controller: 'TabsetController', - templateUrl: 'template/tabs/tabset.html', - link: function(scope, element, attrs) { - scope.vertical = angular.isDefined(attrs.vertical) ? scope.$eval(attrs.vertical) : false; - scope.type = angular.isDefined(attrs.type) ? scope.$parent.$eval(attrs.type) : 'tabs'; - } - }; -}) - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tab - * @restrict EA - * - * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. - * @param {string=} select An expression to evaluate when the tab is selected. - * @param {boolean=} active A binding, telling whether or not this tab is selected. - * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. - * - * @description - * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. - * - * @example - - -
      - - -
      - - First Tab - - Alert me! - Second Tab, with alert callback and html heading! - - - {{item.content}} - - -
      -
      - - function TabsDemoCtrl($scope) { - $scope.items = [ - { title:"Dynamic Title 1", content:"Dynamic Item 0" }, - { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } - ]; - - $scope.alertMe = function() { - setTimeout(function() { - alert("You've selected the alert tab!"); - }); - }; - }; - -
      - */ - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabHeading - * @restrict EA - * - * @description - * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. - * - * @example - - - - - HTML in my titles?! - And some content, too! - - - Icon heading?!? - That's right. - - - - - */ -.directive('tab', ['$parse', '$http', '$templateCache', '$compile', -function($parse, $http, $templateCache, $compile) { - return { - require: '^tabset', - restrict: 'EA', - replace: true, - templateUrl: 'template/tabs/tab.html', - transclude: true, - scope: { - heading: '@', - onSelect: '&select' //This callback is called in contentHeadingTransclude - //once it inserts the tab's content into the dom - }, - controller: function() { - //Empty controller so other directives can require being 'under' a tab - }, - compile: function(elm, attrs, transclude) { - return function postLink(scope, elm, attrs, tabsetCtrl) { - var getActive, setActive; - scope.active = false; // default value - if (attrs.active) { - getActive = $parse(attrs.active); - setActive = getActive.assign; - scope.$parent.$watch(getActive, function updateActive(value) { - if ( !!value && scope.disabled ) { - setActive(scope.$parent, false); // Prevent active assignment - } else { - scope.active = !!value; - } - }); - } else { - setActive = getActive = angular.noop; - } - - scope.$watch('active', function(active) { - setActive(scope.$parent, active); - if (active) { - tabsetCtrl.select(scope); - scope.onSelect(); - } - }); - - scope.disabled = false; - if ( attrs.disabled ) { - scope.$parent.$watch($parse(attrs.disabled), function(value) { - scope.disabled = !! value; - }); - } - - scope.select = function() { - if ( ! scope.disabled ) { - scope.active = true; - } - }; - - tabsetCtrl.addTab(scope); - scope.$on('$destroy', function() { - tabsetCtrl.removeTab(scope); - }); - //If the tabset sets this tab to active, set the parent scope's active - //binding too. We do this so the watch for the parent's initial active - //value won't overwrite what is initially set by the tabset - if (scope.active) { - setActive(scope.$parent, true); - } - - //Transclude the collection of sibling elements. Use forEach to find - //the heading if it exists. We don't use a directive for tab-heading - //because it is problematic. Discussion @ http://git.io/MSNPwQ - transclude(scope.$parent, function(clone) { - //Look at every element in the clone collection. If it's tab-heading, - //mark it as that. If it's not tab-heading, mark it as tab contents - var contents = [], heading; - angular.forEach(clone, function(el) { - //See if it's a tab-heading attr or element directive - //First make sure it's a normal element, one that has a tagName - if (el.tagName && - (el.hasAttribute("tab-heading") || - el.hasAttribute("data-tab-heading") || - el.tagName.toLowerCase() == "tab-heading" || - el.tagName.toLowerCase() == "data-tab-heading" - )) { - heading = el; - } else { - contents.push(el); - } - }); - //Share what we found on the scope, so our tabHeadingTransclude and - //tabContentTransclude directives can find out what the heading and - //contents are. - if (heading) { - scope.headingElement = angular.element(heading); - } - scope.contentElement = angular.element(contents); - }); - }; - } - }; -}]) - -.directive('tabHeadingTransclude', [function() { - return { - restrict: 'A', - require: '^tab', - link: function(scope, elm, attrs, tabCtrl) { - scope.$watch('headingElement', function updateHeadingElement(heading) { - if (heading) { - elm.html(''); - elm.append(heading); - } - }); - } - }; -}]) - -.directive('tabContentTransclude', ['$parse', function($parse) { - return { - restrict: 'A', - require: '^tabset', - link: function(scope, elm, attrs, tabsetCtrl) { - scope.$watch($parse(attrs.tabContentTransclude), function(tab) { - elm.html(''); - if (tab) { - elm.append(tab.contentElement); - } - }); - } - }; -}]) - -; - - -angular.module('ui.bootstrap.timepicker', []) - -.filter('pad', function() { - return function(input) { - if ( angular.isDefined(input) && input.toString().length < 2 ) { - input = '0' + input; - } - return input; - }; -}) - -.constant('timepickerConfig', { - hourStep: 1, - minuteStep: 1, - showMeridian: true, - meridians: ['AM', 'PM'], - readonlyInput: false, - mousewheel: true -}) - -.directive('timepicker', ['padFilter', '$parse', 'timepickerConfig', function (padFilter, $parse, timepickerConfig) { - return { - restrict: 'EA', - require:'ngModel', - replace: true, - templateUrl: 'template/timepicker/timepicker.html', - scope: { - model: '=ngModel' - }, - link: function(scope, element, attrs, ngModelCtrl) { - var selected = new Date(), meridians = timepickerConfig.meridians; - - var hourStep = timepickerConfig.hourStep; - if (attrs.hourStep) { - scope.$parent.$watch($parse(attrs.hourStep), function(value) { - hourStep = parseInt(value, 10); - }); - } - - var minuteStep = timepickerConfig.minuteStep; - if (attrs.minuteStep) { - scope.$parent.$watch($parse(attrs.minuteStep), function(value) { - minuteStep = parseInt(value, 10); - }); - } - - // 12H / 24H mode - scope.showMeridian = timepickerConfig.showMeridian; - if (attrs.showMeridian) { - scope.$parent.$watch($parse(attrs.showMeridian), function(value) { - scope.showMeridian = !! value; - - if ( ! scope.model ) { - // Reset - var dt = new Date( selected ); - var hours = getScopeHours(); - if (angular.isDefined( hours )) { - dt.setHours( hours ); - } - scope.model = new Date( dt ); - } else { - refreshTemplate(); - } - }); - } - - // Get scope.hours in 24H mode if valid - function getScopeHours ( ) { - var hours = parseInt( scope.hours, 10 ); - var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); - if ( !valid ) { - return; - } - - if ( scope.showMeridian ) { - if ( hours === 12 ) { - hours = 0; - } - if ( scope.meridian === meridians[1] ) { - hours = hours + 12; - } - } - return hours; - } - - // Input elements - var inputs = element.find('input'); - var hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1); - - // Respond on mousewheel spin - var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel; - if ( mousewheel ) { - - var isScrollingUp = function(e) { - if (e.originalEvent) { - e = e.originalEvent; - } - return (e.detail || e.wheelDelta > 0); - }; - - hoursInputEl.bind('mousewheel', function(e) { - scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() ); - e.preventDefault(); - }); - - minutesInputEl.bind('mousewheel', function(e) { - scope.$apply( (isScrollingUp(e)) ? scope.incrementMinutes() : scope.decrementMinutes() ); - e.preventDefault(); - }); - } - - var keyboardChange = false; - scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput; - if ( ! scope.readonlyInput ) { - scope.updateHours = function() { - var hours = getScopeHours(); - - if ( angular.isDefined(hours) ) { - keyboardChange = 'h'; - if ( scope.model === null ) { - scope.model = new Date( selected ); - } - scope.model.setHours( hours ); - } else { - scope.model = null; - scope.validHours = false; - } - }; - - hoursInputEl.bind('blur', function(e) { - if ( scope.validHours && scope.hours < 10) { - scope.$apply( function() { - scope.hours = padFilter( scope.hours ); - }); - } - }); - - scope.updateMinutes = function() { - var minutes = parseInt(scope.minutes, 10); - if ( minutes >= 0 && minutes < 60 ) { - keyboardChange = 'm'; - if ( scope.model === null ) { - scope.model = new Date( selected ); - } - scope.model.setMinutes( minutes ); - } else { - scope.model = null; - scope.validMinutes = false; - } - }; - - minutesInputEl.bind('blur', function(e) { - if ( scope.validMinutes && scope.minutes < 10 ) { - scope.$apply( function() { - scope.minutes = padFilter( scope.minutes ); - }); - } - }); - } else { - scope.updateHours = angular.noop; - scope.updateMinutes = angular.noop; - } - - scope.$watch( function getModelTimestamp() { - return +scope.model; - }, function( timestamp ) { - if ( !isNaN( timestamp ) && timestamp > 0 ) { - selected = new Date( timestamp ); - refreshTemplate(); - } - }); - - function refreshTemplate() { - var hours = selected.getHours(); - if ( scope.showMeridian ) { - // Convert 24 to 12 hour system - hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; - } - scope.hours = ( keyboardChange === 'h' ) ? hours : padFilter(hours); - scope.validHours = true; - - var minutes = selected.getMinutes(); - scope.minutes = ( keyboardChange === 'm' ) ? minutes : padFilter(minutes); - scope.validMinutes = true; - - scope.meridian = ( scope.showMeridian ) ? (( selected.getHours() < 12 ) ? meridians[0] : meridians[1]) : ''; - - keyboardChange = false; - } - - function addMinutes( minutes ) { - var dt = new Date( selected.getTime() + minutes * 60000 ); - if ( dt.getDate() !== selected.getDate()) { - dt.setDate( dt.getDate() - 1 ); - } - selected.setTime( dt.getTime() ); - scope.model = new Date( selected ); - } - - scope.incrementHours = function() { - addMinutes( hourStep * 60 ); - }; - scope.decrementHours = function() { - addMinutes( - hourStep * 60 ); - }; - scope.incrementMinutes = function() { - addMinutes( minuteStep ); - }; - scope.decrementMinutes = function() { - addMinutes( - minuteStep ); - }; - scope.toggleMeridian = function() { - addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); - }; - } - }; -}]); -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) - -/** - * A helper service that can parse typeahead's syntax (string provided by users) - * Extracted to a separate service for ease of unit testing - */ - .factory('typeaheadParser', ['$parse', function ($parse) { - - // 00000111000000000000022200000000000000003333333333333330000000000044000 - var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; - - return { - parse:function (input) { - - var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source; - if (!match) { - throw new Error( - "Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + - " but got '" + input + "'."); - } - - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; - } - }; -}]) - - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { - - var HOT_KEYS = [9, 13, 27, 38, 40]; - - return { - require:'ngModel', - link:function (originalScope, element, attrs, modelCtrl) { - - var selected; - - //minimal no of characters that needs to be entered before typeahead kicks-in - var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; - - //minimal wait time after last character typed before typehead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; - - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); - - //should it restrict model values to the ones selected from the popup only? - var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; - - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; - - var onSelectCallback = $parse(attrs.typeaheadOnSelect); - - //pop-up element used to display matches - var popUpEl = angular.element(''); - popUpEl.attr({ - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx)', - query: 'query', - position: 'position' - }); - - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var scope = originalScope.$new(); - originalScope.$on('$destroy', function(){ - scope.$destroy(); - }); - - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - }; - - var getMatchesAsync = function(inputValue) { - - var locals = {$viewValue: inputValue}; - isLoadingSetter(originalScope, true); - $q.when(parserResult.source(scope, locals)).then(function(matches) { - - //it might happen that several async queries were in progress if a user were typing fast - //but we are interested only in responses that correspond to the current view value - if (inputValue === modelCtrl.$viewValue) { - if (matches.length > 0) { - - scope.activeIdx = 0; - scope.matches.length = 0; - - //transform labels - for(var i=0; i= minSearch) { - if (waitTime > 0) { - if (timeoutId) { - $timeout.cancel(timeoutId);//cancel previous timeout - } - timeoutId = $timeout(function () { - getMatchesAsync(inputValue); - }, waitTime); - } else { - getMatchesAsync(inputValue); - } - } - } - - return isEditable ? inputValue : undefined; - }); - - modelCtrl.$render = function () { - var locals = {}; - locals[parserResult.itemName] = selected || modelCtrl.$viewValue; - element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue); - selected = undefined; - }; - - scope.select = function (activeIdx) { - //called from within the $digest() cycle - var locals = {}; - var model, item; - locals[parserResult.itemName] = item = selected = scope.matches[activeIdx].model; - - model = parserResult.modelMapper(scope, locals); - modelCtrl.$setViewValue(model); - modelCtrl.$render(); - onSelectCallback(scope, { - $item: item, - $model: model, - $label: parserResult.viewMapper(scope, locals) - }); - - element[0].focus(); - }; - - //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) - element.bind('keydown', function (evt) { - - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; - } - - evt.preventDefault(); - - if (evt.which === 40) { - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; - scope.$digest(); - - } else if (evt.which === 38) { - scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - - } else if (evt.which === 13 || evt.which === 9) { - scope.$apply(function () { - scope.select(scope.activeIdx); - }); - - } else if (evt.which === 27) { - evt.stopPropagation(); - - resetMatches(); - scope.$digest(); - } - }); - - $document.bind('click', function(){ - resetMatches(); - scope.$digest(); - }); - - element.after($compile(popUpEl)(scope)); - } - }; - -}]) - - .directive('typeaheadPopup', function () { - return { - restrict:'E', - scope:{ - matches:'=', - query:'=', - active:'=', - position:'=', - select:'&' - }, - replace:true, - templateUrl:'template/typeahead/typeahead.html', - link:function (scope, element, attrs) { - - scope.isOpen = function () { - return scope.matches.length > 0; - }; - - scope.isActive = function (matchIdx) { - return scope.active == matchIdx; - }; - - scope.selectActive = function (matchIdx) { - scope.active = matchIdx; - }; - - scope.selectMatch = function (activeIdx) { - scope.select({activeIdx:activeIdx}); - }; - } - }; - }) - - .filter('typeaheadHighlight', function() { - - function escapeRegexp(queryToEscape) { - return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); - } - - return function(matchItem, query) { - return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : query; - }; - }); - -angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/accordion/accordion-group.html", - "
      \n" + - " \n" + - "
      \n" + - "
      \n" + - "
      "); -}]); - -angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/accordion/accordion.html", - "
      "); -}]); - -angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/alert/alert.html", - "
      \n" + - " \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/carousel/carousel.html", - "
      \n" + - "
        1\">\n" + - "
      1. \n" + - "
      \n" + - "
      \n" + - " 1\">‹\n" + - " 1\">›\n" + - "
      \n" + - ""); -}]); - -angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/carousel/slide.html", - "
      \n" + - ""); -}]); - -angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/datepicker.html", - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 0\">\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
      #{{label}}
      {{ getWeekNumber(row) }}\n" + - " \n" + - "
      \n" + - ""); -}]); - -angular.module("template/dialog/message.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/dialog/message.html", - "
      \n" + - "
      \n" + - "
      \n" + - "

      {{ title }}

      \n" + - "
      \n" + - "
      \n" + - "

      \n" + - "
      \n" + - "
      \n" + - " \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/backdrop.html", - "
      "); -}]); - -angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/window.html", - "
      "); -}]); - -angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pager.html", - "
      \n" + - " \n" + - "
      \n" + - ""); -}]); - -angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pagination.html", - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-popup.html", - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/popover/popover.html", - "
      \n" + - "
      \n" + - "\n" + - "
      \n" + - "

      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/bar.html", - "
      "); -}]); - -angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/progress.html", - "
      "); -}]); - -angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/rating/rating.html", - "\n" + - " val}\">\n" + - "\n" + - ""); -}]); - -angular.module("template/tabs/pane.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/pane.html", - "
      \n" + - ""); -}]); - -angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tab.html", - "
    • \n" + - " {{heading}}\n" + - "
    • \n" + - ""); -}]); - -angular.module("template/tabs/tabs.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabs.html", - "
      \n" + - " \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabset.html", - "\n" + - "
      \n" + - "
        \n" + - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - ""); -}]); - -angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/timepicker/timepicker.html", - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
       
      :
       
      "); -}]); - -angular.module("template/typeahead/typeahead.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead.html", - "
        \n" + - "
      • \n" + - " \n" + - "
      • \n" + - "
      "); -}]);