mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Replace HandlerMappingIntrospector with DefaultPreFlightRequestHandler
See gh-36481
This commit is contained in:
+9
-10
@@ -35,7 +35,7 @@ import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
import org.springframework.web.servlet.handler.DefaultPreFlightRequestHandler;
|
||||
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
|
||||
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
|
||||
import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter;
|
||||
@@ -72,14 +72,14 @@ public abstract class MvcNamespaceUtils {
|
||||
|
||||
private static final String CORS_CONFIGURATION_BEAN_NAME = "mvcCorsConfigurations";
|
||||
|
||||
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
|
||||
private static final String PRE_FLIGHT_REQUEST_HANDLER = "mvcPreFlightRequestHandler";
|
||||
|
||||
|
||||
public static void registerDefaultComponents(ParserContext context, @Nullable Object source) {
|
||||
registerBeanNameUrlHandlerMapping(context, source);
|
||||
registerHttpRequestHandlerAdapter(context, source);
|
||||
registerSimpleControllerHandlerAdapter(context, source);
|
||||
registerHandlerMappingIntrospector(context, source);
|
||||
registerPreFlightRequestHandler(context, source);
|
||||
registerLocaleResolver(context, source);
|
||||
registerViewNameTranslator(context, source);
|
||||
registerFlashMapManager(context, source);
|
||||
@@ -276,18 +276,17 @@ public abstract class MvcNamespaceUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an {@link HandlerMappingIntrospector} under a well-known name
|
||||
* Registers an {@link DefaultPreFlightRequestHandler} under a well-known name
|
||||
* unless already registered.
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
private static void registerHandlerMappingIntrospector(ParserContext context, @Nullable Object source) {
|
||||
if (!context.getRegistry().containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(HandlerMappingIntrospector.class);
|
||||
private static void registerPreFlightRequestHandler(ParserContext context, @Nullable Object source) {
|
||||
if (!context.getRegistry().containsBeanDefinition(PRE_FLIGHT_REQUEST_HANDLER)) {
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition(DefaultPreFlightRequestHandler.class);
|
||||
beanDef.setSource(source);
|
||||
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
beanDef.setLazyInit(true);
|
||||
context.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, beanDef);
|
||||
context.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME));
|
||||
context.getRegistry().registerBeanDefinition(PRE_FLIGHT_REQUEST_HANDLER, beanDef);
|
||||
context.registerComponent(new BeanComponentDefinition(beanDef, PRE_FLIGHT_REQUEST_HANDLER));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -61,6 +61,7 @@ import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.context.ServletContextAware;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.PreFlightRequestHandler;
|
||||
import org.springframework.web.method.support.CompositeUriComponentsContributor;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
@@ -76,8 +77,8 @@ import org.springframework.web.servlet.function.support.RouterFunctionMapping;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor;
|
||||
import org.springframework.web.servlet.handler.DefaultPreFlightRequestHandler;
|
||||
import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite;
|
||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
|
||||
import org.springframework.web.servlet.mvc.Controller;
|
||||
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
|
||||
@@ -1161,11 +1162,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
|
||||
protected void addCorsMappings(CorsRegistry registry) {
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Bean
|
||||
@Lazy
|
||||
public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
|
||||
return new HandlerMappingIntrospector();
|
||||
public PreFlightRequestHandler mvcPreFlightRequestHandler() {
|
||||
return new DefaultPreFlightRequestHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PropertiesLoaderUtils;
|
||||
import org.springframework.http.server.RequestPath;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.cors.CorsUtils;
|
||||
import org.springframework.web.cors.PreFlightRequestHandler;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.HandlerExecutionChain;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link PreFlightRequestHandler} that emulates the
|
||||
* DispatcherServlet's algorithm for finding a matching {@link HandlerMapping}
|
||||
* for a pre-flight request, and then invokes the matched handler as a
|
||||
* {@link PreFlightRequestHandler}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.1
|
||||
*/
|
||||
public class DefaultPreFlightRequestHandler
|
||||
implements PreFlightRequestHandler, ApplicationContextAware, InitializingBean {
|
||||
|
||||
private @Nullable ApplicationContext applicationContext;
|
||||
|
||||
private @Nullable List<HandlerMapping> handlerMappings;
|
||||
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured or detected {@code HandlerMapping}s.
|
||||
*/
|
||||
public List<HandlerMapping> getHandlerMappings() {
|
||||
return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
if (this.handlerMappings == null) {
|
||||
Assert.notNull(this.applicationContext, "No ApplicationContext");
|
||||
this.handlerMappings = initHandlerMappings(this.applicationContext);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<HandlerMapping> initHandlerMappings(ApplicationContext context) {
|
||||
|
||||
Map<String, HandlerMapping> beans =
|
||||
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
|
||||
|
||||
if (!beans.isEmpty()) {
|
||||
List<HandlerMapping> mappings = new ArrayList<>(beans.values());
|
||||
AnnotationAwareOrderComparator.sort(mappings);
|
||||
return Collections.unmodifiableList(mappings);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(initFallback(context));
|
||||
}
|
||||
|
||||
private static List<HandlerMapping> initFallback(ApplicationContext applicationContext) {
|
||||
Properties properties;
|
||||
try {
|
||||
Resource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class);
|
||||
properties = PropertiesLoaderUtils.loadProperties(resource);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Could not load DispatcherServlet.properties: " + ex.getMessage());
|
||||
}
|
||||
|
||||
String value = properties.getProperty(HandlerMapping.class.getName());
|
||||
String[] names = StringUtils.commaDelimitedListToStringArray(value);
|
||||
List<HandlerMapping> result = new ArrayList<>(names.length);
|
||||
for (String name : names) {
|
||||
try {
|
||||
Class<?> clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader());
|
||||
Object mapping = applicationContext.getAutowireCapableBeanFactory().createBean(clazz);
|
||||
result.add((HandlerMapping) mapping);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the matching {@link HandlerMapping} for the request, and invoke the
|
||||
* handler it returns as a {@link PreFlightRequestHandler}.
|
||||
* @throws NoHandlerFoundException if no handler matches the request
|
||||
*/
|
||||
@Override
|
||||
public void handlePreFlight(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
Assert.state(this.handlerMappings != null, "Not yet initialized via afterPropertiesSet.");
|
||||
Assert.state(CorsUtils.isPreFlightRequest(request), "Not a pre-flight request.");
|
||||
RequestPath previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
|
||||
try {
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
for (HandlerMapping mapping : this.handlerMappings) {
|
||||
HandlerExecutionChain chain = mapping.getHandler(request);
|
||||
if (chain != null) {
|
||||
Object handler = chain.getHandler();
|
||||
if (handler instanceof PreFlightRequestHandler preFlightHandler) {
|
||||
preFlightHandler.handlePreFlight(request, response);
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Expected PreFlightRequestHandler: " + handler.getClass());
|
||||
}
|
||||
}
|
||||
throw new NoHandlerFoundException(
|
||||
request.getMethod(), request.getRequestURI(), new ServletServerHttpRequest(request).getHeaders());
|
||||
}
|
||||
finally {
|
||||
ServletRequestPathUtils.setParsedRequestPath(previousPath, request);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+7
-7
@@ -95,7 +95,7 @@ import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor;
|
||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
import org.springframework.web.servlet.handler.DefaultPreFlightRequestHandler;
|
||||
import org.springframework.web.servlet.handler.MappedInterceptor;
|
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor;
|
||||
@@ -261,12 +261,12 @@ class MvcNamespaceTests {
|
||||
|
||||
assertThat(uriComponentsContributor).isNotNull();
|
||||
|
||||
String name = "mvcHandlerMappingIntrospector";
|
||||
HandlerMappingIntrospector introspector = this.appContext.getBean(name, HandlerMappingIntrospector.class);
|
||||
assertThat(introspector).isNotNull();
|
||||
assertThat(introspector.getHandlerMappings()).hasSize(2);
|
||||
assertThat(introspector.getHandlerMappings()).element(0).isSameAs(mapping);
|
||||
assertThat(introspector.getHandlerMappings().get(1).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class);
|
||||
String name = "mvcPreFlightRequestHandler";
|
||||
DefaultPreFlightRequestHandler requestHandler = this.appContext.getBean(name, DefaultPreFlightRequestHandler.class);
|
||||
assertThat(requestHandler).isNotNull();
|
||||
assertThat(requestHandler.getHandlerMappings()).hasSize(2);
|
||||
assertThat(requestHandler.getHandlerMappings()).element(0).isSameAs(mapping);
|
||||
assertThat(requestHandler.getHandlerMappings().get(1).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class);
|
||||
}
|
||||
|
||||
@Test // gh-25290
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.handler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.context.support.GenericWebApplicationContext;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunctions;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
import org.springframework.web.servlet.function.support.RouterFunctionMapping;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultPreFlightRequestHandler}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.1
|
||||
*/
|
||||
class DefaultPreFlightRequestHandlerTests {
|
||||
|
||||
@Test
|
||||
void detectHandlerMappings() {
|
||||
StaticWebApplicationContext context = new StaticWebApplicationContext();
|
||||
context.registerSingleton("A", SimpleUrlHandlerMapping.class);
|
||||
context.registerSingleton("B", SimpleUrlHandlerMapping.class);
|
||||
context.registerSingleton("C", SimpleUrlHandlerMapping.class);
|
||||
context.refresh();
|
||||
|
||||
List<?> expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C"));
|
||||
List<HandlerMapping> actual = initHandler(context).getHandlerMappings();
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectHandlerMappingsOrdered() {
|
||||
GenericWebApplicationContext context = new GenericWebApplicationContext();
|
||||
context.registerBean("B", SimpleUrlHandlerMapping.class, () -> {
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setOrder(2);
|
||||
return mapping;
|
||||
});
|
||||
context.registerBean("C", SimpleUrlHandlerMapping.class, () -> {
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setOrder(3);
|
||||
return mapping;
|
||||
});
|
||||
context.registerBean("A", SimpleUrlHandlerMapping.class, () -> {
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setOrder(1);
|
||||
return mapping;
|
||||
});
|
||||
context.refresh();
|
||||
|
||||
List<?> expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C"));
|
||||
List<HandlerMapping> actual = initHandler(context).getHandlerMappings();
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlePreFlight() throws Exception {
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.register(TestConfig.class);
|
||||
context.refresh();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path");
|
||||
request.addHeader("Origin", "http://localhost:9000");
|
||||
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
initHandler(context).handlePreFlight(request, response);
|
||||
|
||||
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("http://localhost:9000");
|
||||
assertThat(response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).containsExactly("POST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlePreFlightWithNoHandlerFoundException() {
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.register(TestConfig.class);
|
||||
context.refresh();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/unknownPath");
|
||||
request.addHeader("Origin", "http://localhost:9000");
|
||||
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
assertThatThrownBy(() -> initHandler(context).handlePreFlight(request, response))
|
||||
.isInstanceOf(NoHandlerFoundException.class);
|
||||
|
||||
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isNull();
|
||||
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isNull();
|
||||
}
|
||||
|
||||
private static DefaultPreFlightRequestHandler initHandler(WebApplicationContext context) {
|
||||
DefaultPreFlightRequestHandler handler = new DefaultPreFlightRequestHandler();
|
||||
handler.setApplicationContext(context);
|
||||
handler.afterPropertiesSet();
|
||||
return handler;
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
public RouterFunctionMapping routerFunctionMapping() {
|
||||
RouterFunctionMapping mapping = new RouterFunctionMapping();
|
||||
mapping.setOrder(1);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RequestMappingHandlerMapping handlerMapping() {
|
||||
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
|
||||
mapping.setOrder(2);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestController testController() {
|
||||
return new TestController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<?> routerFunction() {
|
||||
return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@CrossOrigin("http://localhost:9000")
|
||||
@Controller
|
||||
private static class TestController {
|
||||
|
||||
@PostMapping("/path")
|
||||
void handle() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user