mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 10:04:21 +00:00
Merge branch 'main' into DS-2058-7x
This commit is contained in:
@@ -60,7 +60,9 @@ public interface GroupService extends DSpaceObjectService<Group>, DSpaceObjectLe
|
||||
public void addMember(Context context, Group group, EPerson e);
|
||||
|
||||
/**
|
||||
* add group to this group
|
||||
* add group to this group. Be sure to call the {@link #update(Context, Group)}
|
||||
* method once that all the membership are set to trigger the rebuild of the
|
||||
* group2group cache table
|
||||
*
|
||||
* @param context DSpace context object
|
||||
* @param groupParent parent group
|
||||
@@ -80,7 +82,9 @@ public interface GroupService extends DSpaceObjectService<Group>, DSpaceObjectLe
|
||||
|
||||
|
||||
/**
|
||||
* remove group from this group
|
||||
* remove group from this group. Be sure to call the {@link #update(Context, Group)}
|
||||
* method once that all the membership are set to trigger the rebuild of the
|
||||
* group2group cache table
|
||||
*
|
||||
* @param context DSpace context object
|
||||
* @param groupParent parent group
|
||||
|
@@ -95,7 +95,8 @@ public class GroupRestController {
|
||||
for (Group childGroup : childGroups) {
|
||||
groupService.addMember(context, parentGroup, childGroup);
|
||||
}
|
||||
|
||||
// this is required to trigger the rebuild of the group2group cache
|
||||
groupService.update(context, parentGroup);
|
||||
context.complete();
|
||||
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
@@ -203,7 +204,8 @@ public class GroupRestController {
|
||||
}
|
||||
|
||||
groupService.removeMember(context, parentGroup, childGroup);
|
||||
|
||||
// this is required to trigger the rebuild of the group2group cache
|
||||
groupService.update(context, parentGroup);
|
||||
context.complete();
|
||||
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
|
@@ -1021,16 +1021,19 @@ public class RestResourceController implements InitializingBean {
|
||||
|
||||
/**
|
||||
* Internal method to convert the parameters provided as a MultivalueMap as a string to use in the self-link.
|
||||
* This function will exclude all "embed" parameters and parameters starting with "embed."
|
||||
* @param parameters
|
||||
* @return encoded uriString containing request parameters
|
||||
* @return encoded uriString containing request parameters without embed parameter
|
||||
*/
|
||||
private String getEncodedParameterStringFromRequestParams(
|
||||
@RequestParam MultiValueMap<String, Object> parameters) {
|
||||
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance();
|
||||
|
||||
for (String key : parameters.keySet()) {
|
||||
if (!StringUtils.equals(key, "embed") && !StringUtils.startsWith(key, "embed.")) {
|
||||
uriComponentsBuilder.queryParam(key, parameters.get(key));
|
||||
}
|
||||
}
|
||||
return uriComponentsBuilder.encode().build().toString();
|
||||
}
|
||||
|
||||
|
@@ -92,7 +92,7 @@ function downloadFile(url) {
|
||||
HAL.Http.Client.prototype.get = function(url) {
|
||||
var self = this;
|
||||
this.vent.trigger('location-change', { url: url });
|
||||
var jqxhr = $.ajax({
|
||||
$.ajax({
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
xhrFields: {
|
||||
@@ -109,15 +109,18 @@ HAL.Http.Client.prototype.get = function(url) {
|
||||
headers: jqXHR.getAllResponseHeaders()
|
||||
});
|
||||
},
|
||||
error: function() {
|
||||
self.vent.trigger('fail-response', { jqxhr: jqxhr });
|
||||
var contentTypeResponseHeader = jqxhr.getResponseHeader("content-type");
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
// Also check for updated token during errors. E.g. when a login failure occurs, token may be changed.
|
||||
checkForUpdatedCSRFTokenInResponse(jqXHR);
|
||||
self.vent.trigger('fail-response', { jqxhr: jqXHR });
|
||||
var contentTypeResponseHeader = jqXHR.getResponseHeader("content-type");
|
||||
if (contentTypeResponseHeader != undefined
|
||||
&& !contentTypeResponseHeader.startsWith("application/hal")
|
||||
&& !contentTypeResponseHeader.startsWith("application/json")) {
|
||||
downloadFile(url);
|
||||
}
|
||||
}});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
HAL.Http.Client.prototype.request = function(opts) {
|
||||
@@ -137,6 +140,11 @@ HAL.Http.Client.prototype.request = function(opts) {
|
||||
checkForUpdatedCSRFTokenInResponse(jqXHR);
|
||||
};
|
||||
|
||||
// Also check error responses to see if CSRF Token has been updated
|
||||
opts.error = function(jqXHR, textStatus, errorThrown) {
|
||||
checkForUpdatedCSRFTokenInResponse(jqXHR);
|
||||
};
|
||||
|
||||
self.vent.trigger('location-change', { url: opts.url });
|
||||
return jqxhr = $.ajax(opts);
|
||||
};
|
||||
|
@@ -73,11 +73,8 @@
|
||||
var successHandler = function(result, status, xhr) {
|
||||
// look for Authorization header & save to a MyHalBrowserToken cookie
|
||||
document.cookie = "MyHalBrowserToken=" + xhr.getResponseHeader('Authorization').split(" ")[1];
|
||||
// look for DSpace-XSRF-TOKEN header & save to a MyHalBrowserCsrfToken cookie (if found)
|
||||
var csrfToken = xhr.getResponseHeader('DSPACE-XSRF-TOKEN');
|
||||
if (csrfToken!=null) {
|
||||
document.cookie = "MyHalBrowserCsrfToken=" + csrfToken;
|
||||
}
|
||||
// Check for an update to the CSRF Token & save to a MyHalBrowserCsrfToken cookie (if found)
|
||||
checkForUpdatedCSRFTokenInResponse(xhr);
|
||||
toastr.success('You are now logged in. Please wait while we redirect you...', 'Login Successful');
|
||||
setTimeout(function() {
|
||||
window.location.href = window.location.pathname.replace("login.html", "");
|
||||
@@ -112,9 +109,13 @@
|
||||
}
|
||||
},
|
||||
success : successHandler,
|
||||
error : function(result, status, xhr) {
|
||||
if (result.status === 401) {
|
||||
var authenticate = result.getResponseHeader("WWW-Authenticate");
|
||||
error : function(xhr, textStatus, errorThrown) {
|
||||
// Check for an update to the CSRF Token & save to a MyHalBrowserCsrfToken cookie (if found)
|
||||
checkForUpdatedCSRFTokenInResponse(xhr);
|
||||
|
||||
// If 401 Unauthorized, check WWW-Authenticate for authentication options
|
||||
if (xhr.status === 401) {
|
||||
var authenticate = xhr.getResponseHeader("WWW-Authenticate");
|
||||
var element = $('div.other-login-methods');
|
||||
if(authenticate !== null) {
|
||||
var realms = authenticate.match(/(\w+ (\w+=((".*?")|[^,]*)(, )?)*)/g);
|
||||
@@ -146,6 +147,18 @@
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check current response headers to see if the CSRF Token has changed. If a new value is found in headers,
|
||||
* save the new value into our "MyHalBrowserCsrfToken" cookie.
|
||||
**/
|
||||
function checkForUpdatedCSRFTokenInResponse(jqxhr) {
|
||||
// look for DSpace-XSRF-TOKEN header & save to our MyHalBrowserCsrfToken cookie (if found)
|
||||
var updatedCsrfToken = jqxhr.getResponseHeader('DSPACE-XSRF-TOKEN');
|
||||
if (updatedCsrfToken != null) {
|
||||
document.cookie = "MyHalBrowserCsrfToken=" + updatedCsrfToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSRF Token by parsing it out of the "MyHalBrowserCsrfToken" cookie.
|
||||
* This cookie is set in login.html after a successful login occurs.
|
||||
|
@@ -629,25 +629,16 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
|
||||
public void addChildGroupTest() throws Exception {
|
||||
|
||||
GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
|
||||
|
||||
Group parentGroup = null;
|
||||
Group childGroup1 = null;
|
||||
Group childGroup2 = null;
|
||||
|
||||
try {
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
parentGroup = groupService.create(context);
|
||||
childGroup1 = groupService.create(context);
|
||||
childGroup2 = groupService.create(context);
|
||||
|
||||
context.commit();
|
||||
|
||||
parentGroup = context.reloadEntity(parentGroup);
|
||||
childGroup1 = context.reloadEntity(childGroup1);
|
||||
childGroup2 = context.reloadEntity(childGroup2);
|
||||
|
||||
EPerson member = EPersonBuilder.createEPerson(context).build();
|
||||
Group parentGroup = GroupBuilder.createGroup(context).build();
|
||||
Group parentGroupWithPreviousSubgroup = GroupBuilder.createGroup(context).build();
|
||||
Group subGroup = GroupBuilder.createGroup(context).withParent(parentGroupWithPreviousSubgroup)
|
||||
.addMember(eperson).build();
|
||||
Group childGroup1 = GroupBuilder.createGroup(context).addMember(member).build();
|
||||
Group childGroup2 = GroupBuilder.createGroup(context).build();
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
String authToken = getAuthToken(admin.getEmail(), password);
|
||||
getClient(authToken).perform(
|
||||
post("/api/eperson/groups/" + parentGroup.getID() + "/subgroups")
|
||||
@@ -656,30 +647,49 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
|
||||
+ REST_SERVER_URL + "eperson/groups/" + childGroup2.getID()
|
||||
)
|
||||
).andExpect(status().isNoContent());
|
||||
getClient(authToken).perform(
|
||||
post("/api/eperson/groups/" + parentGroupWithPreviousSubgroup.getID() + "/subgroups")
|
||||
.contentType(parseMediaType(TEXT_URI_LIST_VALUE))
|
||||
.content(REST_SERVER_URL + "eperson/groups/" + childGroup1.getID() + "/\n"
|
||||
+ REST_SERVER_URL + "eperson/groups/" + childGroup2.getID()
|
||||
)
|
||||
).andExpect(status().isNoContent());
|
||||
|
||||
parentGroup = context.reloadEntity(parentGroup);
|
||||
parentGroupWithPreviousSubgroup = context.reloadEntity(parentGroupWithPreviousSubgroup);
|
||||
subGroup = context.reloadEntity(subGroup);
|
||||
childGroup1 = context.reloadEntity(childGroup1);
|
||||
childGroup2 = context.reloadEntity(childGroup2);
|
||||
|
||||
assertTrue(
|
||||
groupService.isMember(parentGroup, childGroup1)
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
groupService.isMember(parentGroup, childGroup2)
|
||||
);
|
||||
// member of the added groups should be member of the group now
|
||||
assertTrue(
|
||||
groupService.isMember(context, member, parentGroup)
|
||||
);
|
||||
|
||||
// verify that the previous subGroup is still here
|
||||
assertTrue(
|
||||
groupService.isMember(parentGroupWithPreviousSubgroup, childGroup1)
|
||||
);
|
||||
assertTrue(
|
||||
groupService.isMember(parentGroupWithPreviousSubgroup, childGroup2)
|
||||
);
|
||||
assertTrue(
|
||||
groupService.isMember(parentGroupWithPreviousSubgroup, subGroup)
|
||||
);
|
||||
// and that both the member of the added groups than existing ones are still member
|
||||
assertTrue(
|
||||
groupService.isMember(context, member, parentGroupWithPreviousSubgroup)
|
||||
);
|
||||
assertTrue(
|
||||
groupService.isMember(context, eperson, parentGroupWithPreviousSubgroup)
|
||||
);
|
||||
|
||||
} finally {
|
||||
if (parentGroup != null) {
|
||||
GroupBuilder.deleteGroup(parentGroup.getID());
|
||||
}
|
||||
if (childGroup1 != null) {
|
||||
GroupBuilder.deleteGroup(childGroup1.getID());
|
||||
}
|
||||
if (childGroup2 != null) {
|
||||
GroupBuilder.deleteGroup(childGroup2.getID());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -942,28 +952,16 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void addMemberTest() throws Exception {
|
||||
|
||||
GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
|
||||
EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
|
||||
|
||||
Group parentGroup = null;
|
||||
EPerson member1 = null;
|
||||
EPerson member2 = null;
|
||||
|
||||
try {
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
parentGroup = groupService.create(context);
|
||||
member1 = ePersonService.create(context);
|
||||
member2 = ePersonService.create(context);
|
||||
|
||||
context.commit();
|
||||
|
||||
parentGroup = context.reloadEntity(parentGroup);
|
||||
member1 = context.reloadEntity(member1);
|
||||
member2 = context.reloadEntity(member2);
|
||||
|
||||
Group parentGroup = GroupBuilder.createGroup(context).build();
|
||||
Group parentGroupWithPreviousMember = GroupBuilder.createGroup(context).addMember(eperson).build();
|
||||
Group groupWithSubgroup = GroupBuilder.createGroup(context).build();
|
||||
Group subGroup = GroupBuilder.createGroup(context).withParent(groupWithSubgroup).build();
|
||||
EPerson member1 = EPersonBuilder.createEPerson(context).build();
|
||||
EPerson member2 = EPersonBuilder.createEPerson(context).build();
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
String authToken = getAuthToken(admin.getEmail(), password);
|
||||
getClient(authToken).perform(
|
||||
post("/api/eperson/groups/" + parentGroup.getID() + "/epersons")
|
||||
@@ -972,30 +970,46 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
|
||||
+ REST_SERVER_URL + "eperson/groups/" + member2.getID()
|
||||
)
|
||||
).andExpect(status().isNoContent());
|
||||
|
||||
getClient(authToken).perform(
|
||||
post("/api/eperson/groups/" + parentGroupWithPreviousMember.getID() + "/epersons")
|
||||
.contentType(parseMediaType(TEXT_URI_LIST_VALUE))
|
||||
.content(REST_SERVER_URL + "eperson/groups/" + member1.getID() + "/\n"
|
||||
+ REST_SERVER_URL + "eperson/groups/" + member2.getID()
|
||||
)
|
||||
).andExpect(status().isNoContent());
|
||||
getClient(authToken).perform(
|
||||
post("/api/eperson/groups/" + subGroup.getID() + "/epersons")
|
||||
.contentType(parseMediaType(TEXT_URI_LIST_VALUE))
|
||||
.content(REST_SERVER_URL + "eperson/groups/" + member1.getID()
|
||||
)
|
||||
).andExpect(status().isNoContent());
|
||||
parentGroup = context.reloadEntity(parentGroup);
|
||||
parentGroupWithPreviousMember = context.reloadEntity(parentGroupWithPreviousMember);
|
||||
groupWithSubgroup = context.reloadEntity(groupWithSubgroup);
|
||||
member1 = context.reloadEntity(member1);
|
||||
member2 = context.reloadEntity(member2);
|
||||
eperson = context.reloadEntity(eperson);
|
||||
|
||||
assertTrue(
|
||||
groupService.isMember(context, member1, parentGroup)
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
groupService.isMember(context, member2, parentGroup)
|
||||
);
|
||||
|
||||
} finally {
|
||||
if (parentGroup != null) {
|
||||
GroupBuilder.deleteGroup(parentGroup.getID());
|
||||
}
|
||||
if (member1 != null) {
|
||||
EPersonBuilder.deleteEPerson(member1.getID());
|
||||
}
|
||||
if (member2 != null) {
|
||||
EPersonBuilder.deleteEPerson(member2.getID());
|
||||
}
|
||||
}
|
||||
assertTrue(
|
||||
groupService.isMember(context, member1, parentGroupWithPreviousMember)
|
||||
);
|
||||
assertTrue(
|
||||
groupService.isMember(context, member2, parentGroupWithPreviousMember)
|
||||
);
|
||||
assertTrue(
|
||||
groupService.isMember(context, eperson, parentGroupWithPreviousMember)
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
groupService.isMember(context, member1, groupWithSubgroup)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1253,22 +1267,11 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
|
||||
public void removeChildGroupTest() throws Exception {
|
||||
|
||||
GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
|
||||
|
||||
Group parentGroup = null;
|
||||
Group childGroup = null;
|
||||
|
||||
try {
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
parentGroup = groupService.create(context);
|
||||
childGroup = groupService.create(context);
|
||||
groupService.addMember(context, parentGroup, childGroup);
|
||||
|
||||
context.commit();
|
||||
|
||||
parentGroup = context.reloadEntity(parentGroup);
|
||||
childGroup = context.reloadEntity(childGroup);
|
||||
|
||||
Group parentGroup = GroupBuilder.createGroup(context).build();
|
||||
Group childGroup = GroupBuilder.createGroup(context).withParent(parentGroup).build();
|
||||
Group childGroupWithMember = GroupBuilder.createGroup(context).addMember(eperson).withParent(parentGroup)
|
||||
.build();
|
||||
context.restoreAuthSystemState();
|
||||
String authToken = getAuthToken(admin.getEmail(), password);
|
||||
getClient(authToken).perform(
|
||||
@@ -1277,19 +1280,34 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
|
||||
|
||||
parentGroup = context.reloadEntity(parentGroup);
|
||||
childGroup = context.reloadEntity(childGroup);
|
||||
childGroupWithMember = context.reloadEntity(childGroupWithMember);
|
||||
eperson = context.reloadEntity(eperson);
|
||||
|
||||
assertFalse(
|
||||
groupService.isMember(parentGroup, childGroup)
|
||||
);
|
||||
assertTrue(
|
||||
groupService.isMember(parentGroup, childGroupWithMember)
|
||||
);
|
||||
assertTrue(
|
||||
groupService.isMember(context, eperson, parentGroup)
|
||||
);
|
||||
|
||||
getClient(authToken).perform(
|
||||
delete("/api/eperson/groups/" + parentGroup.getID() + "/subgroups/" + childGroupWithMember.getID())
|
||||
).andExpect(status().isNoContent());
|
||||
|
||||
parentGroup = context.reloadEntity(parentGroup);
|
||||
childGroup = context.reloadEntity(childGroup);
|
||||
childGroupWithMember = context.reloadEntity(childGroupWithMember);
|
||||
eperson = context.reloadEntity(eperson);
|
||||
assertFalse(
|
||||
groupService.isMember(parentGroup, childGroupWithMember)
|
||||
);
|
||||
assertFalse(
|
||||
groupService.isMember(context, eperson, parentGroup)
|
||||
);
|
||||
|
||||
} finally {
|
||||
if (parentGroup != null) {
|
||||
GroupBuilder.deleteGroup(parentGroup.getID());
|
||||
}
|
||||
if (childGroup != null) {
|
||||
GroupBuilder.deleteGroup(childGroup.getID());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1493,45 +1511,38 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
|
||||
public void removeMemberTest() throws Exception {
|
||||
|
||||
GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
|
||||
EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
|
||||
|
||||
Group parentGroup = null;
|
||||
EPerson member = null;
|
||||
|
||||
try {
|
||||
context.turnOffAuthorisationSystem();
|
||||
|
||||
parentGroup = groupService.create(context);
|
||||
member = ePersonService.create(context);
|
||||
groupService.addMember(context, parentGroup, member);
|
||||
assertTrue(groupService.isMember(context, member, parentGroup));
|
||||
|
||||
context.commit();
|
||||
|
||||
parentGroup = context.reloadEntity(parentGroup);
|
||||
member = context.reloadEntity(member);
|
||||
|
||||
EPerson member = EPersonBuilder.createEPerson(context).build();
|
||||
EPerson member2 = EPersonBuilder.createEPerson(context).build();
|
||||
Group parentGroup = GroupBuilder.createGroup(context).addMember(member).build();
|
||||
Group childGroup = GroupBuilder.createGroup(context).withParent(parentGroup).addMember(member2).build();
|
||||
context.restoreAuthSystemState();
|
||||
|
||||
assertTrue(
|
||||
groupService.isMember(context, member, parentGroup)
|
||||
);
|
||||
// member2 is member of the parentGroup via the childGroup
|
||||
assertTrue(
|
||||
groupService.isMember(context, member2, parentGroup)
|
||||
);
|
||||
|
||||
String authToken = getAuthToken(admin.getEmail(), password);
|
||||
getClient(authToken).perform(
|
||||
delete("/api/eperson/groups/" + parentGroup.getID() + "/epersons/" + member.getID())
|
||||
).andExpect(status().isNoContent());
|
||||
|
||||
// remove the member2 from the children group
|
||||
getClient(authToken).perform(
|
||||
delete("/api/eperson/groups/" + childGroup.getID() + "/epersons/" + member2.getID())
|
||||
).andExpect(status().isNoContent());
|
||||
parentGroup = context.reloadEntity(parentGroup);
|
||||
member = context.reloadEntity(member);
|
||||
|
||||
assertFalse(
|
||||
groupService.isMember(context, member, parentGroup)
|
||||
);
|
||||
|
||||
} finally {
|
||||
if (parentGroup != null) {
|
||||
GroupBuilder.deleteGroup(parentGroup.getID());
|
||||
}
|
||||
if (member != null) {
|
||||
EPersonBuilder.deleteEPerson(member.getID());
|
||||
}
|
||||
}
|
||||
assertFalse(
|
||||
groupService.isMember(context, member2, parentGroup)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -91,6 +91,20 @@ public class RestResourceControllerIT extends AbstractControllerIntegrationTest
|
||||
endsWith("/api/core/metadatafields/search/byFieldName")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selfLinkContainsRequestParametersAndEmbedsWhenProvided() throws Exception {
|
||||
// When we call a search endpoint with additional parameters and an embed parameter
|
||||
getClient().perform(get("/api/core/metadatafields/search/byFieldName?schema=dc&offset=0&embed=schema"))
|
||||
// The self link should contain those same parameters
|
||||
.andExpect(jsonPath("$._links.self.href", endsWith(
|
||||
"/api/core/metadatafields/search/byFieldName?schema=dc&offset=0")));
|
||||
|
||||
getClient().perform(get("/api/core/metadatafields/search/byFieldName?schema=dc&offset=0&embed.size=schema=5"))
|
||||
// The self link should contain those same parameters
|
||||
.andExpect(jsonPath("$._links.self.href", endsWith(
|
||||
"/api/core/metadatafields/search/byFieldName?schema=dc&offset=0")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user