Remove Spring MVC path extension content negotiation
See gh-34036
This commit is contained in:
@@ -188,11 +188,6 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
|
||||
parseResourceChain(resourceHandlerDef, context, resourceChainElement, source);
|
||||
}
|
||||
|
||||
Object manager = MvcNamespaceUtils.getContentNegotiationManager(context);
|
||||
if (manager != null) {
|
||||
values.add("contentNegotiationManager", manager);
|
||||
}
|
||||
|
||||
String beanName = context.getReaderContext().generateBeanName(resourceHandlerDef);
|
||||
context.getRegistry().registerBeanDefinition(beanName, resourceHandlerDef);
|
||||
context.registerComponent(new BeanComponentDefinition(resourceHandlerDef, beanName));
|
||||
|
||||
@@ -55,13 +55,6 @@ import org.springframework.web.accept.ParameterContentNegotiationStrategy;
|
||||
* <td>Off</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link #favorPathExtension}</td>
|
||||
* <td>false (as of 5.3)</td>
|
||||
* <td>{@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy
|
||||
* PathExtensionContentNegotiationStrategy}</td>
|
||||
* <td>Off</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link #ignoreAcceptHeader}</td>
|
||||
* <td>false</td>
|
||||
* <td>{@link HeaderContentNegotiationStrategy}</td>
|
||||
@@ -84,13 +77,6 @@ import org.springframework.web.accept.ParameterContentNegotiationStrategy;
|
||||
* <p>As of 5.0 you can set the exact strategies to use via
|
||||
* {@link #strategies(List)}.
|
||||
*
|
||||
* <p><strong>Note:</strong> if you must use URL-based content type resolution,
|
||||
* the use of a query parameter is simpler and preferable to the use of a path
|
||||
* extension since the latter can cause issues with URI variables, path
|
||||
* parameters, and URI decoding. Consider setting {@link #favorPathExtension}
|
||||
* to {@literal false} or otherwise set the strategies to use explicitly via
|
||||
* {@link #strategies(List)}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.2
|
||||
*/
|
||||
@@ -101,16 +87,6 @@ public class ContentNegotiationConfigurer {
|
||||
private final Map<String, MediaType> mediaTypes = new HashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* Class constructor with {@link jakarta.servlet.ServletContext}.
|
||||
*/
|
||||
public ContentNegotiationConfigurer(@Nullable ServletContext servletContext) {
|
||||
if (servletContext != null) {
|
||||
this.factory.setServletContext(servletContext);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the exact list of strategies to use.
|
||||
* <p><strong>Note:</strong> use of this method is mutually exclusive with
|
||||
@@ -144,20 +120,6 @@ public class ContentNegotiationConfigurer {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the path extension in the URL path should be used to determine
|
||||
* the requested media type.
|
||||
* <p>By default this is set to {@code false} in which case path extensions
|
||||
* have no impact on content negotiation.
|
||||
* @deprecated as of 5.2.4. See deprecation note on
|
||||
* {@link ContentNegotiationManagerFactoryBean#setFavorPathExtension(boolean)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public ContentNegotiationConfigurer favorPathExtension(boolean favorPathExtension) {
|
||||
this.factory.setFavorPathExtension(favorPathExtension);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mapping from a key, extracted from a path extension or a query
|
||||
* parameter, to a MediaType. This is required in order for the parameter
|
||||
@@ -202,37 +164,11 @@ public class ContentNegotiationConfigurer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to ignore requests with path extension that cannot be resolved
|
||||
* to any media type. Setting this to {@code false} will result in an
|
||||
* {@code HttpMediaTypeNotAcceptableException} if there is no match.
|
||||
* <p>By default this is set to {@code true}.
|
||||
* @deprecated as of 5.2.4. See deprecation note on
|
||||
* {@link ContentNegotiationManagerFactoryBean#setIgnoreUnknownPathExtensions(boolean)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public ContentNegotiationConfigurer ignoreUnknownPathExtensions(boolean ignore) {
|
||||
this.factory.setIgnoreUnknownPathExtensions(ignore);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@link #favorPathExtension} is set, this property determines whether
|
||||
* to allow use of JAF (Java Activation Framework) to resolve a path
|
||||
* extension to a specific MediaType.
|
||||
* @deprecated as of 5.0, in favor of {@link #useRegisteredExtensionsOnly(boolean)}
|
||||
* which has reverse behavior
|
||||
*/
|
||||
@Deprecated
|
||||
public ContentNegotiationConfigurer useJaf(boolean useJaf) {
|
||||
return this.useRegisteredExtensionsOnly(!useJaf);
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@link #favorPathExtension favorPathExtension} is set, this
|
||||
* When {@link #favorParameter(boolean)} is set, this
|
||||
* property determines whether to use only registered {@code MediaType} mappings
|
||||
* to resolve a path extension to a specific MediaType.
|
||||
* <p>By default this is not set in which case
|
||||
* {@code PathExtensionContentNegotiationStrategy} will use defaults if available.
|
||||
* <p>By default, this is not set in which case
|
||||
* {@code ParameterContentNegotiationStrategy} will use defaults if available.
|
||||
*/
|
||||
public ContentNegotiationConfigurer useRegisteredExtensionsOnly(boolean useRegisteredExtensionsOnly) {
|
||||
this.factory.setUseRegisteredExtensionsOnly(useRegisteredExtensionsOnly);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -66,9 +66,6 @@ public class ResourceHandlerRegistry {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
@Nullable
|
||||
private final ContentNegotiationManager contentNegotiationManager;
|
||||
|
||||
@Nullable
|
||||
private final UrlPathHelper pathHelper;
|
||||
|
||||
@@ -111,7 +108,6 @@ public class ResourceHandlerRegistry {
|
||||
Assert.notNull(applicationContext, "ApplicationContext is required");
|
||||
this.applicationContext = applicationContext;
|
||||
this.servletContext = servletContext;
|
||||
this.contentNegotiationManager = contentNegotiationManager;
|
||||
this.pathHelper = pathHelper;
|
||||
}
|
||||
|
||||
@@ -173,15 +169,11 @@ public class ResourceHandlerRegistry {
|
||||
return new SimpleUrlHandlerMapping(urlMap, this.order);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private ResourceHttpRequestHandler getRequestHandler(ResourceHandlerRegistration registration) {
|
||||
ResourceHttpRequestHandler handler = registration.getRequestHandler();
|
||||
if (this.pathHelper != null) {
|
||||
handler.setUrlPathHelper(this.pathHelper);
|
||||
}
|
||||
if (this.contentNegotiationManager != null) {
|
||||
handler.setContentNegotiationManager(this.contentNegotiationManager);
|
||||
}
|
||||
handler.setServletContext(this.servletContext);
|
||||
handler.setApplicationContext(this.applicationContext);
|
||||
try {
|
||||
|
||||
@@ -430,7 +430,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
|
||||
@Bean
|
||||
public ContentNegotiationManager mvcContentNegotiationManager() {
|
||||
if (this.contentNegotiationManager == null) {
|
||||
ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext);
|
||||
ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer();
|
||||
configurer.mediaTypes(getDefaultMediaTypes());
|
||||
configureContentNegotiation(configurer);
|
||||
this.contentNegotiationManager = configurer.buildContentNegotiationManager();
|
||||
|
||||
@@ -280,31 +280,6 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
return this.resourceRegionHttpMessageConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@code ContentNegotiationManager} to help determine the
|
||||
* media types for resources being served. If the manager contains a path
|
||||
* extension strategy it will be checked for registered file extension.
|
||||
* @since 4.3
|
||||
* @deprecated as of 5.2.4 in favor of using {@link #setMediaTypes(Map)}
|
||||
* with mappings possibly obtained from
|
||||
* {@link ContentNegotiationManager#getMediaTypeMappings()}.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setContentNegotiationManager(@Nullable ContentNegotiationManager contentNegotiationManager) {
|
||||
this.contentNegotiationManager = contentNegotiationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured content negotiation manager.
|
||||
* @since 4.3
|
||||
* @deprecated as of 5.2.4
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated
|
||||
public ContentNegotiationManager getContentNegotiationManager() {
|
||||
return this.contentNegotiationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add mappings between file extensions, extracted from the filename of a
|
||||
* static {@link Resource}, and corresponding media type to set on the
|
||||
@@ -459,18 +434,6 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
if (this.resourceRegionHttpMessageConverter == null) {
|
||||
this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter();
|
||||
}
|
||||
|
||||
ContentNegotiationManager manager = getContentNegotiationManager();
|
||||
if (manager != null) {
|
||||
setMediaTypes(manager.getMediaTypeMappings());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
org.springframework.web.accept.PathExtensionContentNegotiationStrategy strategy =
|
||||
initContentNegotiationStrategy();
|
||||
if (strategy != null) {
|
||||
setMediaTypes(strategy.getMediaTypes());
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveResourceLocations() {
|
||||
@@ -546,21 +509,6 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the strategy to use to determine the media type for a resource.
|
||||
* @deprecated as of 5.2.4 this method returns {@code null}, and if a
|
||||
* subclass returns an actual instance, the instance is used only as a
|
||||
* source of media type mappings, if it contains any. Please, use
|
||||
* {@link #setMediaTypes(Map)} instead, or if you need to change behavior,
|
||||
* you can override {@link #getMediaType(HttpServletRequest, Resource)}.
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
protected org.springframework.web.accept.PathExtensionContentNegotiationStrategy initContentNegotiationStrategy() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes a resource request.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -205,7 +205,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
|
||||
|
||||
}
|
||||
AnnotationAwareOrderComparator.sort(this.viewResolvers);
|
||||
this.cnmFactoryBean.setServletContext(servletContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -380,7 +380,6 @@ public class MvcNamespaceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void testResources() throws Exception {
|
||||
loadBeanDefinitions("mvc-config-resources.xml");
|
||||
|
||||
@@ -390,10 +389,6 @@ public class MvcNamespaceTests {
|
||||
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
|
||||
ContentNegotiationManager manager = mapping.getContentNegotiationManager();
|
||||
|
||||
ResourceHttpRequestHandler handler = appContext.getBean(ResourceHttpRequestHandler.class);
|
||||
assertThat(handler).isNotNull();
|
||||
assertThat(handler.getContentNegotiationManager()).isSameAs(manager);
|
||||
|
||||
SimpleUrlHandlerMapping resourceMapping = appContext.getBean(SimpleUrlHandlerMapping.class);
|
||||
assertThat(resourceMapping).isNotNull();
|
||||
assertThat(resourceMapping.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE - 1);
|
||||
|
||||
@@ -49,7 +49,7 @@ class ContentNegotiationConfigurerTests {
|
||||
void setup() {
|
||||
this.servletRequest = new MockHttpServletRequest();
|
||||
this.webRequest = new ServletWebRequest(this.servletRequest);
|
||||
this.configurer = new ContentNegotiationConfigurer(this.servletRequest.getServletContext());
|
||||
this.configurer = new ContentNegotiationConfigurer();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionRes
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
|
||||
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
|
||||
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
|
||||
import org.springframework.web.servlet.view.InternalResourceViewResolver;
|
||||
@@ -264,7 +263,6 @@ class WebMvcConfigurationSupportExtensionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void contentNegotiation() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
||||
NativeWebRequest webRequest = new ServletWebRequest(request);
|
||||
@@ -287,11 +285,7 @@ class WebMvcConfigurationSupportExtensionTests {
|
||||
|
||||
request = new MockHttpServletRequest("GET", "/resources/foo.gif");
|
||||
HandlerExecutionChain chain = handlerMapping.getHandler(request);
|
||||
|
||||
assertThat(chain).isNotNull();
|
||||
ResourceHttpRequestHandler handler = (ResourceHttpRequestHandler) chain.getHandler();
|
||||
assertThat(handler).isNotNull();
|
||||
assertThat(handler.getContentNegotiationManager()).isSameAs(manager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.web.servlet.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -33,8 +34,6 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
@@ -127,18 +126,12 @@ class ResourceHttpRequestHandlerTests {
|
||||
}
|
||||
|
||||
@Test // SPR-13658
|
||||
@SuppressWarnings("deprecation")
|
||||
void getResourceWithRegisteredMediaType() throws Exception {
|
||||
ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
|
||||
factory.addMediaType("bar", new MediaType("foo", "bar"));
|
||||
factory.afterPropertiesSet();
|
||||
ContentNegotiationManager manager = factory.getObject();
|
||||
|
||||
List<Resource> paths = List.of(new ClassPathResource("test/", getClass()));
|
||||
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
|
||||
handler.setServletContext(new MockServletContext());
|
||||
handler.setMediaTypes(Map.of("bar", new MediaType("foo", "bar")));
|
||||
handler.setLocations(paths);
|
||||
handler.setContentNegotiationManager(manager);
|
||||
handler.afterPropertiesSet();
|
||||
|
||||
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.bar");
|
||||
@@ -148,17 +141,12 @@ class ResourceHttpRequestHandlerTests {
|
||||
assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }");
|
||||
}
|
||||
|
||||
@Test // SPR-14577
|
||||
@Test // SPR-14577
|
||||
void getMediaTypeWithFavorPathExtensionOff() throws Exception {
|
||||
ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
|
||||
factory.afterPropertiesSet();
|
||||
ContentNegotiationManager manager = factory.getObject();
|
||||
|
||||
List<Resource> paths = List.of(new ClassPathResource("test/", getClass()));
|
||||
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
|
||||
handler.setServletContext(new MockServletContext());
|
||||
handler.setLocations(paths);
|
||||
handler.setContentNegotiationManager(manager);
|
||||
handler.afterPropertiesSet();
|
||||
|
||||
this.request.addHeader("Accept", "application/json,text/plain,*/*");
|
||||
|
||||
@@ -84,7 +84,7 @@ class ContentNegotiatingViewResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveViewNameWithPathExtension() throws Exception {
|
||||
void resolveViewNameWithExtensionByParameter() throws Exception {
|
||||
request.setRequestURI("/test");
|
||||
request.setParameter("format", "xls");
|
||||
|
||||
@@ -310,77 +310,6 @@ class ContentNegotiatingViewResolverTests {
|
||||
assertThat(result).as("Invalid view").isSameAs(viewMock3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void resolveViewNameFilename() throws Exception {
|
||||
request.setRequestURI("/test.html");
|
||||
|
||||
ContentNegotiationManager manager =
|
||||
new ContentNegotiationManager(new org.springframework.web.accept.PathExtensionContentNegotiationStrategy());
|
||||
|
||||
ViewResolver viewResolverMock1 = mock(ViewResolver.class, "viewResolver1");
|
||||
ViewResolver viewResolverMock2 = mock(ViewResolver.class, "viewResolver2");
|
||||
viewResolver.setContentNegotiationManager(manager);
|
||||
viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2));
|
||||
|
||||
viewResolver.afterPropertiesSet();
|
||||
|
||||
View viewMock1 = mock(View.class, "application_xml");
|
||||
View viewMock2 = mock(View.class, "text_html");
|
||||
|
||||
String viewName = "view";
|
||||
Locale locale = Locale.ENGLISH;
|
||||
|
||||
given(viewResolverMock1.resolveViewName(viewName, locale)).willReturn(viewMock1);
|
||||
given(viewResolverMock1.resolveViewName(viewName + ".html", locale)).willReturn(null);
|
||||
given(viewResolverMock2.resolveViewName(viewName, locale)).willReturn(null);
|
||||
given(viewResolverMock2.resolveViewName(viewName + ".html", locale)).willReturn(viewMock2);
|
||||
given(viewMock1.getContentType()).willReturn("application/xml");
|
||||
given(viewMock2.getContentType()).willReturn("text/html;charset=ISO-8859-1");
|
||||
|
||||
View result = viewResolver.resolveViewName(viewName, locale);
|
||||
assertThat(result).as("Invalid view").isSameAs(viewMock2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void resolveViewNameFilenameDefaultView() throws Exception {
|
||||
request.setRequestURI("/test.json");
|
||||
|
||||
Map<String, MediaType> mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON);
|
||||
org.springframework.web.accept.PathExtensionContentNegotiationStrategy pathStrategy =
|
||||
new org.springframework.web.accept.PathExtensionContentNegotiationStrategy(mapping);
|
||||
viewResolver.setContentNegotiationManager(new ContentNegotiationManager(pathStrategy));
|
||||
|
||||
ViewResolver viewResolverMock1 = mock();
|
||||
ViewResolver viewResolverMock2 = mock();
|
||||
viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2));
|
||||
|
||||
View viewMock1 = mock(View.class, "application_xml");
|
||||
View viewMock2 = mock(View.class, "text_html");
|
||||
View viewMock3 = mock(View.class, "application_json");
|
||||
|
||||
List<View> defaultViews = new ArrayList<>();
|
||||
defaultViews.add(viewMock3);
|
||||
viewResolver.setDefaultViews(defaultViews);
|
||||
|
||||
viewResolver.afterPropertiesSet();
|
||||
|
||||
String viewName = "view";
|
||||
Locale locale = Locale.ENGLISH;
|
||||
|
||||
given(viewResolverMock1.resolveViewName(viewName, locale)).willReturn(viewMock1);
|
||||
given(viewResolverMock1.resolveViewName(viewName + ".json", locale)).willReturn(null);
|
||||
given(viewResolverMock2.resolveViewName(viewName, locale)).willReturn(viewMock2);
|
||||
given(viewResolverMock2.resolveViewName(viewName + ".json", locale)).willReturn(null);
|
||||
given(viewMock1.getContentType()).willReturn("application/xml");
|
||||
given(viewMock2.getContentType()).willReturn("text/html;charset=ISO-8859-1");
|
||||
given(viewMock3.getContentType()).willReturn("application/json");
|
||||
|
||||
View result = viewResolver.resolveViewName(viewName, locale);
|
||||
assertThat(result).as("Invalid view").isSameAs(viewMock3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveViewContentTypeNull() throws Exception {
|
||||
request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
|
||||
|
||||
Reference in New Issue
Block a user