mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2024-11-10 18:00:11 +01:00
Compare commits
20 Commits
e824a3e7bd
...
6606850e4a
Author | SHA1 | Date | |
---|---|---|---|
|
6606850e4a | ||
|
7b08d98232 | ||
|
03150c6462 | ||
|
a3bf7baf35 | ||
|
6c09bcf23c | ||
|
e11fa01d10 | ||
|
d60107f48b | ||
|
0b449af9ba | ||
|
4c9c0207ba | ||
|
d36a59442f | ||
|
fd4c75279f | ||
|
fd5f5025ce | ||
|
319ecbcbc1 | ||
|
04b0bcde61 | ||
|
6499b759d9 | ||
|
2081c4872d | ||
|
37c75971f2 | ||
|
7d9edfca6d | ||
|
a40696f16e | ||
|
515b5b1492 |
22
README.md
22
README.md
@ -159,7 +159,7 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR
|
||||
|
||||
## Supported Languages
|
||||
|
||||
Stirling PDF currently supports 28!
|
||||
Stirling PDF currently supports 32!
|
||||
|
||||
| Language | Progress |
|
||||
| ------------------------------------------- | -------------------------------------- |
|
||||
@ -167,15 +167,15 @@ Stirling PDF currently supports 28!
|
||||
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
|
||||
| Arabic (العربية) (ar_AR) | ![40%](https://geps.dev/progress/40) |
|
||||
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
|
||||
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) |
|
||||
| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) |
|
||||
| Spanish (Español) (es_ES) | ![95%](https://geps.dev/progress/95) |
|
||||
| Simplified Chinese (简体中文) (zh_CN) | ![95%](https://geps.dev/progress/95) |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) | ![94%](https://geps.dev/progress/94) |
|
||||
| Catalan (Català) (ca_CA) | ![49%](https://geps.dev/progress/49) |
|
||||
| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
|
||||
| Swedish (Svenska) (sv_SE) | ![40%](https://geps.dev/progress/40) |
|
||||
| Polish (Polski) (pl_PL) | ![43%](https://geps.dev/progress/43) |
|
||||
| Romanian (Română) (ro_RO) | ![40%](https://geps.dev/progress/40) |
|
||||
| Polish (Polski) (pl_PL) | ![42%](https://geps.dev/progress/42) |
|
||||
| Romanian (Română) (ro_RO) | ![39%](https://geps.dev/progress/39) |
|
||||
| Korean (한국어) (ko_KR) | ![87%](https://geps.dev/progress/87) |
|
||||
| Portuguese Brazilian (Português) (pt_BR) | ![61%](https://geps.dev/progress/61) |
|
||||
| Russian (Русский) (ru_RU) | ![87%](https://geps.dev/progress/87) |
|
||||
@ -183,17 +183,17 @@ Stirling PDF currently supports 28!
|
||||
| Japanese (日本語) (ja_JP) | ![87%](https://geps.dev/progress/87) |
|
||||
| Dutch (Nederlands) (nl_NL) | ![85%](https://geps.dev/progress/85) |
|
||||
| Greek (Ελληνικά) (el_GR) | ![85%](https://geps.dev/progress/85) |
|
||||
| Turkish (Türkçe) (tr_TR) | ![98%](https://geps.dev/progress/98) |
|
||||
| Indonesia (Bahasa Indonesia) (id_ID) | ![79%](https://geps.dev/progress/79) |
|
||||
| Turkish (Türkçe) (tr_TR) | ![97%](https://geps.dev/progress/97) |
|
||||
| Indonesia (Bahasa Indonesia) (id_ID) | ![78%](https://geps.dev/progress/78) |
|
||||
| Hindi (हिंदी) (hi_IN) | ![79%](https://geps.dev/progress/79) |
|
||||
| Hungarian (Magyar) (hu_HU) | ![78%](https://geps.dev/progress/78) |
|
||||
| Bulgarian (Български) (bg_BG) | ![98%](https://geps.dev/progress/98) |
|
||||
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![81%](https://geps.dev/progress/81) |
|
||||
| Ukrainian (Українська) (uk_UA) | ![87%](https://geps.dev/progress/87) |
|
||||
| Slovakian (Slovensky) (sk_SK) | ![96%](https://geps.dev/progress/96) |
|
||||
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![80%](https://geps.dev/progress/80) |
|
||||
| Ukrainian (Українська) (uk_UA) | ![86%](https://geps.dev/progress/86) |
|
||||
| Slovakian (Slovensky) (sk_SK) | ![95%](https://geps.dev/progress/95) |
|
||||
| Czech (Česky) (cs_CZ) | ![94%](https://geps.dev/progress/94) |
|
||||
| Croatian (Hrvatski) (hr_HR) | ![94%](https://geps.dev/progress/94) |
|
||||
| Norwegian (Norsk) (no_NB) | ![94%](https://geps.dev/progress/94) |
|
||||
| Croatian (Hrvatski) (hr_HR) | ![98%](https://geps.dev/progress/98) |
|
||||
| Norwegian (Norsk) (no_NB) | ![98%](https://geps.dev/progress/98) |
|
||||
|
||||
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||
|
||||
|
@ -12,7 +12,7 @@ plugins {
|
||||
import com.github.jk1.license.render.*
|
||||
|
||||
group = 'stirling.software'
|
||||
version = '0.25.1'
|
||||
version = '0.25.2'
|
||||
|
||||
//17 is lowest but we support and recommend 21
|
||||
sourceCompatibility = '17'
|
||||
@ -178,7 +178,7 @@ compileJava {
|
||||
options.compilerArgs << '-parameters'
|
||||
}
|
||||
|
||||
tasks.register('writeVersion') {
|
||||
task writeVersion {
|
||||
def propsFile = file('src/main/resources/version.properties')
|
||||
def props = new Properties()
|
||||
props.setProperty('version', version)
|
||||
@ -208,6 +208,6 @@ tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.register('printVersion') {
|
||||
task printVersion {
|
||||
println project.version
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 0.25.1
|
||||
appVersion: 0.25.2
|
||||
description: locally hosted web application that allows you to perform various operations
|
||||
on PDF files
|
||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||
|
@ -15,7 +15,10 @@ ignore = [
|
||||
|
||||
[cs_CZ]
|
||||
ignore = [
|
||||
'info',
|
||||
'language.direction',
|
||||
'pipeline.header',
|
||||
'text',
|
||||
]
|
||||
|
||||
[de_DE]
|
||||
@ -65,6 +68,16 @@ ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[hr_HR]
|
||||
ignore = [
|
||||
'font',
|
||||
'home.pipeline.title',
|
||||
'info',
|
||||
'language.direction',
|
||||
'pdfOrganiser.tags',
|
||||
'showJS.tags',
|
||||
]
|
||||
|
||||
[hu_HU]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
@ -103,6 +116,11 @@ ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[no_NB]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[pl_PL]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
|
@ -1,15 +1,12 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
@ -47,61 +44,49 @@ public class ConfigInitializer
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Load the template content from classpath
|
||||
List<String> templateLines;
|
||||
try (InputStream in =
|
||||
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||
if (in == null) {
|
||||
throw new FileNotFoundException(
|
||||
"Resource file not found: settings.yml.template");
|
||||
}
|
||||
templateLines = new ArrayList<>();
|
||||
try (var reader = new InputStreamReader(in)) {
|
||||
try (var bufferedReader = new BufferedReader(reader)) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
templateLines.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read the user settings file if it exists
|
||||
Path userPath = Paths.get("configs", "settings.yml");
|
||||
List<String> userLines =
|
||||
Files.exists(userPath) ? Files.readAllLines(userPath) : new ArrayList<>();
|
||||
|
||||
List<String> resultLines = new ArrayList<>();
|
||||
int position = 0;
|
||||
for (String templateLine : templateLines) {
|
||||
// Check if the line is a comment
|
||||
if (templateLine.trim().startsWith("#")) {
|
||||
String entry = templateLine.trim().substring(1).trim();
|
||||
if (!entry.isEmpty()) {
|
||||
// Check if this comment has been uncommented in userLines
|
||||
String key = entry.split(":")[0].trim();
|
||||
addLine(resultLines, userLines, templateLine, key, position);
|
||||
} else {
|
||||
resultLines.add(templateLine);
|
||||
}
|
||||
}
|
||||
// Check if the line is a key-value pair
|
||||
else if (templateLine.contains(":")) {
|
||||
String key = templateLine.split(":")[0].trim();
|
||||
addLine(resultLines, userLines, templateLine, key, position);
|
||||
}
|
||||
// Handle empty lines
|
||||
else if (templateLine.trim().length() == 0) {
|
||||
resultLines.add("");
|
||||
}
|
||||
position++;
|
||||
}
|
||||
|
||||
// Write the result to the user settings file
|
||||
Files.write(userPath, resultLines);
|
||||
// Path templatePath =
|
||||
// Paths.get(
|
||||
// getClass()
|
||||
// .getClassLoader()
|
||||
// .getResource("settings.yml.template")
|
||||
// .toURI());
|
||||
// Path userPath = Paths.get("configs", "settings.yml");
|
||||
//
|
||||
// List<String> templateLines = Files.readAllLines(templatePath);
|
||||
// List<String> userLines =
|
||||
// Files.exists(userPath) ? Files.readAllLines(userPath) : new
|
||||
// ArrayList<>();
|
||||
//
|
||||
// List<String> resultLines = new ArrayList<>();
|
||||
// int position = 0;
|
||||
// for (String templateLine : templateLines) {
|
||||
// // Check if the line is a comment
|
||||
// if (templateLine.trim().startsWith("#")) {
|
||||
// String entry = templateLine.trim().substring(1).trim();
|
||||
// if (!entry.isEmpty()) {
|
||||
// // Check if this comment has been uncommented in userLines
|
||||
// String key = entry.split(":")[0].trim();
|
||||
// addLine(resultLines, userLines, templateLine, key, position);
|
||||
// } else {
|
||||
// resultLines.add(templateLine);
|
||||
// }
|
||||
// }
|
||||
// // Check if the line is a key-value pair
|
||||
// else if (templateLine.contains(":")) {
|
||||
// String key = templateLine.split(":")[0].trim();
|
||||
// addLine(resultLines, userLines, templateLine, key, position);
|
||||
// }
|
||||
// // Handle empty lines
|
||||
// else if (templateLine.trim().length() == 0) {
|
||||
// resultLines.add("");
|
||||
// }
|
||||
// position++;
|
||||
// }
|
||||
//
|
||||
// // Write the result to the user settings file
|
||||
// Files.write(userPath, resultLines);
|
||||
}
|
||||
|
||||
// Ensure the custom settings file exists
|
||||
Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
|
||||
if (!Files.exists(customSettingsPath)) {
|
||||
Files.createFile(customSettingsPath);
|
||||
|
@ -1,16 +1,18 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
|
||||
import org.thymeleaf.templateresource.ClassLoaderTemplateResource;
|
||||
import org.thymeleaf.templateresource.FileTemplateResource;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
import stirling.software.SPDF.model.InputStreamTemplateResource;
|
||||
|
||||
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
@ -40,9 +42,13 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
|
||||
|
||||
}
|
||||
|
||||
return new ClassLoaderTemplateResource(
|
||||
Thread.currentThread().getContextClassLoader(),
|
||||
"classpath:/templates/" + resourceName,
|
||||
characterEncoding);
|
||||
InputStream inputStream =
|
||||
Thread.currentThread()
|
||||
.getContextClassLoader()
|
||||
.getResourceAsStream("templates/" + resourceName);
|
||||
if (inputStream != null) {
|
||||
return new InputStreamTemplateResource(inputStream, "UTF-8");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -238,7 +238,7 @@ public class SecurityConfiguration {
|
||||
GoogleProvider google = client.getGoogle();
|
||||
return google != null && google.isSettingsValid()
|
||||
? Optional.of(
|
||||
ClientRegistration.withRegistrationId("google")
|
||||
ClientRegistration.withRegistrationId(google.getName())
|
||||
.clientId(google.getClientId())
|
||||
.clientSecret(google.getClientSecret())
|
||||
.scope(google.getScopes())
|
||||
@ -246,8 +246,8 @@ public class SecurityConfiguration {
|
||||
.tokenUri(google.getTokenuri())
|
||||
.userInfoUri(google.getUserinfouri())
|
||||
.userNameAttributeName(google.getUseAsUsername())
|
||||
.clientName("Google")
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/google")
|
||||
.clientName(google.getClientName())
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
|
||||
.authorizationGrantType(
|
||||
org.springframework.security.oauth2.core
|
||||
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
@ -269,12 +269,12 @@ public class SecurityConfiguration {
|
||||
return keycloak != null && keycloak.isSettingsValid()
|
||||
? Optional.of(
|
||||
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
||||
.registrationId("keycloak")
|
||||
.registrationId(keycloak.getName())
|
||||
.clientId(keycloak.getClientId())
|
||||
.clientSecret(keycloak.getClientSecret())
|
||||
.scope(keycloak.getScopes())
|
||||
.userNameAttributeName(keycloak.getUseAsUsername())
|
||||
.clientName("Keycloak")
|
||||
.clientName(keycloak.getClientName())
|
||||
.build())
|
||||
: Optional.empty();
|
||||
}
|
||||
@ -291,7 +291,7 @@ public class SecurityConfiguration {
|
||||
GithubProvider github = client.getGithub();
|
||||
return github != null && github.isSettingsValid()
|
||||
? Optional.of(
|
||||
ClientRegistration.withRegistrationId("github")
|
||||
ClientRegistration.withRegistrationId(github.getName())
|
||||
.clientId(github.getClientId())
|
||||
.clientSecret(github.getClientSecret())
|
||||
.scope(github.getScopes())
|
||||
@ -299,8 +299,8 @@ public class SecurityConfiguration {
|
||||
.tokenUri(github.getTokenuri())
|
||||
.userInfoUri(github.getUserinfouri())
|
||||
.userNameAttributeName(github.getUseAsUsername())
|
||||
.clientName("GitHub")
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/github")
|
||||
.clientName(github.getClientName())
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
|
||||
.authorizationGrantType(
|
||||
org.springframework.security.oauth2.core
|
||||
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
|
@ -81,7 +81,7 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
||||
logger.info("Session invalidated: " + sessionId);
|
||||
}
|
||||
|
||||
switch (registrationId) {
|
||||
switch (registrationId.toLowerCase()) {
|
||||
case "keycloak":
|
||||
// Add Keycloak specific logout URL if needed
|
||||
String logoutUrl =
|
||||
|
@ -16,6 +16,8 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||
import stirling.software.SPDF.model.User;
|
||||
|
||||
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||
@ -41,11 +43,27 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
||||
|
||||
@Override
|
||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||
String usernameAttribute =
|
||||
applicationProperties.getSecurity().getOAUTH2().getUseAsUsername();
|
||||
OAUTH2 oauth2 = applicationProperties.getSecurity().getOAUTH2();
|
||||
String usernameAttribute = oauth2.getUseAsUsername();
|
||||
if (usernameAttribute == null || usernameAttribute.trim().isEmpty()) {
|
||||
Client client = oauth2.getClient();
|
||||
if (client != null && client.getKeycloak() != null) {
|
||||
usernameAttribute = client.getKeycloak().getUseAsUsername();
|
||||
} else {
|
||||
usernameAttribute = "email";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
OidcUser user = delegate.loadUser(userRequest);
|
||||
String username = user.getUserInfo().getClaimAsString(usernameAttribute);
|
||||
|
||||
// Check if the username claim is null or empty
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Claim '" + usernameAttribute + "' cannot be null or empty");
|
||||
}
|
||||
|
||||
Optional<User> duser = userService.findByUsernameIgnoreCase(username);
|
||||
if (duser.isPresent()) {
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
@ -56,13 +74,14 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
||||
throw new IllegalArgumentException("Password must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
// Return a new OidcUser with adjusted attributes
|
||||
return new DefaultOidcUser(
|
||||
user.getAuthorities(),
|
||||
userRequest.getIdToken(),
|
||||
user.getUserInfo(),
|
||||
usernameAttribute);
|
||||
} catch (java.lang.IllegalArgumentException e) {
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Error loading OIDC user: {}", e.getMessage());
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
||||
} catch (Exception e) {
|
||||
|
@ -66,46 +66,46 @@ public class UserController {
|
||||
RedirectAttributes redirectAttributes) {
|
||||
|
||||
if (!userService.isUsernameValid(newUsername)) {
|
||||
return new RedirectView("/account?messageType=invalidUsername");
|
||||
return new RedirectView("/account?messageType=invalidUsername", true);
|
||||
}
|
||||
|
||||
if (principal == null) {
|
||||
return new RedirectView("/account?messageType=notAuthenticated");
|
||||
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||
}
|
||||
|
||||
// The username MUST be unique when renaming
|
||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||
|
||||
if (userOpt == null || userOpt.isEmpty()) {
|
||||
return new RedirectView("/account?messageType=userNotFound");
|
||||
return new RedirectView("/account?messageType=userNotFound", true);
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
if (user.getUsername().equals(newUsername)) {
|
||||
return new RedirectView("/account?messageType=usernameExists");
|
||||
return new RedirectView("/account?messageType=usernameExists", true);
|
||||
}
|
||||
|
||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return new RedirectView("/account?messageType=incorrectPassword");
|
||||
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||
}
|
||||
|
||||
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||
return new RedirectView("/account?messageType=usernameExists");
|
||||
return new RedirectView("/account?messageType=usernameExists", true);
|
||||
}
|
||||
|
||||
if (newUsername != null && newUsername.length() > 0) {
|
||||
try {
|
||||
userService.changeUsername(user, newUsername);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return new RedirectView("/account?messageType=invalidUsername");
|
||||
return new RedirectView("/account?messageType=invalidUsername", true);
|
||||
}
|
||||
}
|
||||
|
||||
// Logout using Spring's utility
|
||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||
|
||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED);
|
||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@ -118,19 +118,19 @@ public class UserController {
|
||||
HttpServletResponse response,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
if (principal == null) {
|
||||
return new RedirectView("/change-creds?messageType=notAuthenticated");
|
||||
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||
|
||||
if (userOpt == null || userOpt.isEmpty()) {
|
||||
return new RedirectView("/change-creds?messageType=userNotFound");
|
||||
return new RedirectView("/change-creds?messageType=userNotFound", true);
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return new RedirectView("/change-creds?messageType=incorrectPassword");
|
||||
return new RedirectView("/change-creds?messageType=incorrectPassword", true);
|
||||
}
|
||||
|
||||
userService.changePassword(user, newPassword);
|
||||
@ -138,7 +138,7 @@ public class UserController {
|
||||
// Logout using Spring's utility
|
||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||
|
||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED);
|
||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@ -151,19 +151,19 @@ public class UserController {
|
||||
HttpServletResponse response,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
if (principal == null) {
|
||||
return new RedirectView("/account?messageType=notAuthenticated");
|
||||
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||
|
||||
if (userOpt == null || userOpt.isEmpty()) {
|
||||
return new RedirectView("/account?messageType=userNotFound");
|
||||
return new RedirectView("/account?messageType=userNotFound", true);
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return new RedirectView("/account?messageType=incorrectPassword");
|
||||
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||
}
|
||||
|
||||
userService.changePassword(user, newPassword);
|
||||
@ -171,7 +171,7 @@ public class UserController {
|
||||
// Logout using Spring's utility
|
||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||
|
||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED);
|
||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@ -204,7 +204,7 @@ public class UserController {
|
||||
boolean forceChange) {
|
||||
|
||||
if (!userService.isUsernameValid(username)) {
|
||||
return new RedirectView("/addUsers?messageType=invalidUsername");
|
||||
return new RedirectView("/addUsers?messageType=invalidUsername", true);
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
@ -212,26 +212,27 @@ public class UserController {
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
if (user != null && user.getUsername().equalsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=usernameExists");
|
||||
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
||||
}
|
||||
}
|
||||
if (userService.usernameExistsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=usernameExists");
|
||||
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
||||
}
|
||||
try {
|
||||
// Validate the role
|
||||
Role roleEnum = Role.fromString(role);
|
||||
if (roleEnum == Role.INTERNAL_API_USER) {
|
||||
// If the role is INTERNAL_API_USER, reject the request
|
||||
return new RedirectView("/addUsers?messageType=invalidRole");
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If the role ID is not valid, redirect with an error message
|
||||
return new RedirectView("/addUsers?messageType=invalidRole");
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
}
|
||||
|
||||
userService.saveUser(username, password, role, forceChange);
|
||||
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
||||
return new RedirectView(
|
||||
"/addUsers", true); // Redirect to account page after adding the user
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@ -244,33 +245,34 @@ public class UserController {
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
|
||||
if (!userOpt.isPresent()) {
|
||||
return new RedirectView("/addUsers?messageType=userNotFound");
|
||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||
}
|
||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=userNotFound");
|
||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||
}
|
||||
// Get the currently authenticated username
|
||||
String currentUsername = authentication.getName();
|
||||
|
||||
// Check if the provided username matches the current session's username
|
||||
if (currentUsername.equalsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=downgradeCurrentUser");
|
||||
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
|
||||
}
|
||||
try {
|
||||
// Validate the role
|
||||
Role roleEnum = Role.fromString(role);
|
||||
if (roleEnum == Role.INTERNAL_API_USER) {
|
||||
// If the role is INTERNAL_API_USER, reject the request
|
||||
return new RedirectView("/addUsers?messageType=invalidRole");
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If the role ID is not valid, redirect with an error message
|
||||
return new RedirectView("/addUsers?messageType=invalidRole");
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
}
|
||||
User user = userOpt.get();
|
||||
|
||||
userService.changeRole(user, role);
|
||||
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
||||
return new RedirectView(
|
||||
"/addUsers", true); // Redirect to account page after adding the user
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@ -279,7 +281,7 @@ public class UserController {
|
||||
@PathVariable(name = "username") String username, Authentication authentication) {
|
||||
|
||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=deleteUsernameExists");
|
||||
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
|
||||
}
|
||||
|
||||
// Get the currently authenticated username
|
||||
@ -287,11 +289,11 @@ public class UserController {
|
||||
|
||||
// Check if the provided username matches the current session's username
|
||||
if (currentUsername.equalsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=deleteCurrentUser");
|
||||
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
||||
}
|
||||
invalidateUserSessions(username);
|
||||
userService.deleteUser(username);
|
||||
return new RedirectView("/addUsers");
|
||||
return new RedirectView("/addUsers", true);
|
||||
}
|
||||
|
||||
@Autowired private SessionRegistry sessionRegistry;
|
||||
|
@ -52,23 +52,23 @@ public class AccountWebController {
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||
if (oauth != null) {
|
||||
if (oauth.isSettingsValid()) {
|
||||
providerList.put("oidc", "OpenID Connect");
|
||||
providerList.put("oidc", oauth.getProvider());
|
||||
}
|
||||
Client client = oauth.getClient();
|
||||
if (client != null) {
|
||||
GoogleProvider google = client.getGoogle();
|
||||
if (google.isSettingsValid()) {
|
||||
providerList.put("google", "Google");
|
||||
providerList.put(google.getName(), google.getClientName());
|
||||
}
|
||||
|
||||
GithubProvider github = client.getGithub();
|
||||
if (github.isSettingsValid()) {
|
||||
providerList.put("github", "Github");
|
||||
providerList.put(github.getName(), github.getClientName());
|
||||
}
|
||||
|
||||
KeycloakProvider keycloak = client.getKeycloak();
|
||||
if (keycloak.isSettingsValid()) {
|
||||
providerList.put("keycloak", "Keycloak");
|
||||
providerList.put(keycloak.getName(), keycloak.getClientName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -356,7 +356,7 @@ public class ApplicationProperties {
|
||||
private KeycloakProvider keycloak = new KeycloakProvider();
|
||||
|
||||
public Provider get(String registrationId) throws Exception {
|
||||
switch (registrationId) {
|
||||
switch (registrationId.toLowerCase()) {
|
||||
case "google":
|
||||
return getGoogle();
|
||||
case "github":
|
||||
@ -455,6 +455,7 @@ public class ApplicationProperties {
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("https://www.googleapis.com/auth/userinfo.email");
|
||||
scopes.add("https://www.googleapis.com/auth/userinfo.profile");
|
||||
}
|
||||
@ -495,6 +496,11 @@ public class ApplicationProperties {
|
||||
return "google";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return "Google";
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return super.isValid(this.getClientId(), "clientId")
|
||||
&& super.isValid(this.getClientSecret(), "clientSecret")
|
||||
@ -555,6 +561,7 @@ public class ApplicationProperties {
|
||||
|
||||
public Collection<String> getScopes() {
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("read:user");
|
||||
}
|
||||
return scopes;
|
||||
@ -594,6 +601,11 @@ public class ApplicationProperties {
|
||||
return "github";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return "GitHub";
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return super.isValid(this.getClientId(), "clientId")
|
||||
&& super.isValid(this.getClientSecret(), "clientSecret")
|
||||
@ -642,7 +654,7 @@ public class ApplicationProperties {
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes.add("openid");
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("profile");
|
||||
scopes.add("email");
|
||||
}
|
||||
@ -684,6 +696,11 @@ public class ApplicationProperties {
|
||||
return "keycloak";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return "Keycloak";
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return isValid(this.getIssuer(), "issuer")
|
||||
&& isValid(this.getClientId(), "clientId")
|
||||
|
@ -0,0 +1,45 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
public class InputStreamTemplateResource implements ITemplateResource {
|
||||
private InputStream inputStream;
|
||||
private String characterEncoding;
|
||||
|
||||
public InputStreamTemplateResource(InputStream inputStream, String characterEncoding) {
|
||||
this.inputStream = inputStream;
|
||||
this.characterEncoding = characterEncoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader reader() throws IOException {
|
||||
return new InputStreamReader(inputStream, characterEncoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITemplateResource relative(String relativeLocation) {
|
||||
// Implement logic for relative resources, if needed
|
||||
throw new UnsupportedOperationException("Relative resources not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "InputStream resource [Stream]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseName() {
|
||||
return "streamResource";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
// TODO Auto-generated method stub
|
||||
return false;
|
||||
}
|
||||
}
|
@ -4,11 +4,16 @@ import java.util.Collection;
|
||||
|
||||
public class Provider implements ProviderInterface {
|
||||
private String name;
|
||||
private String clientName;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getClientName() {
|
||||
return clientName;
|
||||
}
|
||||
|
||||
protected boolean isValid(String value, String name) {
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
return true;
|
||||
|
@ -26,7 +26,7 @@ bored=Nudíte se při čekání?
|
||||
alphabet=Abeceda
|
||||
downloadPdf=Stáhnout PDF
|
||||
text=Text
|
||||
font=Font
|
||||
font=Písmo
|
||||
selectFillter=-- Vyberte --
|
||||
pageNum=Číslo stránky
|
||||
sizes.small=Malé
|
||||
|
@ -71,7 +71,7 @@ visitGithub=GitHub-Repository besuchen
|
||||
donate=Spenden
|
||||
color=Farbe
|
||||
sponsor=Sponsor
|
||||
info=Die Info
|
||||
info=Informationen
|
||||
|
||||
|
||||
|
||||
@ -660,10 +660,10 @@ certSign.submit=PDF signieren
|
||||
|
||||
|
||||
#removeCertSign
|
||||
removeCertSign.title=Remove Certificate Signature
|
||||
removeCertSign.header=Remove the digital certificate from the PDF
|
||||
removeCertSign.selectPDF=Select a PDF file:
|
||||
removeCertSign.submit=Remove Signature
|
||||
removeCertSign.title=Zertifikatsignatur entfernen
|
||||
removeCertSign.header=Digitales Zertifikat aus dem PDF entfernen
|
||||
removeCertSign.selectPDF=PDF-Datei auswählen:
|
||||
removeCertSign.submit=Signatur entfernen
|
||||
|
||||
|
||||
#removeBlanks
|
||||
|
@ -1,7 +1,7 @@
|
||||
###########
|
||||
# Generic #
|
||||
###########
|
||||
# the direction that the language is written (ltr=left to right, rtl = right to left)
|
||||
# the direction that the language is written (ltr = left to right, rtl = right to left)
|
||||
language.direction=ltr
|
||||
|
||||
pdfPrompt=Odaberi PDF(ove)
|
||||
@ -331,9 +331,10 @@ compare.tags=razlikovati,kontrast,izmjene,analiza
|
||||
home.certSign.title=Potpišite s certifikatom
|
||||
home.certSign.desc=Potpisuje PDF s certifikatom/ključem (PEM/P12)
|
||||
certSign.tags=autentifikacija,PEM,P12,zvanično,šifriranje
|
||||
# home.removeCertSign.title=Remove Certificate Sign
|
||||
# home.removeCertSign.desc=Remove certificate signature from PDF
|
||||
# removeCertSign.tags=authenticate,PEM,P12,official,decrypt
|
||||
|
||||
home.removeCertSign.title=Remove Certificate Sign
|
||||
home.removeCertSign.desc=Remove certificate signature from PDF
|
||||
removeCertSign.tags=authenticate,PEM,P12,official,decrypt
|
||||
|
||||
home.pageLayout.title=Izgled s više stranica
|
||||
home.pageLayout.desc=Spojite više stranica PDF dokumenta u jednu stranicu
|
||||
@ -656,10 +657,13 @@ certSign.reason=Razlog
|
||||
certSign.location=Mjesto
|
||||
certSign.name=Ime
|
||||
certSign.submit=Potpiši PDF
|
||||
# removeCertSign.title=Remove Certificate Signature
|
||||
# removeCertSign.header=Remove the digital certificate from the PDF
|
||||
# removeCertSign.selectPDF=Select a PDF file:
|
||||
# removeCertSign.submit=Remove Signature
|
||||
|
||||
|
||||
#removeCertSign
|
||||
removeCertSign.title=Remove Certificate Signature
|
||||
removeCertSign.header=Remove the digital certificate from the PDF
|
||||
removeCertSign.selectPDF=Select a PDF file:
|
||||
removeCertSign.submit=Remove Signature
|
||||
|
||||
|
||||
#removeBlanks
|
||||
|
@ -438,7 +438,7 @@ PDFToBook.tags=bok,comic,calibre,konvertere,manga,amazon,kindle,epub,mobi,azw3,d
|
||||
|
||||
home.BookToPDF.title=Bok til PDF
|
||||
home.BookToPDF.desc=Konverter bøker/tegneserier til PDF ved hjelp av calibre
|
||||
|
||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
|
||||
|
||||
|
||||
###########################
|
||||
@ -783,7 +783,7 @@ compress.selectText.2=Optimeringsnivå:
|
||||
compress.selectText.3=4 (Dårlig for tekstbilder)
|
||||
compress.selectText.4=Automatisk modus - Justerer automatisk kvaliteten for å få PDF til nøyaktig størrelse
|
||||
compress.selectText.5=Forventet PDF-størrelse (f.eks. 25MB, 10.8MB, 25KB)
|
||||
compress.Submit=Komprimer
|
||||
compress.submit=Komprimer
|
||||
|
||||
|
||||
#Add image
|
||||
@ -855,7 +855,7 @@ split.desc.6=Dokument #4: Side 8
|
||||
split.desc.7=Dokument #5: Side 9
|
||||
split.desc.8=Dokument #6: Side 10
|
||||
split.splitPages=Skriv inn sidene som skal deles på:
|
||||
split.Submit=Del
|
||||
split.submit=Del
|
||||
|
||||
|
||||
#merge
|
||||
|
@ -34,7 +34,7 @@
|
||||
<td th:text="#{${user.roleName}}"></td>
|
||||
<td>
|
||||
<form th:if="${user.username != currentUsername}" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post">
|
||||
<button type="submit" th:text="#{delete}">Delete</button>
|
||||
<button class="btn btn-danger" type="submit" th:text="#{delete}">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
<td th:text="${user.authenticationType}"></td>
|
||||
@ -47,7 +47,7 @@
|
||||
<span th:text="#{${addMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-info" data-toggle="tooltip" data-placement="auto" th:title="#{adminUserSettings.usernameInfo}" th:text="#{help}">Help</button>
|
||||
<form id="formsaveuser" action="/api/v1/user/admin/saveUser" method="post">
|
||||
<form id="formsaveuser" th:action="@{/api/v1/user/admin/saveUser}" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="username" th:text="#{username}">Username</label>
|
||||
<input type="text" class="form-control" name="username" id="username" th:title="#{adminUserSettings.usernameInfo}" required>
|
||||
@ -78,7 +78,7 @@
|
||||
<div th:if="${changeMessage}" class="alert alert-danger">
|
||||
<span th:text="#{${changeMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
<form action="/api/v1/user/admin/changeRole" method="post">
|
||||
<form th:action="@{/api/v1/user/admin/changeRole}" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="username" th:text="#{username}">Username</label>
|
||||
<select name="username" class="form-control" required>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<span class="material-symbols-rounded tool-header-icon organize">crop</span>
|
||||
<span class="tool-header-text" th:text="#{crop.header}"></span>
|
||||
</div>
|
||||
<form id="cropForm" action="/api/v1/general/crop" method="post" enctype="multipart/form-data">
|
||||
<form id="cropForm" th:action="@{/api/v1/general/crop}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
||||
<input id="x" type="hidden" name="x">
|
||||
<input id="y" type="hidden" name="y">
|
||||
|
@ -21,7 +21,7 @@
|
||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" class="btn btn-primary" target="_blank" th:text="#{error.github}"></a>
|
||||
<a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
|
||||
</div>
|
||||
<a href="/" id="home-button" class="home-button btn btn-primary" th:text="#{goHomepage}"></a>
|
||||
<a th:href="@{/}" id="home-button" class="home-button btn btn-primary" th:text="#{goHomepage}"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
<script src="js/githubVersion.js"></script>
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container ">
|
||||
<a class="navbar-brand" href="/" style="display: flex;">
|
||||
<a class="navbar-brand" th:href="@{/}" style="display: flex;">
|
||||
<img class="main-icon" src="favicon.svg" alt="icon">
|
||||
<span class="icon-text" th:text="${@navBarText}"></span>
|
||||
</a>
|
||||
|
@ -298,7 +298,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
<input type="number" id="pageNumber" class="toolbarField" title="Page" value="1" min="1" tabindex="15" data-l10n-id="pdfjs-page-input" autocomplete="off">
|
||||
</span>
|
||||
<span id="numPages" class="toolbarLabel"></span>
|
||||
<a class="navbar-brand hiddenMediumView" href="/" tabindex="16" >
|
||||
<a class="navbar-brand hiddenMediumView" th:href="@{/}" tabindex="16" >
|
||||
<img class="main-icon" src="favicon.svg" alt="icon" style="max-height: 1.6rem; width: auto;">
|
||||
<span class="icon-text" style="color: #ffffff;" th:text="${@appName}">Stirling PDF</span>
|
||||
</a>
|
||||
@ -308,7 +308,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
<button id="editorHighlight" class="toolbarButton" hidden="true" disabled="disabled" title="Highlight" role="radio" aria-checked="false" aria-controls="editorHighlightParamsToolbar" tabindex="31" data-l10n-id="pdfjs-editor-highlight-button">
|
||||
<span data-l10n-id="pdfjs-editor-highlight-button-label">Highlight</span>
|
||||
</button>
|
||||
<a id="backToHome" class="toolbarButton hiddenMediumView" title="Back to Main Page" role="radio" aria-checked="false" tabindex="32" href="/">
|
||||
<a id="backToHome" class="toolbarButton hiddenMediumView" title="Back to Main Page" role="radio" aria-checked="false" tabindex="32" th:href="@{/}">
|
||||
<span data-l10n-id="pdfjs-open-file-button-label">Back to Main Page</span>
|
||||
</a>
|
||||
<button id="openFile" class="toolbarButton hiddenMediumView" title="Open File" role="radio" aria-checked="false" tabindex="33" data-l10n-id="pdfjs-open-file-button">
|
||||
|
Loading…
Reference in New Issue
Block a user