diff --git a/etc/cas/config/cas.properties b/etc/cas/config/cas.properties index 8710bf6f..1c8bad4f 100644 --- a/etc/cas/config/cas.properties +++ b/etc/cas/config/cas.properties @@ -15,6 +15,12 @@ cas.server.prefix=${cas.server.name} # Tomcat Server # cas.server.tomcat.server-name=OSF CAS +# +# Dev Mode Options +# +cas.server.dev-mode.allow-force-authn-exception=${ALLOW_FORCE_AUTHN_EXCEPTION:false} +cas.server.dev-mode.allow-force-http-error=${ALLOW_FORCE_HTTP_ERROR:false} +# ######################################################################################################################## ######################################################################################################################## @@ -90,6 +96,12 @@ cas.logout.remove-descendant-tickets=false # cas.authn.osf-url.home=https://{{ .Values.osfDomain }}/ cas.authn.osf-url.dashboard=https://{{ .Values.osfDomain }}/dashboard/ +cas.authn.osf-url.search=https://{{ .Values.osfDomain }}/search/ +cas.authn.osf-url.support=https://help.osf.io +cas.authn.osf-url.registries=https://{{ .Values.osfDomain }}/registries/discover/ +cas.authn.osf-url.preprints=https://{{ .Values.osfDomain }}/preprints/discover/ +cas.authn.osf-url.meetings=https://{{ .Values.osfDomain }}/meetings/ +cas.authn.osf-url.donate=https://www.cos.io/support-cos cas.authn.osf-url.login-with-next=https://{{ .Values.osfDomain }}/login?next= cas.authn.osf-url.logout=https://{{ .Values.osfDomain }}/logout/ cas.authn.osf-url.resend-confirmation=https://{{ .Values.osfDomain }}/resend/ diff --git a/etc/cas/config/local/cas-local.properties b/etc/cas/config/local/cas-local.properties index 0070f64a..6626573c 100644 --- a/etc/cas/config/local/cas-local.properties +++ b/etc/cas/config/local/cas-local.properties @@ -21,6 +21,11 @@ cas.server.tomcat.server-name=OSF CAS # cas.server.tomcat.http.enabled=true # cas.server.tomcat.http.attributes= # e.g. cas.server.tomcat.http.attributes.{attribute-name}={attributeValue} +# +# Dev Mode Options +# +cas.server.dev-mode.allow-force-authn-exception=true +cas.server.dev-mode.allow-force-http-error=true ######################################################################################################################## ######################################################################################################################## @@ -97,6 +102,12 @@ cas.logout.remove-descendant-tickets=false # cas.authn.osf-url.home=http://localhost:5000/ cas.authn.osf-url.dashboard=http://localhost:5000/dashboard/ +cas.authn.osf-url.search=http://localhost:5000/search/ +cas.authn.osf-url.support=https://help.osf.io +cas.authn.osf-url.registries=http://localhost:5000/registries/discover/ +cas.authn.osf-url.preprints=http://localhost:5000/preprints/discover/ +cas.authn.osf-url.meetings=http://localhost:5000/meetings/ +cas.authn.osf-url.donate=https://www.cos.io/support-cos cas.authn.osf-url.login-with-next=http://localhost:5000/login?next= cas.authn.osf-url.logout=http://localhost:5000/logout/ cas.authn.osf-url.resend-confirmation=http://localhost:5000/resend/ diff --git a/src/main/java/io/cos/cas/osf/configuration/model/DevModeProperties.java b/src/main/java/io/cos/cas/osf/configuration/model/DevModeProperties.java new file mode 100644 index 00000000..c5b509d6 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/configuration/model/DevModeProperties.java @@ -0,0 +1,34 @@ +package io.cos.cas.osf.configuration.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * This is {@link DevModeProperties}. + * + * @author Longze Chen + * @since 25.1.0 + */ +@Getter +@Setter +@Accessors(chain = true) +public class DevModeProperties implements Serializable { + + /** + * Serialization metadata. + */ + private static final long serialVersionUID = -1725182183570276203L; + + /** + * Allow CAS to force throw authentication exceptions and to render respective error pages for testing purpose. + */ + private boolean allowForceAuthnException = Boolean.FALSE; + + /** + * Allow CAS to force http errors which have built-in rendering template for rendering and testing. + */ + private boolean allowForceHttpError = Boolean.FALSE; +} diff --git a/src/main/java/io/cos/cas/osf/configuration/model/OsfUrlProperties.java b/src/main/java/io/cos/cas/osf/configuration/model/OsfUrlProperties.java index be506153..84ec8aa9 100644 --- a/src/main/java/io/cos/cas/osf/configuration/model/OsfUrlProperties.java +++ b/src/main/java/io/cos/cas/osf/configuration/model/OsfUrlProperties.java @@ -36,6 +36,36 @@ public class OsfUrlProperties implements Serializable { */ private String dashboard; + /** + * OSF search page URL. + */ + private String search; + + /** + * OSF support page URL. + */ + private String support; + + /** + * OSF registries page URL. + */ + private String registries; + + /** + * OSF preprints page URL. + */ + private String preprints; + + /** + * OSF meetings page URL. + */ + private String meetings; + + /** + * OSF donate page URL. + */ + private String donate; + /** * OSF sign-up page URL. */ diff --git a/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java b/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java index a8f309ca..20fce1db 100644 --- a/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java +++ b/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java @@ -101,6 +101,7 @@ public Action osfNonInteractiveAuthenticationCheckAction() { adaptiveAuthenticationPolicy.getObject(), centralAuthenticationService.getObject(), jpaOsfDao.getObject(), + casProperties.getServer().getDevMode(), casProperties.getAuthn().getOsfUrl(), casProperties.getAuthn().getOsfApi(), authnDelegationClients diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java index 56e5dc29..142b1785 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java @@ -94,7 +94,7 @@ protected Event doExecute(RequestContext context) { if (institutionId != null) { institutionLoginUrlMapSorted = institutionLoginUrlMap; } else { - institutionLoginUrlMap.put("", " -- select an institution -- "); + institutionLoginUrlMap.put("", " Select institution "); institutionLoginUrlMapSorted = OsfInstitutionUtils.sortByValue(institutionLoginUrlMap); } context.getFlowScope().put("institutions", institutionLoginUrlMapSorted); diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java index 7b883cf1..03f5c5f0 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java @@ -18,6 +18,7 @@ import io.cos.cas.osf.authentication.support.DelegationProtocol; import io.cos.cas.osf.authentication.support.OsfApiPermissionDenied; import io.cos.cas.osf.authentication.support.OsfInstitutionUtils; +import io.cos.cas.osf.configuration.model.DevModeProperties; import io.cos.cas.osf.configuration.model.OsfApiProperties; import io.cos.cas.osf.configuration.model.OsfUrlProperties; import io.cos.cas.osf.dao.JpaOsfDao; @@ -81,6 +82,7 @@ import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import javax.security.auth.login.AccountException; +import javax.security.auth.login.LoginException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -155,6 +157,8 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon private static final String VERIFICATION_KEY_PARAMETER_NAME = "verification_key"; + private static final String FORCE_EXCEPTION_PARAMETER_NAME = "forceException"; + private static final String OSF_URL_FLOW_PARAMETER = "osfUrl"; private static final String AUTHENTICATION_EXCEPTION = "authnError"; @@ -194,6 +198,9 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon @NotNull private final JpaOsfDao jpaOsfDao; + @NotNull + private DevModeProperties devModeProperties; + @NotNull private OsfUrlProperties osfUrlProperties; @@ -211,6 +218,7 @@ public OsfPrincipalFromNonInteractiveCredentialsAction( final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy, final CentralAuthenticationService centralAuthenticationService, final JpaOsfDao jpaOsfDao, + final DevModeProperties devModeProperties, final OsfUrlProperties osfUrlProperties, final OsfApiProperties osfApiProperties, final Map> authnDelegationClients @@ -222,6 +230,7 @@ public OsfPrincipalFromNonInteractiveCredentialsAction( ); this.centralAuthenticationService = centralAuthenticationService; this.jpaOsfDao = jpaOsfDao; + this.devModeProperties = devModeProperties; this.osfUrlProperties = osfUrlProperties; this.osfApiProperties = osfApiProperties; this.authnDelegationClients = authnDelegationClients; @@ -328,6 +337,27 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex } LOGGER.debug("No valid username or verification key found in request parameters."); + // Check 4: check "forceException=" query parameter if in dev mode + if (devModeProperties.isAllowForceAuthnException()) { + final String forcedException = request.getParameter(FORCE_EXCEPTION_PARAMETER_NAME); + if (StringUtils.isNotBlank(forcedException)) { + setSsoErrorContext( + context, + forcedException, + String.format("This exception was thrown on purpose bypassing standard web flow: %s", Class.forName(forcedException).getSimpleName()), + "N/A", + "N/A", + "N/A", + "N/A" + ); + try { + throw (LoginException) Class.forName(forcedException).getConstructor(String.class).newInstance(FORCE_EXCEPTION_PARAMETER_NAME); + } catch (java.lang.ClassCastException e) { + throw (RuntimeException) Class.forName(forcedException).getConstructor(String.class).newInstance(FORCE_EXCEPTION_PARAMETER_NAME); + } + } + } + // Default when there is no non-interactive authentication available // Type 5: return a null credential so that the login webflow will prepare login pages return null; diff --git a/src/main/java/org/apereo/cas/config/CasWebAppConfiguration.java b/src/main/java/org/apereo/cas/config/CasWebAppConfiguration.java index 108ec7cf..d638b0b0 100644 --- a/src/main/java/org/apereo/cas/config/CasWebAppConfiguration.java +++ b/src/main/java/org/apereo/cas/config/CasWebAppConfiguration.java @@ -1,5 +1,7 @@ package org.apereo.cas.config; +import org.apache.http.HttpStatus; + import org.apereo.cas.configuration.CasConfigurationProperties; import lombok.val; @@ -29,7 +31,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Optional; @@ -44,6 +49,15 @@ @EnableConfigurationProperties(CasConfigurationProperties.class) public class CasWebAppConfiguration implements WebMvcConfigurer { + private static final List HTTP_ERROR_WITH_TEMPLATES = List.of( + HttpStatus.SC_UNAUTHORIZED, + HttpStatus.SC_FORBIDDEN, + HttpStatus.SC_NOT_FOUND, + HttpStatus.SC_METHOD_NOT_ALLOWED, + HttpStatus.SC_LOCKED, + HttpStatus.SC_INTERNAL_SERVER_ERROR + ); + @Autowired private CasConfigurationProperties casProperties; @@ -96,8 +110,10 @@ protected UrlFilenameViewController passThroughController() { protected Controller rootController() { return new ParameterizableViewController() { @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, - final HttpServletResponse response) { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) { val queryString = request.getQueryString(); val url = request.getContextPath() + "/login" + Optional.ofNullable(queryString).map(string -> '?' + string).orElse(StringUtils.EMPTY); @@ -108,6 +124,41 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, }; } + /** + * OSF CAS Customization: implement a new controller to support testing error pages with templates in + * "resources/templates/error/" (401, 403, 404, 405 and 423) and with the CAS unavailable template of + * "templates/error.html" (500). + * + * @return {@code null} + */ + @Bean + protected Controller forceHttpErrorController() { + return new ParameterizableViewController() { + @Override + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws IOException { + // TODO: disable this for production environment + var errorCodeString = request.getParameter("code"); + if (StringUtils.isNotBlank(errorCodeString)) { + try { + var errorCode = Integer.parseInt(errorCodeString); + if (HTTP_ERROR_WITH_TEMPLATES.contains(errorCode)) { + response.sendError(errorCode); + return null; + } + } catch (NumberFormatException e) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return null; + } + } + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return null; + } + }; + } + @Bean public SimpleUrlHandlerMapping handlerMapping() { val mapping = new SimpleUrlHandlerMapping(); @@ -118,6 +169,10 @@ public SimpleUrlHandlerMapping handlerMapping() { mapping.setRootHandler(root); val urls = new HashMap(); urls.put("/", root); + if (casProperties.getServer().getDevMode().isAllowForceHttpError()) { + val forceHttpError = forceHttpErrorController(); + urls.put("/forceHttpError", forceHttpError); + } mapping.setUrlMap(urls); return mapping; diff --git a/src/main/java/org/apereo/cas/configuration/model/core/CasServerProperties.java b/src/main/java/org/apereo/cas/configuration/model/core/CasServerProperties.java new file mode 100644 index 00000000..1b931faa --- /dev/null +++ b/src/main/java/org/apereo/cas/configuration/model/core/CasServerProperties.java @@ -0,0 +1,80 @@ +package org.apereo.cas.configuration.model.core; + +import io.cos.cas.osf.configuration.model.DevModeProperties; + +import org.apereo.cas.CasProtocolConstants; +import org.apereo.cas.configuration.model.core.web.tomcat.CasEmbeddedApacheTomcatProperties; +import org.apereo.cas.configuration.support.RequiredProperty; +import org.apereo.cas.configuration.support.RequiresModule; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import java.io.Serializable; + +/** + * This is {@link CasServerProperties}. + * + *

OSF CAS Customization: add {@link DevModeProperties devMode} to {@link CasServerProperties}; this allows + * dev mode options to be easily accessed from {@link org.apereo.cas.configuration.CasConfigurationProperties} + * by calling {@code casProperties.getServer().getDevMode()}.

+ * + * @author Misagh Moayyed + * @since 5.0.0 + * + * @author Longze Chen + * @since osf-cas 25.1.0 + */ +@RequiresModule(name = "cas-server-core", automated = true) +@Getter +@Setter +@Accessors(chain = true) +public class CasServerProperties implements Serializable { + + private static final long serialVersionUID = 7876382696803430817L; + + /** + * Full name of the CAS server. This is the public-facing address + * of the CAS deployment and not the individual node address, + * in the event that CAS is clustered. + */ + @RequiredProperty + private String name = "https://cas.example.org:8443"; + + /** + * A concatenation of the server name plus the CAS context path. + * Deployments at root likely need to blank out this value. + */ + @RequiredProperty + private String prefix = name.concat("/cas"); + + /** + * The CAS Server scope. + */ + @RequiredProperty + private String scope = "example.org"; + + /** + * Configuration settings that control the embedded Apache Tomcat container. + */ + @NestedConfigurationProperty + private CasEmbeddedApacheTomcatProperties tomcat = new CasEmbeddedApacheTomcatProperties(); + + // OSF CAS Customization: a new private field devMode that stores osf-cas customized dev mode options + @NestedConfigurationProperty + private DevModeProperties devMode = new DevModeProperties(); + + @JsonIgnore + public String getLoginUrl() { + return getPrefix().concat(CasProtocolConstants.ENDPOINT_LOGIN); + } + + @JsonIgnore + public String getLogoutUrl() { + return getPrefix().concat(CasProtocolConstants.ENDPOINT_LOGOUT); + } + +} diff --git a/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java b/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java new file mode 100644 index 00000000..6009fcfa --- /dev/null +++ b/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.servlet; + +import org.apereo.cas.configuration.CasConfigurationProperties; + +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.server.ErrorPage; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; +import org.springframework.web.context.request.RequestContextListener; +import org.springframework.web.filter.RequestContextFilter; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC + * infrastructure when a separate management context with a web server running on a + * different port is required. + * + *

OSF CAS Customization: introduce a new field {@link CasConfigurationProperties casProperties} to configuration + * class {@link WebMvcEndpointChildContextConfiguration}; this allows the configuration to initiate osf-cas-customized + * {@link DispatcherServlet dispatcherServlet} with {@link io.cos.cas.osf.configuration.model.OsfUrlProperties}.

+ * + * @author Stephane Nicoll + * @author Andy Wilkinson + * @author Phillip Webb + * + * @author Longze Chen + * @since osf-cas 25.1.0 + */ +@ManagementContextConfiguration(ManagementContextType.CHILD) +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass(DispatcherServlet.class) +@EnableWebMvc +@EnableConfigurationProperties(CasConfigurationProperties.class) +class WebMvcEndpointChildContextConfiguration { + + // OSF CAS Customization: a new private field casProperties, in which osfUrlProperties is embedded + @Autowired + private CasConfigurationProperties casProperties; + + /* + * The error controller is present but not mapped as an endpoint in this context + * because of the DispatcherServlet having had its HandlerMapping explicitly disabled. + * So we expose the same feature but only for machine endpoints. + */ + @Bean + @ConditionalOnBean(ErrorAttributes.class) + ManagementErrorEndpoint errorEndpoint(ErrorAttributes errorAttributes) { + return new ManagementErrorEndpoint(errorAttributes); + } + + @Bean + @ConditionalOnBean(ErrorAttributes.class) + ManagementErrorPageCustomizer managementErrorPageCustomizer(ServerProperties serverProperties) { + return new ManagementErrorPageCustomizer(serverProperties); + } + + @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) + DispatcherServlet dispatcherServlet() { + DispatcherServlet dispatcherServlet = new DispatcherServlet(); + // OSF CAS Customization: set OsfUrlProperties for dispatcherServlet + dispatcherServlet.setOsfUrlProperties(casProperties.getAuthn().getOsfUrl()); + // Ensure the parent configuration does not leak down to us + dispatcherServlet.setDetectAllHandlerAdapters(false); + dispatcherServlet.setDetectAllHandlerExceptionResolvers(false); + dispatcherServlet.setDetectAllHandlerMappings(false); + dispatcherServlet.setDetectAllViewResolvers(false); + return dispatcherServlet; + } + + @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) + DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) { + return new DispatcherServletRegistrationBean(dispatcherServlet, "/"); + } + + @Bean(name = DispatcherServlet.HANDLER_MAPPING_BEAN_NAME) + CompositeHandlerMapping compositeHandlerMapping() { + return new CompositeHandlerMapping(); + } + + @Bean(name = DispatcherServlet.HANDLER_ADAPTER_BEAN_NAME) + CompositeHandlerAdapter compositeHandlerAdapter(ListableBeanFactory beanFactory) { + return new CompositeHandlerAdapter(beanFactory); + } + + @Bean(name = DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME) + CompositeHandlerExceptionResolver compositeHandlerExceptionResolver() { + return new CompositeHandlerExceptionResolver(); + } + + @Bean + @ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class }) + RequestContextFilter requestContextFilter() { + return new OrderedRequestContextFilter(); + } + + /** + * {@link WebServerFactoryCustomizer} to add an {@link ErrorPage} so that the + * {@link ManagementErrorEndpoint} can be used. + */ + private static class ManagementErrorPageCustomizer + implements WebServerFactoryCustomizer, Ordered { + + private final ServerProperties properties; + + ManagementErrorPageCustomizer(ServerProperties properties) { + this.properties = properties; + } + + @Override + public void customize(ConfigurableServletWebServerFactory factory) { + factory.addErrorPages(new ErrorPage(this.properties.getError().getPath())); + } + + @Override + public int getOrder() { + return 0; + } + + } + +} diff --git a/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java b/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java new file mode 100644 index 00000000..69c686d9 --- /dev/null +++ b/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java @@ -0,0 +1,230 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.servlet; + +import java.util.Arrays; +import java.util.List; + +import javax.servlet.MultipartConfigElement; +import javax.servlet.ServletRegistration; + +import org.apereo.cas.configuration.CasConfigurationProperties; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.http.HttpProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for the Spring + * {@link DispatcherServlet}. Should work for a standalone application where an embedded + * web server is already present and also for a deployable application using + * {@link SpringBootServletInitializer}. + * + *

OSF CAS Customization: introduce a new field {@link CasConfigurationProperties casProperties} to configuration + * class {@link DispatcherServletAutoConfiguration}; this allows the configuration to initiate osf-cas-customized + * {@link DispatcherServlet dispatcherServlet} with {@link io.cos.cas.osf.configuration.model.OsfUrlProperties}.

+ * + * @author Phillip Webb + * @author Dave Syer + * @author Stephane Nicoll + * @author Brian Clozel + * @since 2.0.0 + * + * @author Longze Chen + * @since osf-cas 25.1.0 + */ +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass(DispatcherServlet.class) +@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) +@EnableConfigurationProperties(CasConfigurationProperties.class) +public class DispatcherServletAutoConfiguration { + + // OSF CAS Customization: a new private field casProperties, in which osfUrlProperties is embedded + @Autowired + private CasConfigurationProperties casProperties; + + /* + * The bean name for a DispatcherServlet that will be mapped to the root URL "/" + */ + public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet"; + + /* + * The bean name for a ServletRegistrationBean for the DispatcherServlet "/" + */ + public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration"; + + @Configuration(proxyBeanMethods = false) + @Conditional(DefaultDispatcherServletCondition.class) + @ConditionalOnClass(ServletRegistration.class) + @EnableConfigurationProperties({ HttpProperties.class, WebMvcProperties.class, CasConfigurationProperties.class}) + protected static class DispatcherServletConfiguration { + + @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) + public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties, CasConfigurationProperties casProperties) { + DispatcherServlet dispatcherServlet = new DispatcherServlet(); + // OSF CAS Customization: set OsfUrlProperties for dispatcherServlet + dispatcherServlet.setOsfUrlProperties(casProperties.getAuthn().getOsfUrl()); + dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); + dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest()); + dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound()); + dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents()); + dispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails()); + return dispatcherServlet; + } + + @Bean + @ConditionalOnBean(MultipartResolver.class) + @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) + public MultipartResolver multipartResolver(MultipartResolver resolver) { + // Detect if the user has created a MultipartResolver but named it incorrectly + return resolver; + } + + } + + @Configuration(proxyBeanMethods = false) + @Conditional(DispatcherServletRegistrationCondition.class) + @ConditionalOnClass(ServletRegistration.class) + @EnableConfigurationProperties(WebMvcProperties.class) + @Import(DispatcherServletConfiguration.class) + protected static class DispatcherServletRegistrationConfiguration { + + @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) + @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) + public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, + WebMvcProperties webMvcProperties, ObjectProvider multipartConfig) { + DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, + webMvcProperties.getServlet().getPath()); + registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); + registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); + multipartConfig.ifAvailable(registration::setMultipartConfig); + return registration; + } + + } + + @Order(Ordered.LOWEST_PRECEDENCE - 10) + private static class DefaultDispatcherServletCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage.forCondition("Default DispatcherServlet"); + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + List dispatchServletBeans = Arrays + .asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false)); + if (dispatchServletBeans.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) { + return ConditionOutcome + .noMatch(message.found("dispatcher servlet bean").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)); + } + if (beanFactory.containsBean(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) { + return ConditionOutcome.noMatch( + message.found("non dispatcher servlet bean").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)); + } + if (dispatchServletBeans.isEmpty()) { + return ConditionOutcome.match(message.didNotFind("dispatcher servlet beans").atAll()); + } + return ConditionOutcome.match(message.found("dispatcher servlet bean", "dispatcher servlet beans") + .items(Style.QUOTE, dispatchServletBeans) + .append("and none is named " + DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)); + } + + } + + @Order(Ordered.LOWEST_PRECEDENCE - 10) + private static class DispatcherServletRegistrationCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + ConditionOutcome outcome = checkDefaultDispatcherName(beanFactory); + if (!outcome.isMatch()) { + return outcome; + } + return checkServletRegistration(beanFactory); + } + + private ConditionOutcome checkDefaultDispatcherName(ConfigurableListableBeanFactory beanFactory) { + List servlets = Arrays + .asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false)); + boolean containsDispatcherBean = beanFactory.containsBean(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); + if (containsDispatcherBean && !servlets.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) { + return ConditionOutcome.noMatch( + startMessage().found("non dispatcher servlet").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)); + } + return ConditionOutcome.match(); + } + + private ConditionOutcome checkServletRegistration(ConfigurableListableBeanFactory beanFactory) { + ConditionMessage.Builder message = startMessage(); + List registrations = Arrays + .asList(beanFactory.getBeanNamesForType(ServletRegistrationBean.class, false, false)); + boolean containsDispatcherRegistrationBean = beanFactory + .containsBean(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME); + if (registrations.isEmpty()) { + if (containsDispatcherRegistrationBean) { + return ConditionOutcome.noMatch(message.found("non servlet registration bean") + .items(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)); + } + return ConditionOutcome.match(message.didNotFind("servlet registration bean").atAll()); + } + if (registrations.contains(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)) { + return ConditionOutcome.noMatch(message.found("servlet registration bean") + .items(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)); + } + if (containsDispatcherRegistrationBean) { + return ConditionOutcome.noMatch(message.found("non servlet registration bean") + .items(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)); + } + return ConditionOutcome.match(message.found("servlet registration beans").items(Style.QUOTE, registrations) + .append("and none is named " + DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)); + } + + private ConditionMessage.Builder startMessage() { + return ConditionMessage.forCondition("DispatcherServlet Registration"); + } + + } + +} diff --git a/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/src/main/java/org/springframework/web/servlet/DispatcherServlet.java new file mode 100644 index 00000000..08ea8b53 --- /dev/null +++ b/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -0,0 +1,1494 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.servlet.DispatcherType; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.cos.cas.osf.configuration.model.OsfUrlProperties; + +import lombok.Setter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.i18n.LocaleContext; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.core.log.LogFormatUtils; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.lang.Nullable; +import org.springframework.ui.context.ThemeSource; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.util.NestedServletException; +import org.springframework.web.util.WebUtils; + +/** + * Central dispatcher for HTTP request handlers/controllers, e.g. for web UI controllers + * or HTTP-based remote service exporters. Dispatches to registered handlers for processing + * a web request, providing convenient mapping and exception handling facilities. + * + *

This servlet is very flexible: It can be used with just about any workflow, with the + * installation of the appropriate adapter classes. It offers the following functionality + * that distinguishes it from other request-driven web MVC frameworks: + * + *

    + *
  • It is based around a JavaBeans configuration mechanism. + * + *
  • It can use any {@link HandlerMapping} implementation - pre-built or provided as part + * of an application - to control the routing of requests to handler objects. Default is + * {@link org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping} and + * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping}. + * HandlerMapping objects can be defined as beans in the servlet's application context, + * implementing the HandlerMapping interface, overriding the default HandlerMapping if + * present. HandlerMappings can be given any bean name (they are tested by type). + * + *
  • It can use any {@link HandlerAdapter}; this allows for using any handler interface. + * Default adapters are {@link org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter}, + * {@link org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter}, for Spring's + * {@link org.springframework.web.HttpRequestHandler} and + * {@link org.springframework.web.servlet.mvc.Controller} interfaces, respectively. A default + * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter} + * will be registered as well. HandlerAdapter objects can be added as beans in the + * application context, overriding the default HandlerAdapters. Like HandlerMappings, + * HandlerAdapters can be given any bean name (they are tested by type). + * + *
  • The dispatcher's exception resolution strategy can be specified via a + * {@link HandlerExceptionResolver}, for example mapping certain exceptions to error pages. + * Default are + * {@link org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver}, + * {@link org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver}, and + * {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver}. + * These HandlerExceptionResolvers can be overridden through the application context. + * HandlerExceptionResolver can be given any bean name (they are tested by type). + * + *
  • Its view resolution strategy can be specified via a {@link ViewResolver} + * implementation, resolving symbolic view names into View objects. Default is + * {@link org.springframework.web.servlet.view.InternalResourceViewResolver}. + * ViewResolver objects can be added as beans in the application context, overriding the + * default ViewResolver. ViewResolvers can be given any bean name (they are tested by type). + * + *
  • If a {@link View} or view name is not supplied by the user, then the configured + * {@link RequestToViewNameTranslator} will translate the current request into a view name. + * The corresponding bean name is "viewNameTranslator"; the default is + * {@link org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator}. + * + *
  • The dispatcher's strategy for resolving multipart requests is determined by a + * {@link org.springframework.web.multipart.MultipartResolver} implementation. + * Implementations for Apache Commons FileUpload and Servlet 3 are included; the typical + * choice is {@link org.springframework.web.multipart.commons.CommonsMultipartResolver}. + * The MultipartResolver bean name is "multipartResolver"; default is none. + * + *
  • Its locale resolution strategy is determined by a {@link LocaleResolver}. + * Out-of-the-box implementations work via HTTP accept header, cookie, or session. + * The LocaleResolver bean name is "localeResolver"; default is + * {@link org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver}. + * + *
  • Its theme resolution strategy is determined by a {@link ThemeResolver}. + * Implementations for a fixed theme and for cookie and session storage are included. + * The ThemeResolver bean name is "themeResolver"; default is + * {@link org.springframework.web.servlet.theme.FixedThemeResolver}. + *
+ * + *

NOTE: The {@code @RequestMapping} annotation will only be processed if a + * corresponding {@code HandlerMapping} (for type-level annotations) and/or + * {@code HandlerAdapter} (for method-level annotations) is present in the dispatcher. + * This is the case by default. However, if you are defining custom {@code HandlerMappings} + * or {@code HandlerAdapters}, then you need to make sure that a corresponding custom + * {@code RequestMappingHandlerMapping} and/or {@code RequestMappingHandlerAdapter} + * is defined as well - provided that you intend to use {@code @RequestMapping}. + * + *

A web application can define any number of DispatcherServlets. + * Each servlet will operate in its own namespace, loading its own application context + * with mappings, handlers, etc. Only the root application context as loaded by + * {@link org.springframework.web.context.ContextLoaderListener}, if any, will be shared. + * + *

As of Spring 3.1, {@code DispatcherServlet} may now be injected with a web + * application context, rather than creating its own internally. This is useful in Servlet + * 3.0+ environments, which support programmatic registration of servlet instances. + * See the {@link #DispatcherServlet(WebApplicationContext)} javadoc for details. + * + *

OSF CAS Customization: introduce a new private field {@link OsfUrlProperties osfUrlProperties} to class + * {@link DispatcherServlet}; this allows method {@link this#resolveViewName(String, Map, Locale, HttpServletRequest)} + * to send OSF URLs down to the view via model if resolved successfully.

+ * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Chris Beams + * @author Rossen Stoyanchev + * @see org.springframework.web.HttpRequestHandler + * @see org.springframework.web.servlet.mvc.Controller + * @see org.springframework.web.context.ContextLoaderListener + * + * @author Longze Chen + * @since osf-cas 25.1.0 + */ +@SuppressWarnings("serial") +public class DispatcherServlet extends FrameworkServlet { + + /** Well-known name for the MultipartResolver object in the bean factory for this namespace. */ + public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver"; + + /** Well-known name for the LocaleResolver object in the bean factory for this namespace. */ + public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver"; + + /** Well-known name for the ThemeResolver object in the bean factory for this namespace. */ + public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver"; + + /** + * Well-known name for the HandlerMapping object in the bean factory for this namespace. + * Only used when "detectAllHandlerMappings" is turned off. + * @see #setDetectAllHandlerMappings + */ + public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping"; + + /** + * Well-known name for the HandlerAdapter object in the bean factory for this namespace. + * Only used when "detectAllHandlerAdapters" is turned off. + * @see #setDetectAllHandlerAdapters + */ + public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter"; + + /** + * Well-known name for the HandlerExceptionResolver object in the bean factory for this namespace. + * Only used when "detectAllHandlerExceptionResolvers" is turned off. + * @see #setDetectAllHandlerExceptionResolvers + */ + public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver"; + + /** + * Well-known name for the RequestToViewNameTranslator object in the bean factory for this namespace. + */ + public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator"; + + /** + * Well-known name for the ViewResolver object in the bean factory for this namespace. + * Only used when "detectAllViewResolvers" is turned off. + * @see #setDetectAllViewResolvers + */ + public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver"; + + /** + * Well-known name for the FlashMapManager object in the bean factory for this namespace. + */ + public static final String FLASH_MAP_MANAGER_BEAN_NAME = "flashMapManager"; + + /** + * Request attribute to hold the current web application context. + * Otherwise only the global web app context is obtainable by tags etc. + * @see org.springframework.web.servlet.support.RequestContextUtils#findWebApplicationContext + */ + public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT"; + + /** + * Request attribute to hold the current LocaleResolver, retrievable by views. + * @see org.springframework.web.servlet.support.RequestContextUtils#getLocaleResolver + */ + public static final String LOCALE_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".LOCALE_RESOLVER"; + + /** + * Request attribute to hold the current ThemeResolver, retrievable by views. + * @see org.springframework.web.servlet.support.RequestContextUtils#getThemeResolver + */ + public static final String THEME_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_RESOLVER"; + + /** + * Request attribute to hold the current ThemeSource, retrievable by views. + * @see org.springframework.web.servlet.support.RequestContextUtils#getThemeSource + */ + public static final String THEME_SOURCE_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_SOURCE"; + + /** + * Name of request attribute that holds a read-only {@code Map} + * with "input" flash attributes saved by a previous request, if any. + * @see org.springframework.web.servlet.support.RequestContextUtils#getInputFlashMap(HttpServletRequest) + */ + public static final String INPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".INPUT_FLASH_MAP"; + + /** + * Name of request attribute that holds the "output" {@link FlashMap} with + * attributes to save for a subsequent request. + * @see org.springframework.web.servlet.support.RequestContextUtils#getOutputFlashMap(HttpServletRequest) + */ + public static final String OUTPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".OUTPUT_FLASH_MAP"; + + /** + * Name of request attribute that holds the {@link FlashMapManager}. + * @see org.springframework.web.servlet.support.RequestContextUtils#getFlashMapManager(HttpServletRequest) + */ + public static final String FLASH_MAP_MANAGER_ATTRIBUTE = DispatcherServlet.class.getName() + ".FLASH_MAP_MANAGER"; + + /** + * Name of request attribute that exposes an Exception resolved with a + * {@link HandlerExceptionResolver} but where no view was rendered + * (e.g. setting the status code). + */ + public static final String EXCEPTION_ATTRIBUTE = DispatcherServlet.class.getName() + ".EXCEPTION"; + + /** Log category to use when no mapped handler is found for a request. */ + public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; + + /** + * Name of the class path resource (relative to the DispatcherServlet class) + * that defines DispatcherServlet's default strategy names. + */ + private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties"; + + /** + * Common prefix that DispatcherServlet's default strategy attributes start with. + */ + private static final String DEFAULT_STRATEGIES_PREFIX = "org.springframework.web.servlet"; + + /** Additional logger to use when no mapped handler is found for a request. */ + protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); + + private static final Properties defaultStrategies; + + static { + // Load default strategy implementations from properties file. + // This is currently strictly internal and not meant to be customized + // by application developers. + try { + ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); + defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); + } + catch (IOException ex) { + throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage()); + } + } + + /** Detect all HandlerMappings or just expect "handlerMapping" bean?. */ + private boolean detectAllHandlerMappings = true; + + /** Detect all HandlerAdapters or just expect "handlerAdapter" bean?. */ + private boolean detectAllHandlerAdapters = true; + + /** Detect all HandlerExceptionResolvers or just expect "handlerExceptionResolver" bean?. */ + private boolean detectAllHandlerExceptionResolvers = true; + + /** Detect all ViewResolvers or just expect "viewResolver" bean?. */ + private boolean detectAllViewResolvers = true; + + /** Throw a NoHandlerFoundException if no Handler was found to process this request? *.*/ + private boolean throwExceptionIfNoHandlerFound = false; + + /** Perform cleanup of request attributes after include request?. */ + private boolean cleanupAfterInclude = true; + + /** MultipartResolver used by this servlet. */ + @Nullable + private MultipartResolver multipartResolver; + + /** LocaleResolver used by this servlet. */ + @Nullable + private LocaleResolver localeResolver; + + /** ThemeResolver used by this servlet. */ + @Nullable + private ThemeResolver themeResolver; + + /** List of HandlerMappings used by this servlet. */ + @Nullable + private List handlerMappings; + + /** List of HandlerAdapters used by this servlet. */ + @Nullable + private List handlerAdapters; + + /** List of HandlerExceptionResolvers used by this servlet. */ + @Nullable + private List handlerExceptionResolvers; + + /** RequestToViewNameTranslator used by this servlet. */ + @Nullable + private RequestToViewNameTranslator viewNameTranslator; + + /** FlashMapManager used by this servlet. */ + @Nullable + private FlashMapManager flashMapManager; + + /** List of ViewResolvers used by this servlet. */ + @Nullable + private List viewResolvers; + + // OSF CAS Customization: a new private field osfUrlProperties to store OSF URLs + @Setter + private OsfUrlProperties osfUrlProperties; + + /** + * Create a new {@code DispatcherServlet} that will create its own internal web + * application context based on defaults and values provided through servlet + * init-params. Typically used in Servlet 2.5 or earlier environments, where the only + * option for servlet registration is through {@code web.xml} which requires the use + * of a no-arg constructor. + *

Calling {@link #setContextConfigLocation} (init-param 'contextConfigLocation') + * will dictate which XML files will be loaded by the + * {@linkplain #DEFAULT_CONTEXT_CLASS default XmlWebApplicationContext} + *

Calling {@link #setContextClass} (init-param 'contextClass') overrides the + * default {@code XmlWebApplicationContext} and allows for specifying an alternative class, + * such as {@code AnnotationConfigWebApplicationContext}. + *

Calling {@link #setContextInitializerClasses} (init-param 'contextInitializerClasses') + * indicates which {@code ApplicationContextInitializer} classes should be used to + * further configure the internal application context prior to refresh(). + * @see #DispatcherServlet(WebApplicationContext) + */ + public DispatcherServlet() { + super(); + setDispatchOptionsRequest(true); + } + + /** + * Create a new {@code DispatcherServlet} with the given web application context. This + * constructor is useful in Servlet 3.0+ environments where instance-based registration + * of servlets is possible through the {@link ServletContext#addServlet} API. + *

Using this constructor indicates that the following properties / init-params + * will be ignored: + *

    + *
  • {@link #setContextClass(Class)} / 'contextClass'
  • + *
  • {@link #setContextConfigLocation(String)} / 'contextConfigLocation'
  • + *
  • {@link #setContextAttribute(String)} / 'contextAttribute'
  • + *
  • {@link #setNamespace(String)} / 'namespace'
  • + *
+ *

The given web application context may or may not yet be {@linkplain + * ConfigurableApplicationContext#refresh() refreshed}. If it has not + * already been refreshed (the recommended approach), then the following will occur: + *

    + *
  • If the given context does not already have a {@linkplain + * ConfigurableApplicationContext#setParent parent}, the root application context + * will be set as the parent.
  • + *
  • If the given context has not already been assigned an {@linkplain + * ConfigurableApplicationContext#setId id}, one will be assigned to it
  • + *
  • {@code ServletContext} and {@code ServletConfig} objects will be delegated to + * the application context
  • + *
  • {@link #postProcessWebApplicationContext} will be called
  • + *
  • Any {@code ApplicationContextInitializer}s specified through the + * "contextInitializerClasses" init-param or through the {@link + * #setContextInitializers} property will be applied.
  • + *
  • {@link ConfigurableApplicationContext#refresh refresh()} will be called if the + * context implements {@link ConfigurableApplicationContext}
  • + *
+ * If the context has already been refreshed, none of the above will occur, under the + * assumption that the user has performed these actions (or not) per their specific + * needs. + *

See {@link org.springframework.web.WebApplicationInitializer} for usage examples. + * @param webApplicationContext the context to use + * @see #initWebApplicationContext + * @see #configureAndRefreshWebApplicationContext + * @see org.springframework.web.WebApplicationInitializer + */ + public DispatcherServlet(WebApplicationContext webApplicationContext) { + super(webApplicationContext); + setDispatchOptionsRequest(true); + } + + /** + * Set whether to detect all HandlerMapping beans in this servlet's context. Otherwise, + * just a single bean with name "handlerMapping" will be expected. + *

Default is "true". Turn this off if you want this servlet to use a single + * HandlerMapping, despite multiple HandlerMapping beans being defined in the context. + */ + public void setDetectAllHandlerMappings(boolean detectAllHandlerMappings) { + this.detectAllHandlerMappings = detectAllHandlerMappings; + } + + /** + * Set whether to detect all HandlerAdapter beans in this servlet's context. Otherwise, + * just a single bean with name "handlerAdapter" will be expected. + *

Default is "true". Turn this off if you want this servlet to use a single + * HandlerAdapter, despite multiple HandlerAdapter beans being defined in the context. + */ + public void setDetectAllHandlerAdapters(boolean detectAllHandlerAdapters) { + this.detectAllHandlerAdapters = detectAllHandlerAdapters; + } + + /** + * Set whether to detect all HandlerExceptionResolver beans in this servlet's context. Otherwise, + * just a single bean with name "handlerExceptionResolver" will be expected. + *

Default is "true". Turn this off if you want this servlet to use a single + * HandlerExceptionResolver, despite multiple HandlerExceptionResolver beans being defined in the context. + */ + public void setDetectAllHandlerExceptionResolvers(boolean detectAllHandlerExceptionResolvers) { + this.detectAllHandlerExceptionResolvers = detectAllHandlerExceptionResolvers; + } + + /** + * Set whether to detect all ViewResolver beans in this servlet's context. Otherwise, + * just a single bean with name "viewResolver" will be expected. + *

Default is "true". Turn this off if you want this servlet to use a single + * ViewResolver, despite multiple ViewResolver beans being defined in the context. + */ + public void setDetectAllViewResolvers(boolean detectAllViewResolvers) { + this.detectAllViewResolvers = detectAllViewResolvers; + } + + /** + * Set whether to throw a NoHandlerFoundException when no Handler was found for this request. + * This exception can then be caught with a HandlerExceptionResolver or an + * {@code @ExceptionHandler} controller method. + *

Note that if {@link org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler} + * is used, then requests will always be forwarded to the default servlet and a + * NoHandlerFoundException would never be thrown in that case. + *

Default is "false", meaning the DispatcherServlet sends a NOT_FOUND error through the + * Servlet response. + * @since 4.0 + */ + public void setThrowExceptionIfNoHandlerFound(boolean throwExceptionIfNoHandlerFound) { + this.throwExceptionIfNoHandlerFound = throwExceptionIfNoHandlerFound; + } + + /** + * Set whether to perform cleanup of request attributes after an include request, that is, + * whether to reset the original state of all request attributes after the DispatcherServlet + * has processed within an include request. Otherwise, just the DispatcherServlet's own + * request attributes will be reset, but not model attributes for JSPs or special attributes + * set by views (for example, JSTL's). + *

Default is "true", which is strongly recommended. Views should not rely on request attributes + * having been set by (dynamic) includes. This allows JSP views rendered by an included controller + * to use any model attributes, even with the same names as in the main JSP, without causing side + * effects. Only turn this off for special needs, for example to deliberately allow main JSPs to + * access attributes from JSP views rendered by an included controller. + */ + public void setCleanupAfterInclude(boolean cleanupAfterInclude) { + this.cleanupAfterInclude = cleanupAfterInclude; + } + + + /** + * This implementation calls {@link #initStrategies}. + */ + @Override + protected void onRefresh(ApplicationContext context) { + initStrategies(context); + } + + /** + * Initialize the strategy objects that this servlet uses. + *

May be overridden in subclasses in order to initialize further strategy objects. + */ + protected void initStrategies(ApplicationContext context) { + initMultipartResolver(context); + initLocaleResolver(context); + initThemeResolver(context); + initHandlerMappings(context); + initHandlerAdapters(context); + initHandlerExceptionResolvers(context); + initRequestToViewNameTranslator(context); + initViewResolvers(context); + initFlashMapManager(context); + } + + /** + * Initialize the MultipartResolver used by this class. + *

If no bean is defined with the given name in the BeanFactory for this namespace, + * no multipart handling is provided. + */ + private void initMultipartResolver(ApplicationContext context) { + try { + this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); + if (logger.isTraceEnabled()) { + logger.trace("Detected " + this.multipartResolver); + } + else if (logger.isDebugEnabled()) { + logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName()); + } + } + catch (NoSuchBeanDefinitionException ex) { + // Default is no multipart resolver. + this.multipartResolver = null; + if (logger.isTraceEnabled()) { + logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared"); + } + } + } + + /** + * Initialize the LocaleResolver used by this class. + *

If no bean is defined with the given name in the BeanFactory for this namespace, + * we default to AcceptHeaderLocaleResolver. + */ + private void initLocaleResolver(ApplicationContext context) { + try { + this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class); + if (logger.isTraceEnabled()) { + logger.trace("Detected " + this.localeResolver); + } + else if (logger.isDebugEnabled()) { + logger.debug("Detected " + this.localeResolver.getClass().getSimpleName()); + } + } + catch (NoSuchBeanDefinitionException ex) { + // We need to use the default. + this.localeResolver = getDefaultStrategy(context, LocaleResolver.class); + if (logger.isTraceEnabled()) { + logger.trace("No LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME + + "': using default [" + this.localeResolver.getClass().getSimpleName() + "]"); + } + } + } + + /** + * Initialize the ThemeResolver used by this class. + *

If no bean is defined with the given name in the BeanFactory for this namespace, + * we default to a FixedThemeResolver. + */ + private void initThemeResolver(ApplicationContext context) { + try { + this.themeResolver = context.getBean(THEME_RESOLVER_BEAN_NAME, ThemeResolver.class); + if (logger.isTraceEnabled()) { + logger.trace("Detected " + this.themeResolver); + } + else if (logger.isDebugEnabled()) { + logger.debug("Detected " + this.themeResolver.getClass().getSimpleName()); + } + } + catch (NoSuchBeanDefinitionException ex) { + // We need to use the default. + this.themeResolver = getDefaultStrategy(context, ThemeResolver.class); + if (logger.isTraceEnabled()) { + logger.trace("No ThemeResolver '" + THEME_RESOLVER_BEAN_NAME + + "': using default [" + this.themeResolver.getClass().getSimpleName() + "]"); + } + } + } + + /** + * Initialize the HandlerMappings used by this class. + *

If no HandlerMapping beans are defined in the BeanFactory for this namespace, + * we default to BeanNameUrlHandlerMapping. + */ + private void initHandlerMappings(ApplicationContext context) { + this.handlerMappings = null; + + if (this.detectAllHandlerMappings) { + // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. + Map matchingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerMappings = new ArrayList<>(matchingBeans.values()); + // We keep HandlerMappings in sorted order. + AnnotationAwareOrderComparator.sort(this.handlerMappings); + } + } + else { + try { + HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); + this.handlerMappings = Collections.singletonList(hm); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default HandlerMapping later. + } + } + + // Ensure we have at least one HandlerMapping, by registering + // a default HandlerMapping if no other mappings are found. + if (this.handlerMappings == null) { + this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); + if (logger.isTraceEnabled()) { + logger.trace("No HandlerMappings declared for servlet '" + getServletName() + + "': using default strategies from DispatcherServlet.properties"); + } + } + } + + /** + * Initialize the HandlerAdapters used by this class. + *

If no HandlerAdapter beans are defined in the BeanFactory for this namespace, + * we default to SimpleControllerHandlerAdapter. + */ + private void initHandlerAdapters(ApplicationContext context) { + this.handlerAdapters = null; + + if (this.detectAllHandlerAdapters) { + // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts. + Map matchingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerAdapters = new ArrayList<>(matchingBeans.values()); + // We keep HandlerAdapters in sorted order. + AnnotationAwareOrderComparator.sort(this.handlerAdapters); + } + } + else { + try { + HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class); + this.handlerAdapters = Collections.singletonList(ha); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default HandlerAdapter later. + } + } + + // Ensure we have at least some HandlerAdapters, by registering + // default HandlerAdapters if no other adapters are found. + if (this.handlerAdapters == null) { + this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class); + if (logger.isTraceEnabled()) { + logger.trace("No HandlerAdapters declared for servlet '" + getServletName() + + "': using default strategies from DispatcherServlet.properties"); + } + } + } + + /** + * Initialize the HandlerExceptionResolver used by this class. + *

If no bean is defined with the given name in the BeanFactory for this namespace, + * we default to no exception resolver. + */ + private void initHandlerExceptionResolvers(ApplicationContext context) { + this.handlerExceptionResolvers = null; + + if (this.detectAllHandlerExceptionResolvers) { + // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts. + Map matchingBeans = BeanFactoryUtils + .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values()); + // We keep HandlerExceptionResolvers in sorted order. + AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers); + } + } + else { + try { + HandlerExceptionResolver her = + context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class); + this.handlerExceptionResolvers = Collections.singletonList(her); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, no HandlerExceptionResolver is fine too. + } + } + + // Ensure we have at least some HandlerExceptionResolvers, by registering + // default HandlerExceptionResolvers if no other resolvers are found. + if (this.handlerExceptionResolvers == null) { + this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class); + if (logger.isTraceEnabled()) { + logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() + + "': using default strategies from DispatcherServlet.properties"); + } + } + } + + /** + * Initialize the RequestToViewNameTranslator used by this servlet instance. + *

If no implementation is configured then we default to DefaultRequestToViewNameTranslator. + */ + private void initRequestToViewNameTranslator(ApplicationContext context) { + try { + this.viewNameTranslator = + context.getBean(REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, RequestToViewNameTranslator.class); + if (logger.isTraceEnabled()) { + logger.trace("Detected " + this.viewNameTranslator.getClass().getSimpleName()); + } + else if (logger.isDebugEnabled()) { + logger.debug("Detected " + this.viewNameTranslator); + } + } + catch (NoSuchBeanDefinitionException ex) { + // We need to use the default. + this.viewNameTranslator = getDefaultStrategy(context, RequestToViewNameTranslator.class); + if (logger.isTraceEnabled()) { + logger.trace("No RequestToViewNameTranslator '" + REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME + + "': using default [" + this.viewNameTranslator.getClass().getSimpleName() + "]"); + } + } + } + + /** + * Initialize the ViewResolvers used by this class. + *

If no ViewResolver beans are defined in the BeanFactory for this + * namespace, we default to InternalResourceViewResolver. + */ + private void initViewResolvers(ApplicationContext context) { + this.viewResolvers = null; + + if (this.detectAllViewResolvers) { + // Find all ViewResolvers in the ApplicationContext, including ancestor contexts. + Map matchingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false); + if (!matchingBeans.isEmpty()) { + this.viewResolvers = new ArrayList<>(matchingBeans.values()); + // We keep ViewResolvers in sorted order. + AnnotationAwareOrderComparator.sort(this.viewResolvers); + } + } + else { + try { + ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class); + this.viewResolvers = Collections.singletonList(vr); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default ViewResolver later. + } + } + + // Ensure we have at least one ViewResolver, by registering + // a default ViewResolver if no other resolvers are found. + if (this.viewResolvers == null) { + this.viewResolvers = getDefaultStrategies(context, ViewResolver.class); + if (logger.isTraceEnabled()) { + logger.trace("No ViewResolvers declared for servlet '" + getServletName() + + "': using default strategies from DispatcherServlet.properties"); + } + } + } + + /** + * Initialize the {@link FlashMapManager} used by this servlet instance. + *

If no implementation is configured then we default to + * {@code org.springframework.web.servlet.support.DefaultFlashMapManager}. + */ + private void initFlashMapManager(ApplicationContext context) { + try { + this.flashMapManager = context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class); + if (logger.isTraceEnabled()) { + logger.trace("Detected " + this.flashMapManager.getClass().getSimpleName()); + } + else if (logger.isDebugEnabled()) { + logger.debug("Detected " + this.flashMapManager); + } + } + catch (NoSuchBeanDefinitionException ex) { + // We need to use the default. + this.flashMapManager = getDefaultStrategy(context, FlashMapManager.class); + if (logger.isTraceEnabled()) { + logger.trace("No FlashMapManager '" + FLASH_MAP_MANAGER_BEAN_NAME + + "': using default [" + this.flashMapManager.getClass().getSimpleName() + "]"); + } + } + } + + /** + * Return this servlet's ThemeSource, if any; else return {@code null}. + *

Default is to return the WebApplicationContext as ThemeSource, + * provided that it implements the ThemeSource interface. + * @return the ThemeSource, if any + * @see #getWebApplicationContext() + */ + @Nullable + public final ThemeSource getThemeSource() { + return (getWebApplicationContext() instanceof ThemeSource ? (ThemeSource) getWebApplicationContext() : null); + } + + /** + * Obtain this servlet's MultipartResolver, if any. + * @return the MultipartResolver used by this servlet, or {@code null} if none + * (indicating that no multipart support is available) + */ + @Nullable + public final MultipartResolver getMultipartResolver() { + return this.multipartResolver; + } + + /** + * Return the configured {@link HandlerMapping} beans that were detected by + * type in the {@link WebApplicationContext} or initialized based on the + * default set of strategies from {@literal DispatcherServlet.properties}. + *

Note: This method may return {@code null} if invoked + * prior to {@link #onRefresh(ApplicationContext)}. + * @return an immutable list with the configured mappings, or {@code null} + * if not initialized yet + * @since 5.0 + */ + @Nullable + public final List getHandlerMappings() { + return (this.handlerMappings != null ? Collections.unmodifiableList(this.handlerMappings) : null); + } + + /** + * Return the default strategy object for the given strategy interface. + *

The default implementation delegates to {@link #getDefaultStrategies}, + * expecting a single object in the list. + * @param context the current WebApplicationContext + * @param strategyInterface the strategy interface + * @return the corresponding strategy object + * @see #getDefaultStrategies + */ + protected T getDefaultStrategy(ApplicationContext context, Class strategyInterface) { + List strategies = getDefaultStrategies(context, strategyInterface); + if (strategies.size() != 1) { + throw new BeanInitializationException( + "DispatcherServlet needs exactly 1 strategy for interface [" + strategyInterface.getName() + "]"); + } + return strategies.get(0); + } + + /** + * Create a List of default strategy objects for the given strategy interface. + *

The default implementation uses the "DispatcherServlet.properties" file (in the same + * package as the DispatcherServlet class) to determine the class names. It instantiates + * the strategy objects through the context's BeanFactory. + * @param context the current WebApplicationContext + * @param strategyInterface the strategy interface + * @return the List of corresponding strategy objects + */ + @SuppressWarnings("unchecked") + protected List getDefaultStrategies(ApplicationContext context, Class strategyInterface) { + String key = strategyInterface.getName(); + String value = defaultStrategies.getProperty(key); + if (value != null) { + String[] classNames = StringUtils.commaDelimitedListToStringArray(value); + List strategies = new ArrayList<>(classNames.length); + for (String className : classNames) { + try { + Class clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader()); + Object strategy = createDefaultStrategy(context, clazz); + strategies.add((T) strategy); + } + catch (ClassNotFoundException ex) { + throw new BeanInitializationException( + "Could not find DispatcherServlet's default strategy class [" + className + + "] for interface [" + key + "]", ex); + } + catch (LinkageError err) { + throw new BeanInitializationException( + "Unresolvable class definition for DispatcherServlet's default strategy class [" + + className + "] for interface [" + key + "]", err); + } + } + return strategies; + } + else { + return new LinkedList<>(); + } + } + + /** + * Create a default strategy. + *

The default implementation uses + * {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}. + * @param context the current WebApplicationContext + * @param clazz the strategy implementation class to instantiate + * @return the fully configured strategy instance + * @see org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory() + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean + */ + protected Object createDefaultStrategy(ApplicationContext context, Class clazz) { + return context.getAutowireCapableBeanFactory().createBean(clazz); + } + + + /** + * Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch} + * for the actual dispatching. + */ + @Override + protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { + logRequest(request); + + // Keep a snapshot of the request attributes in case of an include, + // to be able to restore the original attributes after the include. + Map attributesSnapshot = null; + if (WebUtils.isIncludeRequest(request)) { + attributesSnapshot = new HashMap<>(); + Enumeration attrNames = request.getAttributeNames(); + while (attrNames.hasMoreElements()) { + String attrName = (String) attrNames.nextElement(); + if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) { + attributesSnapshot.put(attrName, request.getAttribute(attrName)); + } + } + } + + // Make framework objects available to handlers and view objects. + request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); + request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); + request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); + request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource()); + + if (this.flashMapManager != null) { + FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); + if (inputFlashMap != null) { + request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); + } + request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); + request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); + } + + try { + doDispatch(request, response); + } + finally { + if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { + // Restore the original attribute snapshot, in case of an include. + if (attributesSnapshot != null) { + restoreAttributesAfterInclude(request, attributesSnapshot); + } + } + } + } + + private void logRequest(HttpServletRequest request) { + LogFormatUtils.traceDebug(logger, traceOn -> { + String params; + if (isEnableLoggingRequestDetails()) { + params = request.getParameterMap().entrySet().stream() + .map(entry -> entry.getKey() + ":" + Arrays.toString(entry.getValue())) + .collect(Collectors.joining(", ")); + } + else { + params = (request.getParameterMap().isEmpty() ? "" : "masked"); + } + + String queryString = request.getQueryString(); + String queryClause = (StringUtils.hasLength(queryString) ? "?" + queryString : ""); + String dispatchType = (!request.getDispatcherType().equals(DispatcherType.REQUEST) ? + "\"" + request.getDispatcherType().name() + "\" dispatch for " : ""); + String message = (dispatchType + request.getMethod() + " \"" + getRequestUri(request) + + queryClause + "\", parameters={" + params + "}"); + + if (traceOn) { + List values = Collections.list(request.getHeaderNames()); + String headers = values.size() > 0 ? "masked" : ""; + if (isEnableLoggingRequestDetails()) { + headers = values.stream().map(name -> name + ":" + Collections.list(request.getHeaders(name))) + .collect(Collectors.joining(", ")); + } + return message + ", headers={" + headers + "} in DispatcherServlet '" + getServletName() + "'"; + } + else { + return message; + } + }); + } + + /** + * Process the actual dispatching to the handler. + *

The handler will be obtained by applying the servlet's HandlerMappings in order. + * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters + * to find the first that supports the handler class. + *

All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers + * themselves to decide which methods are acceptable. + * @param request current HTTP request + * @param response current HTTP response + * @throws Exception in case of any kind of processing failure + */ + protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpServletRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; + boolean multipartRequestParsed = false; + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + + try { + ModelAndView mv = null; + Exception dispatchException = null; + + try { + processedRequest = checkMultipart(request); + multipartRequestParsed = (processedRequest != request); + + // Determine handler for the current request. + mappedHandler = getHandler(processedRequest); + if (mappedHandler == null) { + noHandlerFound(processedRequest, response); + return; + } + + // Determine handler adapter for the current request. + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + // Process last-modified header, if supported by the handler. + String method = request.getMethod(); + boolean isGet = "GET".equals(method); + if (isGet || "HEAD".equals(method)) { + long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); + if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { + return; + } + } + + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } + + // Actually invoke the handler. + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + if (asyncManager.isConcurrentHandlingStarted()) { + return; + } + + applyDefaultViewName(processedRequest, mv); + mappedHandler.applyPostHandle(processedRequest, response, mv); + } + catch (Exception ex) { + dispatchException = ex; + } + catch (Throwable err) { + // As of 4.3, we're processing Errors thrown from handler methods as well, + // making them available for @ExceptionHandler methods and other scenarios. + dispatchException = new NestedServletException("Handler dispatch failed", err); + } + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + } + catch (Exception ex) { + triggerAfterCompletion(processedRequest, response, mappedHandler, ex); + } + catch (Throwable err) { + triggerAfterCompletion(processedRequest, response, mappedHandler, + new NestedServletException("Handler processing failed", err)); + } + finally { + if (asyncManager.isConcurrentHandlingStarted()) { + // Instead of postHandle and afterCompletion + if (mappedHandler != null) { + mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); + } + } + else { + // Clean up any resources used by a multipart request. + if (multipartRequestParsed) { + cleanupMultipart(processedRequest); + } + } + } + } + + /** + * Do we need view name translation? + */ + private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception { + if (mv != null && !mv.hasView()) { + String defaultViewName = getDefaultViewName(request); + if (defaultViewName != null) { + mv.setViewName(defaultViewName); + } + } + } + + /** + * Handle the result of handler selection and handler invocation, which is + * either a ModelAndView or an Exception to be resolved to a ModelAndView. + */ + private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, + @Nullable Exception exception) throws Exception { + + boolean errorView = false; + + if (exception != null) { + if (exception instanceof ModelAndViewDefiningException) { + logger.debug("ModelAndViewDefiningException encountered", exception); + mv = ((ModelAndViewDefiningException) exception).getModelAndView(); + } + else { + Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); + mv = processHandlerException(request, response, handler, exception); + errorView = (mv != null); + } + } + + // Did the handler return a view to render? + if (mv != null && !mv.wasCleared()) { + render(mv, request, response); + if (errorView) { + WebUtils.clearErrorRequestAttributes(request); + } + } + else { + if (logger.isTraceEnabled()) { + logger.trace("No view rendering, null ModelAndView returned."); + } + } + + if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { + // Concurrent handling started during a forward + return; + } + + if (mappedHandler != null) { + // Exception (if any) is already handled.. + mappedHandler.triggerAfterCompletion(request, response, null); + } + } + + /** + * Build a LocaleContext for the given request, exposing the request's primary locale as current locale. + *

The default implementation uses the dispatcher's LocaleResolver to obtain the current locale, + * which might change during a request. + * @param request current HTTP request + * @return the corresponding LocaleContext + */ + @Override + protected LocaleContext buildLocaleContext(final HttpServletRequest request) { + LocaleResolver lr = this.localeResolver; + if (lr instanceof LocaleContextResolver) { + return ((LocaleContextResolver) lr).resolveLocaleContext(request); + } + else { + return () -> (lr != null ? lr.resolveLocale(request) : request.getLocale()); + } + } + + /** + * Convert the request into a multipart request, and make multipart resolver available. + *

If no multipart resolver is set, simply use the existing request. + * @param request current HTTP request + * @return the processed request (multipart wrapper if necessary) + * @see MultipartResolver#resolveMultipart + */ + protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { + if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { + if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) { + if (request.getDispatcherType().equals(DispatcherType.REQUEST)) { + logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter"); + } + } + else if (hasMultipartException(request)) { + logger.debug("Multipart resolution previously failed for current request - " + + "skipping re-resolution for undisturbed error rendering"); + } + else { + try { + return this.multipartResolver.resolveMultipart(request); + } + catch (MultipartException ex) { + if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) { + logger.debug("Multipart resolution failed for error dispatch", ex); + // Keep processing error dispatch with regular request handle below + } + else { + throw ex; + } + } + } + } + // If not returned before: return original request. + return request; + } + + /** + * Check "javax.servlet.error.exception" attribute for a multipart exception. + */ + private boolean hasMultipartException(HttpServletRequest request) { + Throwable error = (Throwable) request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE); + while (error != null) { + if (error instanceof MultipartException) { + return true; + } + error = error.getCause(); + } + return false; + } + + /** + * Clean up any resources used by the given multipart request (if any). + * @param request current HTTP request + * @see MultipartResolver#cleanupMultipart + */ + protected void cleanupMultipart(HttpServletRequest request) { + if (this.multipartResolver != null) { + MultipartHttpServletRequest multipartRequest = + WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); + if (multipartRequest != null) { + this.multipartResolver.cleanupMultipart(multipartRequest); + } + } + } + + /** + * Return the HandlerExecutionChain for this request. + *

Tries all handler mappings in order. + * @param request current HTTP request + * @return the HandlerExecutionChain, or {@code null} if no handler could be found + */ + @Nullable + protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + if (this.handlerMappings != null) { + for (HandlerMapping mapping : this.handlerMappings) { + HandlerExecutionChain handler = mapping.getHandler(request); + if (handler != null) { + return handler; + } + } + } + return null; + } + + /** + * No handler found -> set appropriate HTTP response status. + * @param request current HTTP request + * @param response current HTTP response + * @throws Exception if preparing the response failed + */ + protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (pageNotFoundLogger.isWarnEnabled()) { + pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request)); + } + if (this.throwExceptionIfNoHandlerFound) { + throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request), + new ServletServerHttpRequest(request).getHeaders()); + } + else { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + /** + * Return the HandlerAdapter for this handler object. + * @param handler the handler object to find an adapter for + * @throws ServletException if no HandlerAdapter can be found for the handler. This is a fatal error. + */ + protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { + if (this.handlerAdapters != null) { + for (HandlerAdapter adapter : this.handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + } + throw new ServletException("No adapter for handler [" + handler + + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); + } + + /** + * Determine an error ModelAndView via the registered HandlerExceptionResolvers. + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or {@code null} if none chosen at the time of the exception + * (for example, if multipart resolution failed) + * @param ex the exception that got thrown during handler execution + * @return a corresponding ModelAndView to forward to + * @throws Exception if no error ModelAndView found + */ + @Nullable + protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, + @Nullable Object handler, Exception ex) throws Exception { + + // Success and error responses may use different content types + request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + + // Check registered HandlerExceptionResolvers... + ModelAndView exMv = null; + if (this.handlerExceptionResolvers != null) { + for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { + exMv = resolver.resolveException(request, response, handler, ex); + if (exMv != null) { + break; + } + } + } + if (exMv != null) { + if (exMv.isEmpty()) { + request.setAttribute(EXCEPTION_ATTRIBUTE, ex); + return null; + } + // We might still need view name translation for a plain error model... + if (!exMv.hasView()) { + String defaultViewName = getDefaultViewName(request); + if (defaultViewName != null) { + exMv.setViewName(defaultViewName); + } + } + if (logger.isTraceEnabled()) { + logger.trace("Using resolved error view: " + exMv, ex); + } + else if (logger.isDebugEnabled()) { + logger.debug("Using resolved error view: " + exMv); + } + WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); + return exMv; + } + + throw ex; + } + + /** + * Render the given ModelAndView. + *

This is the last stage in handling a request. It may involve resolving the view by name. + * @param mv the ModelAndView to render + * @param request current HTTP servlet request + * @param response current HTTP servlet response + * @throws ServletException if view is missing or cannot be resolved + * @throws Exception if there's a problem rendering the view + */ + protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { + // Determine locale for request and apply it to the response. + Locale locale = + (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale()); + response.setLocale(locale); + + View view; + String viewName = mv.getViewName(); + if (viewName != null) { + // We need to resolve the view name. + view = resolveViewName(viewName, mv.getModelInternal(), locale, request); + if (view == null) { + throw new ServletException("Could not resolve view with name '" + mv.getViewName() + + "' in servlet with name '" + getServletName() + "'"); + } + } + else { + // No need to lookup: the ModelAndView object contains the actual View object. + view = mv.getView(); + if (view == null) { + throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + + "View object in servlet with name '" + getServletName() + "'"); + } + } + + // Delegate to the View object for rendering. + if (logger.isTraceEnabled()) { + logger.trace("Rendering view [" + view + "] "); + } + try { + if (mv.getStatus() != null) { + response.setStatus(mv.getStatus().value()); + } + view.render(mv.getModelInternal(), request, response); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Error rendering view [" + view + "]", ex); + } + throw ex; + } + } + + /** + * Translate the supplied request into a default view name. + * @param request current HTTP servlet request + * @return the view name (or {@code null} if no default found) + * @throws Exception if view name translation failed + */ + @Nullable + protected String getDefaultViewName(HttpServletRequest request) throws Exception { + return (this.viewNameTranslator != null ? this.viewNameTranslator.getViewName(request) : null); + } + + /** + * Resolve the given view name into a View object (to be rendered). + *

The default implementations asks all ViewResolvers of this dispatcher. + * Can be overridden for custom resolution strategies, potentially based on + * specific model attributes or request parameters. + * @param viewName the name of the view to resolve + * @param model the model to be passed to the view + * @param locale the current locale + * @param request current HTTP servlet request + * @return the View object, or {@code null} if none found + * @throws Exception if the view cannot be resolved + * (typically in case of problems creating an actual View object) + * @see ViewResolver#resolveViewName + */ + @Nullable + protected View resolveViewName(String viewName, @Nullable Map model, + Locale locale, HttpServletRequest request) throws Exception { + + if (this.viewResolvers != null) { + for (ViewResolver viewResolver : this.viewResolvers) { + View view = viewResolver.resolveViewName(viewName, locale); + if (view != null) { + // OSF CAS Customization: add osfUrlProperties to the model map so the view has access to OSF URLs + if (this.osfUrlProperties != null ) { + if (model == null) { + model = new HashMap<>(); + } + model.put("dispatcherOsfUrl", this.osfUrlProperties); + } + return view; + } + } + } + return null; + } + + private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception { + + if (mappedHandler != null) { + mappedHandler.triggerAfterCompletion(request, response, ex); + } + throw ex; + } + + /** + * Restore the request attributes after an include. + * @param request current HTTP request + * @param attributesSnapshot the snapshot of the request attributes before the include + */ + @SuppressWarnings("unchecked") + private void restoreAttributesAfterInclude(HttpServletRequest request, Map attributesSnapshot) { + // Need to copy into separate Collection here, to avoid side effects + // on the Enumeration when removing attributes. + Set attrsToCheck = new HashSet<>(); + Enumeration attrNames = request.getAttributeNames(); + while (attrNames.hasMoreElements()) { + String attrName = (String) attrNames.nextElement(); + if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) { + attrsToCheck.add(attrName); + } + } + + // Add attributes that may have been removed + attrsToCheck.addAll((Set) attributesSnapshot.keySet()); + + // Iterate over the attributes to check, restoring the original value + // or removing the attribute, respectively, if appropriate. + for (String attrName : attrsToCheck) { + Object attrValue = attributesSnapshot.get(attrName); + if (attrValue == null) { + request.removeAttribute(attrName); + } + else if (attrValue != request.getAttribute(attrName)) { + request.setAttribute(attrName, attrValue); + } + } + } + + private static String getRequestUri(HttpServletRequest request) { + String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE); + if (uri == null) { + uri = request.getRequestURI(); + } + return uri; + } + +} diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index b051bb89..25aa8bb5 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -168,7 +168,7 @@ screen.error.page.title.pagenotfound=Error - Page Not Found screen.error.page.title.requestunsupported=Error - Unsupported Request screen.error.page.accessdenied=Access Denied screen.error.page.permissiondenied=You do not have permission to view this page. -screen.error.page.requestunsupported=The request type or syntax is not supported. +screen.error.page.requestunsupported=Request Not Supported # screen.error.page.loginagain=Login Again screen.error.page.notfound=Page Not Found screen.error.page.doesnotexist=The page you are attempting to access does not exist at the moment. @@ -434,10 +434,10 @@ cas.mfa.providerselection.mfa-simple.notes=Allow CAS to act as a multifactor aut issuing tokens and sending them to end-users via pre-defined communication channels such as email or text messages. cas.mfa.providerselection.mfa-yubikey=YubiKey Multifactor Authentication cas.mfa.providerselection.mfa-yubikey.notes=Yubico is a cloud-based service that enables strong, easy-to-use \ - and affordable two-factor authentication with one-time passwords through their flagship product, YubiKey. + and affordable two-factor authentication with one-time passwords through their flagship product, YubiKey. cas.mfa.providerselection.mfa-u2f=YubiKey Multifactor Authentication cas.mfa.providerselection.mfa-u2f.notes=U2F is an open authentication standard that enables internet users \ - to securely access any number of online services, with one single device, instantly and with no drivers, or client software needed. + to securely access any number of online services, with one single device, instantly and with no drivers, or client software needed. cas.mfa.duologin.pagetitle=Duo Security Login @@ -496,6 +496,8 @@ noop.message=This's a noop message. # # Header # +cas.header.togglenav=Toggle Navigation + # App bar (top) # # App drawer (left) @@ -516,11 +518,35 @@ cas.login.resources.osf.donate=Donate # # Footer # -copyright.cos=Copyright © 2011 – {0} \ - Center for Open Science | \ - Terms of Use | \ - Privacy Policy | \ - Status +footer.line1=\ + \ + Center for Open Science\ + \ + \ + \ + GitHub\ +  \ + \ + LinkedIn\ +  \ + \ + Bluesky\ +  \ + \ + Mastodon\ +  \ + + +footer.line2=\ + \ + Terms of Use | \ + Privacy Policy | \ + Status | \ + API | \ + TOP Guidelines\ + \ + Copyright © 2011 – {0} + # # IdP SSO # @@ -541,6 +567,7 @@ screen.welcome.label.email.accesskey=e screen.welcome.label.password=Password screen.welcome.label.password.accesskey=p screen.welcome.button.login=Sign in +screen.welcome.button.signup=Sign up screen.welcome.checkbox.rememberme=Stay signed in screen.welcome.link.resetpassword=Reset password screen.welcome.button.orcidlogin=Sign in with ORCiD @@ -581,12 +608,12 @@ screen.institutionlogin.title=Institution SSO screen.institutionlogin.heading=Sign in through your institution screen.institutionlogin.message.select=If your institution has partnered with OSF, please select its name below and sign in with your institutional credentials. If you do not currently have an OSF account, this will create one for you. screen.institutionlogin.message.auto=Your institution has partnered with OSF. Please continue to sign in with your institutional credentials. If you do not currently have an OSF account, this will create one for you. -screen.institutionlogin.heading.select=Select your institution +screen.institutionlogin.heading.select=Your institution screen.institutionlogin.heading.auto=Your institution screen.institutionlogin.link.select=Not your institution? screen.institutionlogin.link.unsupported=I can't find my institution screen.institutionlogin.button.submit=Sign in -screen.institutionlogin.osf=Sign in with your OSF account +screen.institutionlogin.osf=Sign in with email # # Unsupported Institution # @@ -733,14 +760,15 @@ screen.institutionssomultipleemailsnotsupported.message=\ screen.oauth.confirm.title=Approve Access screen.oauth.confirm.header=Approve or deny authorization screen.oauth.confirm.message=Do you want to grant access to the following service with listed access scopes? -screen.oauth.confirm.service=Name: {0} -screen.oauth.confirm.description=Description: {0} -screen.oauth.confirm.scopes=Scope(s) requested: {0} +screen.oauth.confirm.service.name=Service Name +screen.oauth.confirm.service.description=Service Description +screen.oauth.confirm.service.scopes=Scope(s) Requested screen.oauth.confirm.allow=Allow screen.oauth.confirm.deny=Deny screen.oauth.confirm.backtoosf=Exit and go back to OSF screen.oauth.error.title=OAuth Error screen.oauth.error.heading=Authorization failed +screen.oauth.error.detail=Error Detail screen.oauth.error.exit=Exit # # Pac4j Authentication Delegation Error Views diff --git a/src/main/resources/static/css/cas.css b/src/main/resources/static/css/cas.css index a9280e01..aa25dfe1 100644 --- a/src/main/resources/static/css/cas.css +++ b/src/main/resources/static/css/cas.css @@ -32,8 +32,11 @@ body { height: 100vh; margin: 0; padding: 0; + /* flex column for mobile, flex row for desktop */ flex-direction: column; - justify-content: space-between; + @media (min-width: 1200px) { + flex-direction: row; + } font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; background: #EFEFEF; } @@ -99,16 +102,33 @@ aside, section, main { top: 56px; } -.mdc-drawer-app-content { - flex: auto; - overflow: auto; - position: relative; +.flex-column { + flex-direction: column; +} + +.full-width { + width: 100%; +} + +.center-align-text { + display: inline-block; + text-align: center; } .main-content { display: flex; } +.main-background-gradient { + background: linear-gradient(90deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.80)), url(/images/dark-blue-gradient.png) center center no-repeat; + background-size: cover; + border-radius: 0.75rem 0 0 0.75rem; +} + +.flex-grow-1 { + flex-grow: 1; +} + .mdc-top-app-bar { z-index: 7; } @@ -149,6 +169,7 @@ header>nav .cas-brand .cas-logo { border-right: var(--cas-theme-border-light, 1px solid rgba(0, 0, 0, .2)); padding: 2rem 2.5rem; flex: 1; + background: #fff; } .login-section:last-child { @@ -432,8 +453,9 @@ button.close { color: var(--cas-theme-primary, #153e50); } -.mdc-button--raised:not(:disabled) { - background-color: var(--mdc-theme-primary, #153E50); +.mdc-button--raised { + background-color: transparent; + font-weight: bold; } .mdc-button--raised.mdc-button-twitter:not(:disabled) { @@ -647,13 +669,17 @@ button.close { ******************************************/ :root { - --cas-theme-osf-navbar: #263947; + --cas-theme-osf-navbar: #24384a; + --cas-theme-navbar-hover: #384b5c; --cas-theme-osf-surface: #f7f7f7; --cas-theme-osf-footer: #efefef; --cas-theme-osf-grey: #eeeeee; --cas-theme-osf-green: #357935; - --cas-theme-osf-blue: #1b6d85; + --cas-theme-osf-blue: #337ab7; + --cas-theme-osf-blue-hover: #0089ff; + --cas-theme-grey-hover: #f5f8fb; --cas-theme-osf-red: #b52b27; + --cas-theme-osf-red-hover: #E40303; --cas-theme-osf-disabled: #eeeeee; --cas-theme-osf-disabled-dark: #cccccc; --cas-theme-primary: var(--cas-theme-osf-navbar, #263947); @@ -663,9 +689,8 @@ button.close { } body { - background: - linear-gradient(to right, #009574d8, #003f7cd8), - url(/images/page-background.png) center center no-repeat; + background: var(--cas-theme-osf-navbar); + background-size: cover; background-blend-mode: normal; backdrop-filter: brightness(0.8); @@ -732,8 +757,20 @@ body { margin: 0.25rem 0; } +.form-button-column { + display: flex; + flex-direction: column; + margin: 0.25rem 0; + + .mdc-button { + margin: 0.25rem 0; + padding: 0 8px; + } +} + .form-button .mdc-button, -.form-button-inline .mdc-button { +.form-button-inline .mdc-button, +.form-button-column .mdc-button { width: 100%; min-width: fit-content; text-transform: none; @@ -741,6 +778,14 @@ body { height: 56px; } +.form-button .sign-up-button { + width: auto; + float: right; + font-size: 1rem; + height: 40px; + margin-right: 1rem; +} + .form-button-inline .mdc-button { flex-basis: 48%; margin: 0.25rem 0; @@ -754,8 +799,13 @@ body { .form-button .button-osf-grey, .form-button-inline .button-osf-grey { - background-color: var(--cas-theme-osf-grey, #eeeeee); - box-shadow: 0px 5px 4px 0px rgba(0, 0, 0, 0.2), 0px 4px 4px 0px rgba(0, 0, 0, 0.14), 0px 3px 8px 0px rgba(0, 0, 0, 0.12); + background-color: transparent; + box-shadow: 0 0 4px 0 #00000029; + display: inline-flex; + + &:hover { + background: var(--cas-theme-grey-hover, #f5f8fb); + } } .form-button .button-osf-green, @@ -765,12 +815,26 @@ body { .form-button .button-osf-blue, .form-button-inline .button-osf-blue { - background-color: var(--cas-theme-osf-blue, #1b6d85); + background-color: var(--cas-theme-osf-blue, #337ab7); + + &:hover { + background-color: var(--cas-theme-osf-blue-hover, #0089ff); + } + + &:disabled { + opacity: 0.6; + background-color: var(--cas-theme-osf-blue, #337ab7) !important; /* override mdc styles */ + color: white; + } } .form-button .button-osf-red, .form-button-inline .button-osf-red { background-color: var(--cas-theme-osf-red, #b52b27); + + &:hover { + background-color: var(--cas-theme-osf-red-hover, #E40303); + } } .form-button .button-osf-disabled, @@ -811,8 +875,7 @@ body { color: #F7F7F7; } -.mdc-top-app-bar__row .hidden-narrow, -.service-ui .osf-shield-with-name .hidden-narrow { +.mdc-top-app-bar__row .hidden-narrow { font-weight: normal; } @@ -827,47 +890,9 @@ body { flex: 1; } -#serviceui { - background-color: transparent; -} - -.service-ui { - margin-top: 1rem!important; - margin-bottom: 1rem!important; -} - -.service-ui .service-ui-logo { - max-height: 56px; - max-width: 360px; -} - -.service-ui .service-ui-logo-branded { - max-height: 48px; - max-width: 360px; -} - -.service-ui .osf-shield-with-name { - margin: 0 auto; - padding-bottom: 1rem; -} - -.osf-shield-with-name .service-ui-logo { - padding-right: 0.5rem; -} - -.osf-shield-with-name .service-ui-logo-branded { - padding-right: 1rem; -} - -.osf-shield-with-name .service-ui-name { - font-size: 2.25rem; - font-weight: bold; -} - -.osf-shield-with-name .service-ui-name-branded { - font-size: 2rem; - font-weight: normal; - overflow-wrap: anywhere; +.login-section .card-message h1, +.login-error-card .card-message h1 { + text-align: center; } .text-with-mdi, @@ -912,25 +937,20 @@ body { } .form-button-inline .delegation-button-logo { - position: absolute; - left: 4px; - top: 12px; padding: 6px; height: 36px; } .form-button-inline .delegation-button-label { font-size: 0.875rem; - color: black; + font-weight: 700; + color: var(--cas-theme-osf-blue, #337ab7); white-space: nowrap; padding-left: 28px; letter-spacing: normal; } .form-button .delegation-button-logo { - position: absolute; - left: 20px; - top: 11px; padding: 2px; height: 36px; } @@ -953,7 +973,8 @@ body { font-size: 1rem; } -.login-error-card .pre-formatted-small pre { +.login-error-card .pre-formatted-small pre, +.login-section .card-message .pre-formatted-small pre { font-size: 0.75rem; white-space: pre-wrap; } @@ -963,7 +984,9 @@ body { } .login-section .reveal-password { - background-color: var(--cas-theme-osf-blue, #1b6d85); + color: #94a3b8; + inset-inline-end: 0rem; + position: absolute; } .login-section .login-error-list { @@ -977,11 +1000,15 @@ body { .login-instn-card .card-message, .login-error-card .card-message { padding: 0; + margin-top: 0.5rem } .login-error-card #errorInfo, -.login-error-card #authnAttr { +.login-error-card #authnAttr, +.login-error-card .text-with-border, +.login-section .text-with-border { margin-top: 1rem; + margin-bottom: 1rem; padding: 0 1rem; border: solid #e7e7e7; } @@ -1033,7 +1060,25 @@ body { .cas-footer-osf { text-align: center; - background: var(--cas-theme-osf-footer, #efefef); + flex-direction: column; + border-radius: 0 0 0 0.75rem; + + > *:first-child { + background: var(--cas-theme-osf-footer, #efefef); + } +} + +.footer-section { + display: flex; + justify-content: space-between; + flex-wrap: wrap-reverse; + width: 100%; + padding: 1rem; + + img { + height: 36px; + width: 36px; + } } @media all and (min-height: 1199.99px) { @@ -1058,11 +1103,6 @@ body { .mdc-top-app-bar__row .mdc-button { } - .service-ui { - margin-top: 1rem!important; - margin-bottom: 1rem!important; - } - .cas-footer-osf { font-size: 1.125rem!important; padding-bottom: 1.125rem!important; @@ -1089,11 +1129,6 @@ body { padding-top: 48px; } - .service-ui { - margin-top: 0.875rem!important; - margin-bottom: 0.875rem!important; - } - .cas-footer-osf { font-size: 1rem!important; padding-bottom: 1rem!important; @@ -1120,11 +1155,6 @@ body { padding-top: 36px; } - .service-ui { - margin-top: 0.75rem!important; - margin-bottom: 0.75rem!important; - } - .cas-footer-osf { font-size: 0.875rem!important; padding-bottom: 0.875rem!important; @@ -1151,11 +1181,6 @@ body { padding-top: 24px; } - .service-ui { - margin-top: 0.5rem!important; - margin-bottom: 0.5rem!important; - } - .cas-footer-osf { font-size: 0.625rem!important; padding-bottom: 0.625rem!important; @@ -1171,16 +1196,11 @@ body { .w-card-wide { width: 75%; } - - .service-ui .service-ui-logo { - max-width: 320px; - } } @media all and (max-width: 699.99px) { - .mdc-top-app-bar__row .hidden-narrow, - .service-ui-name .osf-shield-with-name .hidden-narrow { + .mdc-top-app-bar__row .hidden-narrow { display: None; } @@ -1190,22 +1210,6 @@ body { .w-card-wide { width: 100%; } - - .service-ui .service-ui-logo { - max-width: 280px; - } - - .osf-shield-with-name .service-ui-logo { - max-height: 48px; - } - - .osf-shield-with-name .service-ui-name { - font-size: 2rem; - } - - .osf-shield-with-name .service-ui-name-branded { - font-size: 1.75rem; - } } @media all and (max-width: 511.99px) { @@ -1238,31 +1242,128 @@ body { @media all and (max-width: 399.99px) { - .service-ui .service-ui-logo { - max-width: 240px; + .login-section .mdi-before-text, + .login-error-card .mdi-before-text { + display: none; } - .osf-shield-with-name .service-ui-logo { - max-height: 36px; + .cas-footer-osf { + font-size: 0.625rem!important; + padding-bottom: 0.625rem!important; + padding-top: 0.625rem!important; } +} +/* Left navigation pane */ +.toggle-nav-input { + opacity: 0; +} - .osf-shield-with-name .service-ui-name { - font-size: 1.75rem; +.toggle-nav-label { + cursor: pointer; + padding: 0.5rem; +} + +.slideout { + position: fixed; + top: 0; + left: 0; + height: 100%; + background: var(--cas-theme-osf-navbar); + color: var(--cas-theme-osf-surface, #f7f7f7); + transition: transform 0.3s ease; + margin-left: 0 !important; +} + +#slideout-nav { + margin-left: 0 !important; + margin-top: 37px; +} + +.left-pane { + padding: 8px 12px; + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; + background: var(--cas-theme-osf-navbar); + color: var(--cas-theme-osf-surface, #f7f7f7); + + .cas-brand-name { + font-size: 2rem; } +} - .osf-shield-with-name .service-ui-name-branded { - font-size: 1.5rem; +@media all and (max-width: 1199.99px) { + .slideout { + transform: translateX(-100%); } - .login-section .mdi-before-text, - .login-error-card .mdi-before-text { + input#toggle-leftnav:checked~.left-pane { + transform: translateX(0); + } + + .desktop-only { display: none; } - .cas-footer-osf { - font-size: 0.625rem!important; - padding-bottom: 0.625rem!important; - padding-top: 0.625rem!important; + .small-screen-only { + display: block; + } +} + +@media all and (min-width: 1200px) { + .slideout { + transform: translateX(0); + } + + .desktop-only { + display: block; + } + + .small-screen-only { + display: none; + } +} + +.left-pane-logo-wrapper { + margin-left: 8px; + + .cas-logo { + height: 36px; + } +} + +.left-pane-nav { + width: 250px; + max-width: 250px; +} + +.left-pane-nav-list { + list-style: none; + padding: 0; + margin: 0; + margin-top: 2rem; + margin-bottom: 4rem; + + li { + margin-bottom: 2rem; + } + + a.navbar-link { + display: flex; + padding: 0.75rem; + gap: 0.5rem; + width: 100%; + font-weight: 400; + } + + a.navbar-link:hover { + background: var(--cas-theme-navbar-hover); + border-radius: 0.375rem; + } + + .arrow-icon { + margin-left: auto; } } diff --git a/src/main/resources/static/images/arrow.svg b/src/main/resources/static/images/arrow.svg new file mode 100644 index 00000000..879866a3 --- /dev/null +++ b/src/main/resources/static/images/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/static/images/bluesky.svg b/src/main/resources/static/images/bluesky.svg new file mode 100644 index 00000000..3907119c --- /dev/null +++ b/src/main/resources/static/images/bluesky.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/images/dark-blue-gradient.png b/src/main/resources/static/images/dark-blue-gradient.png new file mode 100644 index 00000000..105d00bd Binary files /dev/null and b/src/main/resources/static/images/dark-blue-gradient.png differ diff --git a/src/main/resources/static/images/donate.svg b/src/main/resources/static/images/donate.svg new file mode 100644 index 00000000..0dc08577 --- /dev/null +++ b/src/main/resources/static/images/donate.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/static/images/email.svg b/src/main/resources/static/images/email.svg new file mode 100644 index 00000000..367c0cf5 --- /dev/null +++ b/src/main/resources/static/images/email.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/static/images/github.svg b/src/main/resources/static/images/github.svg new file mode 100644 index 00000000..a1d1c554 --- /dev/null +++ b/src/main/resources/static/images/github.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/static/images/home.svg b/src/main/resources/static/images/home.svg new file mode 100644 index 00000000..a8ebe9e0 --- /dev/null +++ b/src/main/resources/static/images/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/static/images/institutions.svg b/src/main/resources/static/images/institutions.svg new file mode 100644 index 00000000..a8f8cd53 --- /dev/null +++ b/src/main/resources/static/images/institutions.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/static/images/linkedin.svg b/src/main/resources/static/images/linkedin.svg new file mode 100644 index 00000000..cb47bd4c --- /dev/null +++ b/src/main/resources/static/images/linkedin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/static/images/mastodon.svg b/src/main/resources/static/images/mastodon.svg new file mode 100644 index 00000000..ac9f5aaf --- /dev/null +++ b/src/main/resources/static/images/mastodon.svg @@ -0,0 +1,8 @@ + + + Graphic + + + + + \ No newline at end of file diff --git a/src/main/resources/static/images/meetings.svg b/src/main/resources/static/images/meetings.svg new file mode 100644 index 00000000..0dc4b6f1 --- /dev/null +++ b/src/main/resources/static/images/meetings.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/static/images/menu.svg b/src/main/resources/static/images/menu.svg new file mode 100644 index 00000000..3b498997 --- /dev/null +++ b/src/main/resources/static/images/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/static/images/osf-logo-white.svg b/src/main/resources/static/images/osf-logo-white.svg new file mode 100644 index 00000000..97f04ab2 --- /dev/null +++ b/src/main/resources/static/images/osf-logo-white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/static/images/preprints.svg b/src/main/resources/static/images/preprints.svg new file mode 100644 index 00000000..7573b028 --- /dev/null +++ b/src/main/resources/static/images/preprints.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/resources/static/images/registries.svg b/src/main/resources/static/images/registries.svg new file mode 100644 index 00000000..02bf3043 --- /dev/null +++ b/src/main/resources/static/images/registries.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/static/images/search.svg b/src/main/resources/static/images/search.svg new file mode 100644 index 00000000..79c8a389 --- /dev/null +++ b/src/main/resources/static/images/search.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/static/images/support.svg b/src/main/resources/static/images/support.svg new file mode 100644 index 00000000..21c0b13e --- /dev/null +++ b/src/main/resources/static/images/support.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/resources/templates/casAccountDisabledView.html b/src/main/resources/templates/casAccountDisabledView.html index 56d19952..29b1277b 100644 --- a/src/main/resources/templates/casAccountDisabledView.html +++ b/src/main/resources/templates/casAccountDisabledView.html @@ -14,15 +14,6 @@