Merge pull request #307 from Frooodle/login

Login
This commit is contained in:
Anthony Stirling 2023-08-17 22:48:47 +01:00 committed by GitHub
commit 620b954336
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 3349 additions and 678 deletions

2
.gitignore vendored
View file

@ -116,7 +116,7 @@ watchedFolders/
*.zip
*.tar.gz
*.rar
*.db
/build
/.vscode

View file

@ -185,21 +185,30 @@ For those wanting to use Stirling-PDFs backend API to link with their own custom
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
## Login authentication (CURRENTLY ALPHA TAG ONLY)
### Prerequisites:
- User must have the folder ./configs volumed within docker so that it is retained during updates.
- The environment variable 'login.enabled' must be set to true
- The environment variables "INITIAL_USERNAME" and "INITIAL_PASSWORD" must also be populated (only required on first boot to create initial user, ignored after.)
Once the above has been done, on restart a new stirling-pdf-DB.mv.db will show if everything worked.
When you login to Stirling PDF you will be redirected to /login page to login with those credentials. After login everything should function as normal
To access your account settings go to Account settings in the settings cog menu (top right in navbar) this Account settings menu is also where you find your API key.
To add new users go to bottom of Account settings and hit 'Admin Settings', here you can add new users. The different roles mentioned within this are for rate limiting. This is a Work in progress which will be expanding on more in future
For API usage you must provide a header with 'X-API-Key' and the associated API key for that user.
## FAQ
### Q1: Can you add authentication in Stirling PDF?
There is no Auth within Stirling PDF and there is none planned. This feature will not be added. Instead we recommended you use trusted and secure authentication software like Authentik or Authelia.
### Q2: What are your planned features?
- Crop
### Q1: What are your planned features?
- Progress bar/Tracking
- Full custom logic pipelines to combine multiple operations together.
- Folder support with auto scanning to perform operations on
- Redact sections of pages
- Add page numbers
- Auto rename (Renames file based on file title text)
- URL to PDF
- Change contrast
### Q3: Why is my application downloading .htm files?
### Q2: Why is my application downloading .htm files?
This is a issue caused commonly by your NGINX congifuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. client_max_body_size SIZE; Where "SIZE" is 50M for example for 50MB files.

View file

@ -8,7 +8,7 @@ plugins {
}
group = 'stirling.software'
version = '0.12.3'
version = '0.13.0'
sourceCompatibility = '17'
repositories {
@ -48,7 +48,12 @@ dependencies {
implementation 'org.yaml:snakeyaml:2.1'
implementation 'org.springframework.boot:spring-boot-starter-web:3.1.2'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.2'
implementation 'org.springframework.boot:spring-boot-starter-security:3.1.2'
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.2'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "com.h2database:h2"
// https://mvnrepository.com/artifact/org.apache.pdfbox/jbig2-imageio
implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4'
implementation 'commons-io:commons-io:2.13.0'
@ -65,6 +70,8 @@ dependencies {
implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'
// https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation 'org.commonmark:commonmark:0.21.0'
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
developmentOnly("org.springframework.boot:spring-boot-devtools")

View file

@ -10,11 +10,14 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.utils.GeneralUtils;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication
@EnableWebSecurity()
@EnableGlobalMethodSecurity(prePostEnabled = true)
//@EnableScheduling
public class SPdfApplication {

View file

@ -1,42 +1,63 @@
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean(name = "appName")
public String appName() {
String appName = System.getProperty("APP_HOME_NAME");
if (appName == null)
appName = System.getenv("APP_HOME_NAME");
return (appName != null) ? appName : "Stirling PDF";
}
@Bean(name = "appVersion")
public String appVersion() {
String version = getClass().getPackage().getImplementationVersion();
return (version != null) ? version : "0.0.0";
}
@Bean(name = "homeText")
public String homeText() {
String homeText = System.getProperty("APP_HOME_DESCRIPTION");
if (homeText == null)
homeText = System.getenv("APP_HOME_DESCRIPTION");
return (homeText != null) ? homeText : "null";
}
@Bean(name = "navBarText")
public String navBarText() {
String navBarText = System.getProperty("APP_NAVBAR_NAME");
if (navBarText == null)
navBarText = System.getenv("APP_NAVBAR_NAME");
if (navBarText == null)
navBarText = System.getProperty("APP_HOME_NAME");
if (navBarText == null)
navBarText = System.getenv("APP_HOME_NAME");
return (navBarText != null) ? navBarText : "Stirling PDF";
}
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean(name = "rateLimit")
public boolean rateLimit() {
String appName = System.getProperty("rateLimit");
if (appName == null)
appName = System.getenv("rateLimit");
System.out.println("rateLimit=" + appName);
return (appName != null) ? Boolean.valueOf(appName) : false;
}
@Bean(name = "loginEnabled")
public boolean loginEnabled() {
String appName = System.getProperty("login.enabled");
if (appName == null)
appName = System.getenv("login.enabled");
System.out.println("loginEnabled=" + appName);
return (appName != null) ? Boolean.valueOf(appName) : false;
}
@Bean(name = "appName")
public String appName() {
String appName = System.getProperty("APP_HOME_NAME");
if (appName == null)
appName = System.getenv("APP_HOME_NAME");
return (appName != null) ? appName : "Stirling PDF";
}
@Bean(name = "appVersion")
public String appVersion() {
String version = getClass().getPackage().getImplementationVersion();
return (version != null) ? version : "0.0.0";
}
@Bean(name = "homeText")
public String homeText() {
String homeText = System.getProperty("APP_HOME_DESCRIPTION");
if (homeText == null)
homeText = System.getenv("APP_HOME_DESCRIPTION");
return (homeText != null) ? homeText : "null";
}
@Bean(name = "navBarText")
public String navBarText() {
String navBarText = System.getProperty("APP_NAVBAR_NAME");
if (navBarText == null)
navBarText = System.getenv("APP_NAVBAR_NAME");
if (navBarText == null)
navBarText = System.getProperty("APP_HOME_NAME");
if (navBarText == null)
navBarText = System.getenv("APP_HOME_NAME");
return (navBarText != null) ? navBarText : "Stirling PDF";
}
}

View file

@ -1,5 +1,6 @@
package stirling.software.SPDF.config;
import java.time.Duration;
import java.util.Locale;
import org.springframework.context.annotation.Bean;
@ -10,9 +11,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
@Configuration
public class Beans implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());

View file

@ -1,79 +1,83 @@
package stirling.software.SPDF.config;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI();
Map<String, String> parameters = new HashMap<>();
// Keep only the allowed parameters
String[] queryParameters = queryString.split("&");
for (String param : queryParameters) {
String[] keyValue = param.split("=");
if (keyValue.length != 2) {
continue;
}
if (ALLOWED_PARAMS.contains(keyValue[0])) {
parameters.put(keyValue[0], keyValue[1]);
}
}
// If there are any parameters that are not allowed
if (parameters.size() != queryParameters.length) {
// Construct new query string
StringBuilder newQueryString = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (newQueryString.length() > 0) {
newQueryString.append("&");
}
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
}
// Redirect to the URL with only allowed query parameters
String redirectUrl = requestURI + "?" + newQueryString;
response.sendRedirect(redirectUrl);
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
}
}
package stirling.software.SPDF.config;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI();
Map<String, String> parameters = new HashMap<>();
// Keep only the allowed parameters
String[] queryParameters = queryString.split("&");
for (String param : queryParameters) {
String[] keyValue = param.split("=");
System.out.print("astirli " + keyValue[0]);
if (keyValue.length != 2) {
continue;
}
System.out.print("astirli2 " + keyValue[0]);
if (ALLOWED_PARAMS.contains(keyValue[0])) {
parameters.put(keyValue[0], keyValue[1]);
}
}
// If there are any parameters that are not allowed
if (parameters.size() != queryParameters.length) {
// Construct new query string
StringBuilder newQueryString = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (newQueryString.length() > 0) {
newQueryString.append("&");
}
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
}
// Redirect to the URL with only allowed query parameters
String redirectUrl = requestURI + "?" + newQueryString;
response.sendRedirect(redirectUrl);
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
}
}

View file

@ -1,26 +1,26 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class EndpointInterceptor implements HandlerInterceptor {
@Autowired
private EndpointConfiguration endpointConfiguration;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false;
}
return true;
}
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class EndpointInterceptor implements HandlerInterceptor {
@Autowired
private EndpointConfiguration endpointConfiguration;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false;
}
return true;
}
}

View file

@ -1,24 +1,24 @@
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
@Configuration
public class MetricsConfig {
@Bean
public MeterFilter meterFilter() {
return new MeterFilter() {
@Override
public MeterFilterReply accept(Meter.Id id) {
if (id.getName().equals("http.requests")) {
return MeterFilterReply.NEUTRAL;
}
return MeterFilterReply.DENY;
}
};
}
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
@Configuration
public class MetricsConfig {
@Bean
public MeterFilter meterFilter() {
return new MeterFilter() {
@Override
public MeterFilterReply accept(Meter.Id id) {
if (id.getName().equals("http.requests")) {
return MeterFilterReply.NEUTRAL;
}
return MeterFilterReply.DENY;
}
};
}
}

View file

@ -1,48 +1,48 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry;
@Autowired
public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = request.getRequestURI();
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
Counter counter = Counter.builder("http.requests")
.tag("uri", uri)
.tag("method", request.getMethod())
.register(meterRegistry);
counter.increment();
//System.out.println("Counted");
}
filterChain.doFilter(request, response);
}
}
package stirling.software.SPDF.config;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry;
@Autowired
public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = request.getRequestURI();
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
Counter counter = Counter.builder("http.requests")
.tag("uri", uri)
.tag("method", request.getMethod())
.register(meterRegistry);
counter.increment();
//System.out.println("Counted");
}
filterChain.doFilter(request, response);
}
}

View file

@ -1,36 +1,36 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
Properties props = new Properties();
try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) {
props.load(input);
version = props.getProperty("version");
} catch (IOException ex) {
ex.printStackTrace();
version = "1.0.0"; // default version if all else fails
}
}
return new OpenAPI().components(new Components()).info(
new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
}
}
package stirling.software.SPDF.config;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
Properties props = new Properties();
try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) {
props.load(input);
version = props.getProperty("version");
} catch (IOException ex) {
ex.printStackTrace();
version = "1.0.0"; // default version if all else fails
}
}
return new OpenAPI().components(new Components()).info(
new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
}
}

View file

@ -1,20 +1,20 @@
package stirling.software.SPDF.config;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
public static LocalDateTime startTime;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
startTime = LocalDateTime.now();
}
}
package stirling.software.SPDF.config;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
public static LocalDateTime startTime;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
startTime = LocalDateTime.now();
}
}

View file

@ -0,0 +1,125 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.Role;
@Component
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
@Autowired
private UserDetailsService userDetailsService;
@Autowired
@Qualifier("rateLimit")
public boolean rateLimit;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!rateLimit) {
// If rateLimit is not enabled, just pass all requests without rate limiting
filterChain.doFilter(request, response);
return;
}
String method = request.getMethod();
if (!"POST".equalsIgnoreCase(method)) {
// If the request is not a POST, just pass it through without rate limiting
filterChain.doFilter(request, response);
return;
}
String identifier = null;
// Check for API key in the request headers
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
} else {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
identifier = userDetails.getUsername();
}
}
// If neither API key nor an authenticated user is present, use IP address
if (identifier == null) {
identifier = request.getRemoteAddr();
}
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-Key") != null) {
// It's an API call
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain);
} else {
// It's a Web UI call
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain);
}
}
private Role getRoleFromAuthentication(Authentication authentication) {
if (authentication != null && authentication.isAuthenticated()) {
for (GrantedAuthority authority : authentication.getAuthorities()) {
try {
return Role.fromString(authority.getAuthority());
} catch (IllegalArgumentException ex) {
// Ignore and continue to next authority.
}
}
}
throw new IllegalStateException("User does not have a valid role.");
}
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets,
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
filterChain.doFilter(request, response);
} else {
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
response.getWriter().write("Rate limit exceeded for POST requests.");
}
}
private Bucket createUserBucket(int limitPerDay) {
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
return Bucket.builder().addLimit(limit).build();
}
}

View file

@ -1,27 +1,27 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private EndpointInterceptor endpointInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(endpointInterceptor);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Handler for external static resources
registry.addResourceHandler("/**")
.addResourceLocations("file:customFiles/static/", "classpath:/static/")
.setCachePeriod(0); // Optional: disable caching
}
}
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private EndpointInterceptor endpointInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(endpointInterceptor);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Handler for external static resources
registry.addResourceHandler("/**")
.addResourceLocations("file:customFiles/static/", "classpath:/static/");
//.setCachePeriod(0); // Optional: disable caching
}
}

View file

@ -0,0 +1,26 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
setDefaultFailureUrl("/login?error=badcredentials");
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
setDefaultFailureUrl("/login?error=locked");
}
super.onAuthenticationFailure(request, response, exception);
}
}

View file

@ -0,0 +1,46 @@
package stirling.software.SPDF.config.security;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true, true, true,
getAuthorities(user.getAuthorities())
);
}
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
return authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
}

View file

@ -0,0 +1,26 @@
package stirling.software.SPDF.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.Role;
@Component
public class InitialSetup {
@Autowired
private UserService userService;
@PostConstruct
public void init() {
if(!userService.hasUsers()) {
String initialUsername = System.getenv("INITIAL_USERNAME");
String initialPassword = System.getenv("INITIAL_PASSWORD");
if(initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
}
}
}
}

View file

@ -0,0 +1,90 @@
package stirling.software.SPDF.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfiguration {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
@Lazy
private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
private UserAuthenticationFilter userAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if(loginEnabledValue) {
http.csrf().disable();
http
.formLogin(formLogin -> formLogin
.loginPage("/login")
.defaultSuccessUrl("/")
.failureHandler(new CustomAuthenticationFailureHandler())
.permitAll()
)
.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID")
)
.authorizeHttpRequests(authz -> authz
.requestMatchers(req -> req.getRequestURI().startsWith("/login") || req.getRequestURI().endsWith(".svg") || req.getRequestURI().startsWith("/register") || req.getRequestURI().startsWith("/error") || req.getRequestURI().startsWith("/images/") || req.getRequestURI().startsWith("/public/") || req.getRequestURI().startsWith("/css/") || req.getRequestURI().startsWith("/js/"))
.permitAll()
.anyRequest().authenticated()
)
.userDetailsService(userDetailsService)
.authenticationProvider(authenticationProvider());
} else {
http
.csrf().disable()
.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll()
);
}
return http.build();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}

View file

@ -0,0 +1,120 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
@Component
public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
@Lazy
private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!loginEnabledValue) {
// If login is not enabled, just pass all requests without authentication
filterChain.doFilter(request, response);
return;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for API key in the request headers if no authentication exists
if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) {
try {
// Use API key to authenticate. This requires you to have an authentication provider for API keys.
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
if(userDetails == null)
{
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key.");
return;
}
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) {
// If API key authentication fails, deny the request
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key.");
return;
}
}
}
// If we still don't have any authentication, deny the request
if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod();
if ("GET".equalsIgnoreCase(method)) {
response.sendRedirect("/login"); // redirect to the login page
return;
}
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
return;
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String uri = request.getRequestURI();
String[] permitAllPatterns = {
"/login",
"/register",
"/error",
"/images/",
"/public/",
"/css/",
"/js/"
};
for (String pattern : permitAllPatterns) {
if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,178 @@
package stirling.software.SPDF.config.security;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.Collection;
import java.util.HashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.repository.UserRepository;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey);
if (user == null) {
throw new UsernameNotFoundException("API key is not valid");
}
// Convert the user into an Authentication object
return new UsernamePasswordAuthenticationToken(
user, // principal (typically the user)
null, // credentials (we don't expose the password or API key here)
getAuthorities(user) // user's authorities (roles/permissions)
);
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
// Convert each Authority object into a SimpleGrantedAuthority object.
return user.getAuthorities().stream()
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
private String generateApiKey() {
String apiKey;
do {
apiKey = UUID.randomUUID().toString();
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
return apiKey;
}
public User addApiKeyToUser(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
user.setApiKey(generateApiKey());
return userRepository.save(user);
}
public User refreshApiKeyForUser(String username) {
return addApiKeyToUser(username); // reuse the add API key method for refreshing
}
public String getApiKeyForUser(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey();
}
public boolean isValidApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey) != null;
}
public User getUserByApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey);
}
public UserDetails loadUserByApiKey(String apiKey) {
User userOptional = userRepository.findByApiKey(apiKey);
if (userOptional != null) {
User user = userOptional;
// Convert your User entity to a UserDetails object with authorities
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(), // you might not need this for API key auth
getAuthorities(user)
);
}
return null; // or throw an exception
}
public boolean validateApiKeyForUser(String username, String apiKey) {
Optional<User> userOpt = userRepository.findByUsername(username);
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
}
public void saveUser(String username, String password) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true);
userRepository.save(user);
}
public void saveUser(String username, String password, String role) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user));
user.setEnabled(true);
userRepository.save(user);
}
public void deleteUser(String username) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) {
userRepository.delete(userOpt.get());
}
}
public boolean usernameExists(String username) {
return userRepository.findByUsername(username).isPresent();
}
public boolean hasUsers() {
return userRepository.count() > 0;
}
public void updateUserSettings(String username, Map<String, String> updates) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings();
if(settingsMap == null) {
settingsMap = new HashMap<String,String>();
}
settingsMap.clear();
settingsMap.putAll(updates);
user.setSettings(settingsMap);
userRepository.save(user);
}
}
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
public void changeUsername(User user, String newUsername) {
user.setUsername(newUsername);
userRepository.save(user);
}
public void changePassword(User user, String newPassword) {
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
public boolean isPasswordCorrect(User user, String currentPassword) {
return passwordEncoder.matches(currentPassword, user.getPassword());
}
}

View file

@ -1,7 +1,13 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
@ -11,10 +17,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -26,55 +33,93 @@ public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
// Create a new empty document
PDDocument mergedDoc = new PDDocument();
// Iterate over the list of documents and add their pages to the merged document
for (PDDocument doc : documents) {
// Get all pages from the current document
PDPageTree pages = doc.getPages();
// Iterate over the pages and add them to the merged document
for (PDPage page : pages) {
mergedDoc.addPage(page);
}
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
PDDocument mergedDoc = new PDDocument();
for (PDDocument doc : documents) {
for (PDPage page : doc.getPages()) {
mergedDoc.addPage(page);
}
}
return mergedDoc;
}
// Return the merged document
return mergedDoc;
private Comparator<MultipartFile> getSortComparator(String sortType) {
switch (sortType) {
case "byFileName":
return Comparator.comparing(MultipartFile::getOriginalFilename);
case "byDateModified":
return (file1, file2) -> {
try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime());
} catch (IOException e) {
return 0; // If there's an error, treat them as equal
}
};
case "byDateCreated":
return (file1, file2) -> {
try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
return attr1.creationTime().compareTo(attr2.creationTime());
} catch (IOException e) {
return 0; // If there's an error, treat them as equal
}
};
case "byPDFTitle":
return (file1, file2) -> {
try (PDDocument doc1 = PDDocument.load(file1.getInputStream());
PDDocument doc2 = PDDocument.load(file2.getInputStream())) {
String title1 = doc1.getDocumentInformation().getTitle();
String title2 = doc2.getDocumentInformation().getTitle();
return title1.compareTo(title2);
} catch (IOException e) {
return 0;
}
};
case "orderProvided":
default:
return (file1, file2) -> 0; // Default is the order provided
}
}
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation(summary = "Merge multiple PDF files into one",
description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> mergePdfs(
@RequestPart(required = true, value = "fileInput") MultipartFile[] files,
@RequestParam(value = "sortType", defaultValue = "orderProvided")
@Parameter(schema = @Schema(description = "The type of sorting to be applied on the input files before merging.",
allowableValues = {
"orderProvided",
"byFileName",
"byDateModified",
"byDateCreated",
"byPDFTitle"
}))
String sortType) throws IOException {
Arrays.sort(files, getSortComparator(sortType));
List<PDDocument> documents = new ArrayList<>();
for (MultipartFile file : files) {
try (InputStream is = file.getInputStream()) {
documents.add(PDDocument.load(is));
}
}
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation(
summary = "Merge multiple PDF files into one",
description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO"
)
public ResponseEntity<byte[]> mergePdfs(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF files to be merged into a single file", required = true)
MultipartFile[] files) throws IOException {
// Read the input PDF files into PDDocument objects
List<PDDocument> documents = new ArrayList<>();
// Loop through the files array and read each file into a PDDocument
for (MultipartFile file : files) {
documents.add(PDDocument.load(file.getInputStream()));
}
PDDocument mergedDoc = mergeDocuments(documents);
// Return the merged PDF as a response
try (PDDocument mergedDoc = mergeDocuments(documents)) {
ResponseEntity<byte[]> response = WebResponseUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
for (PDDocument doc : documents) {
// Close the document after processing
doc.close();
}
return response;
} finally {
for (PDDocument doc : documents) {
if (doc != null) {
doc.close();
}
}
}
}
}

View file

@ -0,0 +1,163 @@
package stirling.software.SPDF.controller.api;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
@Controller
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/register")
public String register(@RequestParam String username, @RequestParam String password, Model model) {
if(userService.usernameExists(username)) {
model.addAttribute("error", "Username already exists");
return "register";
}
userService.saveUser(username, password);
return "redirect:/login?registered=true";
}
@PostMapping("/change-username")
public ResponseEntity<String> changeUsername(Principal principal, @RequestParam String currentPassword, @RequestParam String newUsername, HttpServletRequest request, HttpServletResponse response) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
Optional<User> userOpt = userService.findByUsername(principal.getName());
if(userOpt == null || userOpt.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found.");
}
User user = userOpt.get();
if(!userService.isPasswordCorrect(user, currentPassword)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect.");
}
if(userService.usernameExists(newUsername)) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("New username already exists.");
}
userService.changeUsername(user, newUsername);
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return ResponseEntity.ok("Username updated successfully.");
}
@PostMapping("/change-password")
public ResponseEntity<String> changePassword(Principal principal, @RequestParam String currentPassword, @RequestParam String newPassword, HttpServletRequest request, HttpServletResponse response) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
Optional<User> userOpt = userService.findByUsername(principal.getName());
if(userOpt == null || userOpt.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found.");
}
User user = userOpt.get();
if(!userService.isPasswordCorrect(user, currentPassword)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect.");
}
userService.changePassword(user, passwordEncoder.encode(newPassword));
// Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null);
return ResponseEntity.ok("Password updated successfully.");
}
@PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) {
Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>();
System.out.println("Received parameter map: " + paramMap);
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]);
}
System.out.println("Processed updates: " + updates);
// Assuming you have a method in userService to update the settings for a user
userService.updateUserSettings(principal.getName(), updates);
return "redirect:/account"; // Redirect to a page of your choice after updating
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser")
public String saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role) {
userService.saveUser(username, password, role);
return "redirect:/addUsers"; // Redirect to account page after adding the user
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/admin/deleteUser/{username}")
public String deleteUser(@PathVariable String username) {
userService.deleteUser(username);
return "redirect:/addUsers";
}
@PostMapping("/get-api-key")
public ResponseEntity<String> getApiKey(Principal principal) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
String username = principal.getName();
String apiKey = userService.getApiKeyForUser(username);
if (apiKey == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
}
return ResponseEntity.ok(apiKey);
}
@PostMapping("/update-api-key")
public ResponseEntity<String> updateApiKey(Principal principal) {
if (principal == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
}
String username = principal.getName();
User user = userService.refreshApiKeyForUser(username);
String apiKey = user.getApiKey();
if (apiKey == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
}
return ResponseEntity.ok(apiKey);
}
}

View file

@ -52,13 +52,13 @@ public class PasswordController {
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to which the password should be added", required = true)
MultipartFile fileInput,
@RequestParam(value = "", name = "ownerPassword")
@RequestParam(value = "", name = "ownerPassword", required = false, defaultValue = "")
@Parameter(description = "The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)")
String ownerPassword,
@RequestParam( name = "password", required = false)
@RequestParam( name = "password", required = false, defaultValue = "")
@Parameter(description = "The password to be added to the PDF file (Restricts the opening of the document itself.)")
String password,
@RequestParam( name = "keyLength", required = false)
@RequestParam( name = "keyLength", required = false, defaultValue = "256")
@Parameter(description = "The length of the encryption key", schema = @Schema(allowableValues = {"40", "128", "256"}))
int keyLength,
@RequestParam( name = "canAssembleDocument", required = false)
@ -98,15 +98,15 @@ public class PasswordController {
ap.setCanPrint(!canPrint);
ap.setCanPrintFaithful(!canPrintFaithful);
StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap);
spp.setEncryptionKeyLength(keyLength);
if(!"".equals(ownerPassword) || !"".equals(password)) {
spp.setEncryptionKeyLength(keyLength);
}
spp.setPermissions(ap);
document.protect(spp);
if("".equals(ownerPassword) && "".equals(password))
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf");
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
}

View file

@ -4,11 +4,13 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -16,18 +18,108 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Controller
@Tag(name = "General", description = "General APIs")
public class GeneralWebController {
@GetMapping("/login")
public String login(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication != null && authentication.isAuthenticated()) {
return "redirect:/";
}
if (request.getParameter("error") != null) {
model.addAttribute("error", request.getParameter("error"));
}
if (request.getParameter("logout") != null) {
model.addAttribute("logoutMessage", "You have been logged out.");
}
return "login";
}
@Autowired
private UserRepository userRepository; // Assuming you have a repository for user operations
@Autowired
private UserService userService; // Assuming you have a repository for user operations
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/addUsers")
public String showAddUserForm(Model model) {
List<User> allUsers = userRepository.findAll();
model.addAttribute("users", allUsers);
return "addUsers";
}
@GetMapping("/account")
public String account(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/";
}
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
// Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes
String username = userDetails.getUsername();
// Fetch user details from the database
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
if (!user.isPresent()) {
// Handle error appropriately
return "redirect:/error"; // Example redirection in case of error
}
// Convert settings map to JSON string
ObjectMapper objectMapper = new ObjectMapper();
String settingsJson;
try {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
} catch (JsonProcessingException e) {
// Handle JSON conversion error
e.printStackTrace();
return "redirect:/error"; // Example redirection in case of error
}
// Add attributes to the model
model.addAttribute("username", username);
model.addAttribute("role", user.get().getRolesAsString());
model.addAttribute("settings", settingsJson);
}
} else {
return "redirect:/";
}
return "account";
}
@GetMapping("/pipeline")
@Hidden
public String pipelineForm(Model model) {

View file

@ -0,0 +1,49 @@
package stirling.software.SPDF.model;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public ApiKeyAuthenticationToken(String apiKey) {
super(null);
this.principal = null;
this.credentials = apiKey;
setAuthenticated(false);
}
public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal; // principal can be a UserDetails object
this.credentials = apiKey;
super.setAuthenticated(true); // this authentication is trusted
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead.");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}

View file

@ -0,0 +1,64 @@
package stirling.software.SPDF.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "authorities")
public class Authority {
public Authority() {
}
public Authority(String authority, User user) {
this.authority = authority;
this.user = user;
user.getAuthorities().add(this);
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "authority")
private String authority;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAuthority() {
return authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}

View file

@ -0,0 +1,50 @@
package stirling.software.SPDF.model;
public enum Role {
// Unlimited access
ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE),
// Unlimited access
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE),
// 40 API calls Per Day, 40 web calls
LIMITED_API_USER("ROLE_LIMITED_API_USER", 40, 40),
// 20 API calls Per Day, 20 web calls
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20),
// 0 API calls per day and 20 web calls
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20);
private final String roleId;
private final int apiCallsPerDay;
private final int webCallsPerDay;
Role(String roleId, int apiCallsPerDay, int webCallsPerDay) {
this.roleId = roleId;
this.apiCallsPerDay = apiCallsPerDay;
this.webCallsPerDay = webCallsPerDay;
}
public String getRoleId() {
return roleId;
}
public int getApiCallsPerDay() {
return apiCallsPerDay;
}
public int getWebCallsPerDay() {
return webCallsPerDay;
}
public static Role fromString(String roleId) {
for (Role role : Role.values()) {
if (role.getRoleId().equalsIgnoreCase(roleId)) {
return role;
}
}
throw new IllegalArgumentException("No Role defined for id: " + roleId);
}
}

View file

@ -0,0 +1,124 @@
package stirling.software.SPDF.model;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.persistence.CascadeType;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MapKeyColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.JoinColumn;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(name = "username", unique = true)
private String username;
@Column(name = "password")
private String password;
@Column(name = "apiKey")
private String apiKey;
@Column(name = "enabled")
private boolean enabled;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Authority> authorities = new HashSet<>();
@ElementCollection
@MapKeyColumn(name = "setting_key")
@Column(name = "setting_value")
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public Map<String, String> getSettings() {
return settings;
}
public void setSettings(Map<String, String> settings) {
this.settings = settings;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Set<Authority> getAuthorities() {
return authorities;
}
public void setAuthorities(Set<Authority> authorities) {
this.authorities = authorities;
}
public void addAuthorities(Set<Authority> authorities) {
this.authorities.addAll(authorities);
}
public void addAuthority(Authority authorities) {
this.authorities.add(authorities);
}
public String getRolesAsString() {
return this.authorities.stream()
.map(Authority::getAuthority)
.collect(Collectors.joining(", "));
}
}

View file

@ -0,0 +1,12 @@
package stirling.software.SPDF.repository;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
import stirling.software.SPDF.model.Authority;
public interface AuthorityRepository extends JpaRepository<Authority, Long> {
//Set<Authority> findByUsername(String username);
Set<Authority> findByUser_Username(String username);
}

View file

@ -0,0 +1,13 @@
package stirling.software.SPDF.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import stirling.software.SPDF.model.User;
public interface UserRepository extends JpaRepository<User, String> {
Optional<User> findByUsername(String username);
User findByApiKey(String apiKey);
}

View file

@ -17,7 +17,10 @@ server.error.include-exception=true
server.error.include-message=always
#logging.level.org.springframework.web=DEBUG
#logging.level.org.springframework=DEBUG
#logging.level.org.springframework.security=DEBUG
#login.enabled=true
server.servlet.session.tracking-modes=cookie
server.servlet.context-path=${APP_ROOT_PATH:/}
@ -32,4 +35,12 @@ spring.mvc.async.request-timeout=${ASYNC_CONNECTION_TIMEOUT:300000}
spring.resources.static-locations=file:customFiles/static/
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
#spring.thymeleaf.cache=false
#spring.thymeleaf.cache=false
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=\u0641\u062A\u062D \u0641\u064A \u0646\u0641\u0633 \u0
settings.downloadOption.2=\u0641\u062A\u062D \u0641\u064A \u0646\u0627\u0641\u0630\u0629 \u062C\u062F\u064A\u062F\u0629
settings.downloadOption.3=\u062A\u0646\u0632\u064A\u0644 \u0627\u0644\u0645\u0644\u0641
settings.zipThreshold=\u0645\u0644\u0641\u0627\u062A \u0645\u0636\u063A\u0648\u0637\u0629 \u0639\u0646\u062F \u062A\u062C\u0627\u0648\u0632 \u0639\u062F\u062F \u0627\u0644\u0645\u0644\u0641\u0627\u062A \u0627\u0644\u062A\u064A \u062A\u0645 \u062A\u0646\u0632\u064A\u0644\u0647\u0627
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=إضافة صورة
#merge
merge.title=دمج
merge.header=دمج ملفات PDF متعددة (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=دمج
@ -626,17 +676,14 @@ watermark.selectText.4=دوران (0-360):
watermark.selectText.5=widthSpacer (مسافة بين كل علامة مائية أفقيًا):
watermark.selectText.6=heightSpacer (مسافة بين كل علامة مائية عموديًا):
watermark.selectText.7=\u0627\u0644\u062A\u0639\u062A\u064A\u0645 (0\u066A - 100\u066A):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=إضافة علامة مائية
#remove-watermark
remove-watermark.title=\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629
remove-watermark.header=\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629
remove-watermark.selectText.1=\u062D\u062F\u062F PDF \u0644\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629 \u0645\u0646:
remove-watermark.selectText.2=\u0646\u0635 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629:
remove-watermark.submit=\u0625\u0632\u0627\u0644\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0629 \u0627\u0644\u0645\u0627\u0626\u064A\u0629
#Change permissions
permissions.title=تغيير الأذونات
permissions.header=تغيير الأذونات

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Obre mateixa finestra
settings.downloadOption.2=Obre mateixa finestra
settings.downloadOption.3=Descarrega Arxiu
settings.zipThreshold=Comprimiu els fitxers quan el nombre de fitxers baixats superi
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=Afegir Imatge
#merge
merge.title=Fusiona
merge.header=Fusiona múltiples PDFs (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Fusiona
@ -626,17 +676,14 @@ watermark.selectText.4=Rotació (0-360):
watermark.selectText.5=separació d'amplada (Espai horitzontal entre cada Marca d'Aigua):
watermark.selectText.6=separació d'alçada (Espai vertical entre cada Marca d'Aigua):
watermark.selectText.7=Opacitat (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Afegir Marca d'Aigua
#remove-watermark
remove-watermark.title=Elimina Marca d'Aigua
remove-watermark.header=Elimina Marca d'Aigua
remove-watermark.selectText.1=Seleciona PDF per eliminar Marca d'Aigua:
remove-watermark.selectText.2=Text de la Marca d'Aigua:
remove-watermark.submit=Elimina Marca d'Aigua
#Change permissions
permissions.title=Canviar Permissos
permissions.header=Canviar Permissos

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Im selben Fenster öffnen
settings.downloadOption.2=In neuem Fenster öffnen
settings.downloadOption.3=Datei herunterladen
settings.zipThreshold=Dateien komprimieren, wenn die Anzahl der heruntergeladenen Dateien überschritten wird
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=Bild hinzufügen
#merge
merge.title=Zusammenführen
merge.header=Mehrere PDFs zusammenführen (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Zusammenführen
@ -626,17 +676,14 @@ watermark.selectText.4=Drehung (0-360):
watermark.selectText.5=breiteSpacer (horizontaler Abstand zwischen den einzelnen Wasserzeichen):
watermark.selectText.6=höheSpacer (vertikaler Abstand zwischen den einzelnen Wasserzeichen):
watermark.selectText.7=Deckkraft (0% - 100 %):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Wasserzeichen hinzufügen
#remove-watermark
remove-watermark.title=Wasserzeichen entfernen
remove-watermark.header=Wasserzeichen entfernen
remove-watermark.selectText.1=PDF auswählen, um Wasserzeichen zu entfernen von:
remove-watermark.selectText.2=Wasserzeichentext:
remove-watermark.submit=Wasserzeichen entfernen
#Change permissions
permissions.title=Berechtigungen ändern
permissions.header=Berechtigungen ändern

View file

@ -31,7 +31,11 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +58,41 @@ settings.downloadOption.1=Open in same window
settings.downloadOption.2=Open in new window
settings.downloadOption.3=Download file
settings.zipThreshold=Zip files when the number of downloaded files exceeds
settings.signOut=Sign Out
settings.accountSettings=Account Settings
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -520,6 +559,8 @@ addImage.submit=Add image
#merge
merge.title=Merge
merge.header=Merge multiple PDFs (2+)
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Merge
@ -578,9 +619,9 @@ imageToPDF.selectText.5=Convert to separate PDFs
pdfToImage.title=PDF to Image
pdfToImage.header=PDF to Image
pdfToImage.selectText=Image Format
pdfToImage.singleOrMultiple=Image result type
pdfToImage.single=Single Big Image
pdfToImage.multi=Multiple Images
pdfToImage.singleOrMultiple=Page to Image result type
pdfToImage.single=Single Big Image Combing all pages
pdfToImage.multi=Multiple Images, one image per page
pdfToImage.colorType=Colour type
pdfToImage.color=Colour
pdfToImage.grey=Greyscale
@ -620,17 +661,11 @@ watermark.selectText.4=Rotation (0-360):
watermark.selectText.5=widthSpacer (Space between each watermark horizontally):
watermark.selectText.6=heightSpacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%):
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Add Watermark
#remove-watermark
remove-watermark.title=Remove Watermark
remove-watermark.header=Remove Watermark
remove-watermark.selectText.1=Select PDF to remove watermark from:
remove-watermark.selectText.2=Watermark Text:
remove-watermark.submit=Remove Watermark
#Change permissions
permissions.title=Change Permissions
permissions.header=Change Permissions

View file

@ -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=Select PDF(s)
@ -25,13 +25,20 @@ downloadPdf=Download PDF
text=Text
font=Font
selectFillter=-- Select --
pageNum=Page Number
pageNum=Page Number
sizes.small=Small
sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Open in same window
settings.downloadOption.2=Open in new window
settings.downloadOption.3=Download file
settings.zipThreshold=Zip files when the number of downloaded files exceeds
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -71,7 +122,7 @@ merge.tags=merge,Page operations,Back end,server side
home.split.title=Split
home.split.desc=Split PDFs into multiple documents
split.tags=Page operations,divide,Multi Page,cut,server side
split.tags=Page operations,divide,Multi Page,cut,server side
home.rotate.title=Rotate
home.rotate.desc=Easily rotate your PDFs.
@ -208,7 +259,7 @@ home.add-page-numbers.desc=Add Page numbers throughout a document in a set locat
add-page-numbers.tags=paginate,label,organize,index
home.auto-rename.title=Auto Rename PDF File
home.auto-rename.desc=Auto renames a PDF file based on its detected header
home.auto-rename.desc=Auto renames a PDF file based on its detected header
auto-rename.tags=auto-detect,header-based,organize,relabel
home.adjust-contrast.title=Adjust Colors/Contrast
@ -396,16 +447,16 @@ scalePages.submit=Submit
#certSign
certSign.title=Certificate Signing
certSign.header=Sign a PDF with your certificate (Work in progress)
certSign.selectPDF=Select a PDF File for Signing:
certSign.selectKey=Select Your Private Key File (PKCS#8 format, could be .pem or .der):
certSign.selectCert=Select Your Certificate File (X.509 format, could be .pem or .der):
certSign.selectP12=Select Your PKCS#12 Keystore File (.p12 or .pfx) (Optional, If provided, it should contain your private key and certificate):
certSign.selectPDF=Select a PDF File for Signing:
certSign.selectKey=Select Your Private Key File (PKCS#8 format, could be .pem or .der):
certSign.selectCert=Select Your Certificate File (X.509 format, could be .pem or .der):
certSign.selectP12=Select Your PKCS#12 Keystore File (.p12 or .pfx) (Optional, If provided, it should contain your private key and certificate):
certSign.certType=Certificate Type
certSign.password=Enter Your Keystore or Private Key Password (If Any):
certSign.password=Enter Your Keystore or Private Key Password (If Any):
certSign.showSig=Show Signature
certSign.reason=Reason
certSign.location=Location
certSign.name=Name
certSign.name=Name
certSign.submit=Sign PDF
@ -505,7 +556,7 @@ compress.selectText.1=Manual Mode - From 1 to 4
compress.selectText.2=Optimization level:
compress.selectText.3=4 (Terrible for text images)
compress.selectText.4=Auto mode - Auto adjusts quality to get PDF to exact size
compress.selectText.5=Expected PDF Size (e.g. 25MB, 10.8MB, 25KB)
compress.selectText.5=Expected PDF Size (e.g. 25MB, 10.8MB, 25KB)
compress.submit=Compress
@ -520,6 +571,11 @@ addImage.submit=Add image
#merge
merge.title=Merge
merge.header=Merge multiple PDFs (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Merge
@ -620,17 +676,14 @@ watermark.selectText.4=Rotation (0-360):
watermark.selectText.5=widthSpacer (Space between each watermark horizontally):
watermark.selectText.6=heightSpacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Add Watermark
#remove-watermark
remove-watermark.title=Remove Watermark
remove-watermark.header=Remove Watermark
remove-watermark.selectText.1=Select PDF to remove watermark from:
remove-watermark.selectText.2=Watermark Text:
remove-watermark.submit=Remove Watermark
#Change permissions
permissions.title=Change Permissions
permissions.header=Change Permissions
@ -657,7 +710,7 @@ removePassword.submit=Remove
#changeMetadata
changeMetadata.title=Change Metadata
changeMetadata.title=Title:
changeMetadata.header=Change Metadata
changeMetadata.selectText.1=Please edit the variables you wish to change
changeMetadata.selectText.2=Delete all metadata
@ -725,4 +778,4 @@ PDFToHTML.submit=Convert
PDFToXML.title=PDF to XML
PDFToXML.header=PDF to XML
PDFToXML.credit=This service uses LibreOffice for file conversion.
PDFToXML.submit=Convert
PDFToXML.submit=Convert

View file

@ -31,7 +31,14 @@ sizes.medium=Mediano
sizes.large=Grande
sizes.x-large=Extra grande
error.pdfPassword=El documento PDF está protegido con contraseña y no se ha proporcionado o es incorrecta
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Abrir en la misma ventana
settings.downloadOption.2=Abrir en una nueva ventana
settings.downloadOption.3=Descargar el fichero
settings.zipThreshold=Ficheros ZIP cuando excede el número de ficheros descargados
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=Añadir imagen
#merge
merge.title=Unir
merge.header=Unir múltiples PDFs (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Unir
@ -626,17 +676,14 @@ watermark.selectText.4=Rotación (0-360):
watermark.selectText.5=Ancho (Espacio entre cada marca de agua horizontalmente):
watermark.selectText.6=Alto (Espacio entre cada marca de agua verticalmente):
watermark.selectText.7=Opacidad (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Añadir marca de agua
#remove-watermark
remove-watermark.title=Eliminar marca de agua
remove-watermark.header=Eliminar marca de agua
remove-watermark.selectText.1=Seleccionar PDF para eliminar la marca de agua:
remove-watermark.selectText.2=Texto de la marca de agua:
remove-watermark.submit=Eliminar marca de agua
#Change permissions
permissions.title=Cambiar permisos
permissions.header=Cambiar permisos

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=PDF dokumentua pasahitzarekin babestuta dago eta pasahitza ez da sartu edo akastuna da
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Ireki leiho berean
settings.downloadOption.2=Ireki leiho berrian
settings.downloadOption.3=Deskargatu fitxategia
settings.zipThreshold=ZIP fitxategiak deskargatutako fitxategi kopurua gainditzen denean
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=Gehitu irudia
#merge
merge.title=Elkartu
merge.header=Elkartu zenbait PDF (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Elkartu
@ -626,17 +676,14 @@ watermark.selectText.4=Errotazioa (0-360):
watermark.selectText.5=Zabalera (ur-marka bakoitzaren arteko espazioa horizontalean):
watermark.selectText.6=Altuera (ur-marka bakoitzaren arteko espazioa bertikalean):
watermark.selectText.7=Opakutasuna (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Gehitu ur-marka
#remove-watermark
remove-watermark.title=Ezabatu ur-marka
remove-watermark.header=Ezabatu ur-marka
remove-watermark.selectText.1=Hautatu PDFa ur-marka ezabatzeko:
remove-watermark.selectText.2=Ur-markaren testua:
remove-watermark.submit=Ezabatu ur-marka
#Change permissions
permissions.title=Aldatu baimenak
permissions.header=Aldatu baimenak

View file

@ -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=Sélectionnez le(s) PDF
@ -31,7 +31,14 @@ sizes.medium=Moyen
sizes.large=Grand
sizes.x-large=Très grand
error.pdfPassword=Le document PDF est protégé par un mot de passe et le mot de passe n\u2019a pas été fourni ou était incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Ouvrir dans la même fenêtre
settings.downloadOption.2=Ouvrir dans une nouvelle fenêtre
settings.downloadOption.3=Télécharger le fichier
settings.zipThreshold=Compresser les fichiers en ZIP lorsque le nombre de fichiers téléchargés dépasse
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -278,6 +329,15 @@ pdfToSinglePage.header=Fusionner des pages
pdfToSinglePage.submit=Convertir en une seule page
#pageExtracter
##########################
### TODO: Translate ###
##########################
pageExtracter.title=Extract Pages
pageExtracter.header=Extract Pages
pageExtracter.submit=Extract
#getPdfInfo
getPdfInfo.title=Récupérer les informations
getPdfInfo.header=Récupérer les informations
@ -293,6 +353,7 @@ MarkdownToPDF.help=(Travail en cours).
MarkdownToPDF.credit=Utilise WeasyPrint.
#url-to-pdf
URLToPDF.title=URL en PDF
URLToPDF.header=URL en PDF
@ -454,7 +515,7 @@ ScannerImageSplit.selectText.8=Définit la surface de contour minimale pour une
ScannerImageSplit.selectText.9=Taille de la bordure
ScannerImageSplit.selectText.10=Définit la taille de la bordure ajoutée et supprimée pour éviter les bordures blanches dans la sortie (par défaut\u00a0: 1).
#OCR
ocr.title=OCR / Nettoyage des numérisations
ocr.header=OCR (Reconnaissance optique de caractères) / Nettoyage des numérisations
@ -513,6 +574,11 @@ addImage.submit=Ajouter une image
#merge
merge.title=Fusionner
merge.header=Fusionner plusieurs PDF
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Fusionner
@ -527,6 +593,16 @@ multiTool.title=Outil multifonction PDF
multiTool.header=Outil multifonction PDF
#pageRemover
##########################
### TODO: Translate ###
##########################
pageRemover.title=Page Remover
pageRemover.header=PDF Page remover
pageRemover.pagesToDelete=Pages to delete (Enter a comma-separated list of page numbers) :
pageRemover.submit=Delete Pages
#rotate
rotate.title=Pivoter
rotate.header=Pivoter
@ -534,7 +610,7 @@ rotate.selectAngle=Angle de rotation (par multiples de 90\u202fdegrés)
rotate.submit=Pivoter
#split
#merge
split.title=Diviser
split.header=Diviser
split.desc.1=Les numéros que vous sélectionnez sont le numéro de page sur lequel vous souhaitez faire une division
@ -549,17 +625,17 @@ split.splitPages=Pages sur lesquelles diviser
split.submit=Diviser
#imageToPDF
#merge
imageToPDF.title=Image en PDF
imageToPDF.header=Image en PDF
imageToPDF.submit=Convertir
imageToPDF.selectText.1=Étirer pour adapter
imageToPDF.selectText.2=Rotation automatique du PDF
imageToPDF.selectText.3=Logique multi-fichiers (uniquement activée si vous travaillez avec plusieurs images)
imageToPDF.selectText.4=Fusionner en un seul PDF
imageToPDF.selectText.5=Convertir en PDF séparés
imageToPDF.submit=Convertir
#pdfToImage
pdfToImage.title=Image en PDF
pdfToImage.header=Image en PDF
@ -637,7 +713,7 @@ removePassword.submit=Supprimer
#changeMetadata
changeMetadata.title=Modifier les métadonnées
changeMetadata.title=Titre
changeMetadata.header=Modifier les métadonnées
changeMetadata.selectText.1=Veuillez modifier les variables que vous souhaitez modifier.
changeMetadata.selectText.2=Supprimer toutes les métadonnées
@ -656,6 +732,16 @@ changeMetadata.selectText.5=Ajouter une entrée de métadonnées personnalisée
changeMetadata.submit=Modifier
#xlsToPdf
##########################
### TODO: Translate ###
##########################
xlsToPdf.title=Excel to PDF
xlsToPdf.header=Excel to PDF
xlsToPdf.selectText.1=Select XLS or XLSX Excel sheet to convert
xlsToPdf.convert=convert
#pdfToPDFA
pdfToPDFA.title=PDF en PDF/A
pdfToPDFA.header=PDF en PDF/A

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Apri in questa finestra
settings.downloadOption.2=Apri in una nuova finestra
settings.downloadOption.3=Scarica file
settings.zipThreshold=Comprimi file in .zip quando il numero di download supera
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=Aggiungi immagine
#merge
merge.title=Unisci
merge.header=Unisci 2 o più PDF
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Unisci
@ -626,17 +676,14 @@ watermark.selectText.4=Rotazione (0-360):
watermark.selectText.5=spazio orizzontale (tra ogni filigrana):
watermark.selectText.6=spazio verticale (tra ogni filigrana):
watermark.selectText.7=Opacità (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Aggiungi Filigrana
#remove-watermark
remove-watermark.title=Rimuovi Filigrana
remove-watermark.header=Rimuovi filigrana
remove-watermark.selectText.1=Seleziona PDF da cui rimuovere la filigrana:
remove-watermark.selectText.2=Testo:
remove-watermark.submit=Rimuovi Filigrana
#Change permissions
permissions.title=Cambia Permessi
permissions.header=Cambia permessi

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=PDFにパスワードが設定されてますが、パスワードが入力されてないか間違ってます。
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=同じウィンドウで開く
settings.downloadOption.2=新しいウィンドウで開く
settings.downloadOption.3=ファイルをダウンロード
settings.zipThreshold=このファイル数を超えたときにファイルを圧縮する
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=画像の追加
#merge
merge.title=結合
merge.header=複数のPDFを結合 (2ファイル以上)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=結合
@ -626,17 +676,14 @@ watermark.selectText.4=回転 (0-360):
watermark.selectText.5=幅スペース (各透かし間の水平方向のスペース):
watermark.selectText.6=高さスペース (各透かし間の垂直方向のスペース):
watermark.selectText.7=不透明度 (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=透かしを追加
#remove-watermark
remove-watermark.title=透かしの削除
remove-watermark.header=透かしの削除
remove-watermark.selectText.1=透かしを削除するPDFを選択:
remove-watermark.selectText.2=透かしのテキスト:
remove-watermark.submit=透かしを削除
#Change permissions
permissions.title=権限の変更
permissions.header=権限の変更

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=현재 창에서 열기
settings.downloadOption.2=새 창에서 열기
settings.downloadOption.3=다운로드
settings.zipThreshold=다운로드한 파일 수가 초과된 경우 파일 압축하기
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=이미지 추가
#merge
merge.title=병합
merge.header=여러 개의 PDF 병합 (2개 이상)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=병합
@ -626,17 +676,14 @@ watermark.selectText.4=회전 각도 (0-360):
watermark.selectText.5=가로 간격 (각 워터마크 사이의 가로 공간):
watermark.selectText.6=세로 간격 (각 워터마크 사이의 세로 공간):
watermark.selectText.7=투명도 (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=워터마크 추가
#remove-watermark
remove-watermark.title=워터마크 제거
remove-watermark.header=워터마크 제거
remove-watermark.selectText.1=워터마크를 제거할 PDF 선택:
remove-watermark.selectText.2=워터마크 텍스트:
remove-watermark.submit=워터마크 제거
#Change permissions
permissions.title=권한 변경
permissions.header=권한 변경

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=Dokument PDF jest zabezpieczony hasłem, musisz podać prawidłowe hasło.
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Otwórz w tym samym oknie
settings.downloadOption.2=Otwórz w nowym oknie
settings.downloadOption.3=Pobierz plik
settings.zipThreshold=Spakuj pliki, gdy liczba pobranych plików przekroczy
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=Dodaj obraz
#merge
merge.title=Połącz
merge.header=Połącz wiele dokumentów PDF (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Połącz
@ -626,17 +676,14 @@ watermark.selectText.4=Obrót (0-360):
watermark.selectText.5=Odstęp w poziomie (odstęp między każdym znakiem wodnym w poziomie):
watermark.selectText.6=Odstęp w pionie (odstęp między każdym znakiem wodnym w pionie):
watermark.selectText.7=Nieprzezroczystość (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Dodaj znak wodny
#remove-watermark
remove-watermark.title=Usuń znak wodny
remove-watermark.header=Usuń znak wodny
remove-watermark.selectText.1=Wybierz dokument PDF, aby usunąć znak wodny z:
remove-watermark.selectText.2=Treść zanku wodnego:
remove-watermark.submit=Usuń znak wodny
#Change permissions
permissions.title=Zmień uprawnienia
permissions.header=Zmień uprawnienia

View file

@ -31,7 +31,14 @@ sizes.medium=Médio
sizes.large=Grande
sizes.x-large=Muito grande
error.pdfPassword=O documento PDF está protegido por senha e a senha não foi fornecida ou está incorreta
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,12 +61,57 @@ settings.downloadOption.1=Abrir na mesma janela
settings.downloadOption.2=Abrir em nova janela
settings.downloadOption.3=⇬ Fazer download do arquivo
settings.zipThreshold=Compactar arquivos quando o número de arquivos baixados exceder
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
#############
home.desc=Seu melhor utilitário para suas necessidades de PDF.
home.multiTool.title=Multiferramenta de PDF
home.multiTool.desc=Mesclar, girar, reorganizar e remover páginas
multiTool.tags=Multi Ferramenta, Operação Múltipla, Interface do Usuário, Clique e Arraste, Front-end, Lado do Cliente
@ -76,6 +128,7 @@ home.rotate.title=Girar
home.rotate.desc=Girar facilmente seus PDFs.
rotate.tags=Lado do Servidor
home.imageToPdf.title=Imagem para PDF
home.imageToPdf.desc=Converter uma imagem (PNG, JPEG, GIF) em PDF.
imageToPdf.tags=conversão, img, jpg, imagem, foto
@ -88,6 +141,7 @@ home.pdfOrganiser.title=Organizar
home.pdfOrganiser.desc=Remover/reorganizar as páginas em qualquer ordem.
pdfOrganiser.tags=duplex, par, ímpar, ordenar, mover
home.addImage.title=Adicionar Imagem
home.addImage.desc=Adicionar uma imagem em um local definido no PDF (trabalho em andamento)
addImage.tags=img, jpg, imagem, foto
@ -100,6 +154,7 @@ home.permissions.title=Alterar Permissões
home.permissions.desc=Alterar as permissões do seu documento PDF.
permissions.tags=leitura, escrita, edição, impressão
home.removePages.title=Remover
home.removePages.desc=Excluir as páginas indesejadas do seu documento PDF.
removePages.tags=Remover páginas, excluir páginas
@ -116,6 +171,7 @@ home.compressPdfs.title=Comprimir
home.compressPdfs.desc=Comprimir PDFs para reduzir o tamanho do arquivo.
compressPdfs.tags=compactar, pequeno, mínimo
home.changeMetadata.title=Alterar Metadados
home.changeMetadata.desc=Alterar/remover/adicionar metadados de um documento PDF.
changeMetadata.tags=Título, autor, data, criação, hora, editor, produtor, estatísticas
@ -128,6 +184,7 @@ home.ocr.title=OCR / Limpeza de Digitalizações
home.ocr.desc=A limpeza verifica e detecta texto em imagens de um PDF e o adiciona novamente como texto.
ocr.tags=reconhecimento, texto, imagem, digitalização, leitura, identificação, detecção, editável
home.extractImages.title=Extrair Imagens
home.extractImages.desc=Extrair todas as imagens de um PDF e salvá-las em um arquivo zip.
extractImages.tags=imagem, foto, salvar, arquivo, zip, captura, coleta
@ -152,6 +209,7 @@ home.PDFToHTML.title=PDF para HTML
home.PDFToHTML.desc=Converter PDF para o formato HTML
PDFToHTML.tags=conteúdo web, compatível com navegador
home.PDFToXML.title=PDF para XML
home.PDFToXML.desc=Converter PDF para o formato XML
PDFToXML.tags=extração-de-dados,conteúdo-estruturado,interoperabilidade,transformação,converter
@ -228,26 +286,27 @@ home.HTMLToPDF.title=HTML para PDF
home.HTMLToPDF.desc=Converte qualquer arquivo HTML ou zip para PDF
HTMLToPDF.tags=marcação,conteúdo-web,transformação,converter
home.MarkdownToPDF.title=Markdown para PDF
home.MarkdownToPDF.desc=Converte qualquer arquivo Markdown para PDF
MarkdownToPDF.tags=marcação,conteúdo-web,transformação,converter
home.getPdfInfo.title=Obter TODAS as Informações de um PDF
home.getPdfInfo.desc=Obtém todas as informações possíveis de um PDF
getPdfInfo.tags=informações,dados,estatísticas
home.extractPage.title=Extrair Página(s)
home.extractPage.desc=Extrai páginas selecionadas de um PDF
extractPage.tags=extrair
home.PdfToSinglePage.title=PDF para Página Única Grande
home.PdfToSinglePage.desc=Combina todas as páginas de um PDF em uma única página grande
PdfToSinglePage.tags=página única
##########################
### TODO: Translate ###
##########################
home.showJS.title=Mostrar Javascript
home.showJS.desc=Procura e exibe qualquer JavaScript injetado em um PDF
showJS.tags=JavaScript
@ -258,30 +317,31 @@ showJS.tags=JavaScript
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Exibir JavaScript
showJS.header=Exibir JavaScript
showJS.downloadJS=Download do JavaScript
showJS.submit=Exibir
#pdfToSinglePage
pdfToSinglePage.title=PDF para Página Única
pdfToSinglePage.header=PDF para Página Única
pdfToSinglePage.submit=Converter para Página Única
#pageExtracter
pageExtracter.title=Extrair Páginas
pageExtracter.header=Extrair Páginas
pageExtracter.submit=Extrair
#getPdfInfo
getPdfInfo.title=Obter Informações do PDF
getPdfInfo.header=Obter Informações do PDF
getPdfInfo.submit=Obter Informações
getPdfInfo.downloadJson=Download JSON
#markdown-to-pdf
MarkdownToPDF.title=Markdown para PDF
MarkdownToPDF.header=Markdown para PDF
@ -289,12 +349,15 @@ MarkdownToPDF.submit=Converter
MarkdownToPDF.help=Trabalho em andamento
MarkdownToPDF.credit=Usa o WeasyPrint
#url-to-pdf
URLToPDF.title=URL para PDF
URLToPDF.header=URL para PDF
URLToPDF.submit=Converter
URLToPDF.credit=Usa o WeasyPrint
#html-to-pdf
HTMLToPDF.title=HTML para PDF
HTMLToPDF.header=HTML para PDF
@ -302,6 +365,7 @@ HTMLToPDF.help=Aceita arquivos HTML e ZIPs contendo html/css/imagens etc necess
HTMLToPDF.submit=Converter
HTMLToPDF.credit=Usa o WeasyPrint
#sanitizePDF
sanitizePDF.title=Sanitizar PDF
sanitizePDF.header=Sanitizar um arquivo PDF
@ -312,8 +376,8 @@ sanitizePDF.selectText.4=Remover links
sanitizePDF.selectText.5=Remover fontes
sanitizePDF.submit=Sanitizar PDF
#addPageNumbers
autoCrop.title=Adicionar Números de Página
addPageNumbers.title=Adicionar Números de Página
addPageNumbers.header=Adicionar Números de Página
addPageNumbers.selectText.1=Selecionar arquivo PDF:
@ -324,11 +388,13 @@ addPageNumbers.selectText.5=Páginas a Numerar
addPageNumbers.selectText.6=Texto Personalizado
addPageNumbers.submit=Adicionar Números de Página
#auto-rename
auto-rename.title=Rename Automático
auto-rename.header=Rename Automático de PDF
auto-rename.submit=Rename Automático
#adjustContrast
adjustContrast.title=Ajustar Contraste
adjustContrast.header=Ajustar Contraste
@ -337,11 +403,13 @@ adjustContrast.brightness=Brilho:
adjustContrast.saturation=Saturação:
adjustContrast.download=Download
#crop
crop.title=Cortar
crop.header=Cortar Imagem
crop.submit=Enviar
#autoSplitPDF
autoSplitPDF.title=Divisão Automática de PDF
autoSplitPDF.header=Divisão Automática de PDF
@ -356,6 +424,7 @@ autoSplitPDF.dividerDownload1=Download 'Folha Divisória Automática (mínima).p
autoSplitPDF.dividerDownload2=Download 'Folha Divisória Automática (com instruções).pdf'
autoSplitPDF.submit=Enviar
#pipeline
pipeline.title=Pipeline
@ -366,6 +435,7 @@ pageLayout.header=Layout de Múltiplas Páginas
pageLayout.pagesPerSheet=Páginas por folha:
pageLayout.submit=Enviar
#scalePages
scalePages.title=Ajustar Tamanho/Escala da Página
scalePages.header=Ajustar Tamanho/Escala da Página
@ -373,6 +443,7 @@ scalePages.pageSize=Tamanho de uma página do documento.
scalePages.scaleFactor=Fator de zoom (corte) de uma página.
scalePages.submit=Enviar
#certSign
certSign.title=Assinatura com Certificado
certSign.header=Assine um PDF com o seu certificado (Em desenvolvimento)
@ -388,6 +459,7 @@ certSign.location=Localização
certSign.name=Nome
certSign.submit=Assinar PDF
#removeBlanks
removeBlanks.title=Remover Páginas em Branco
removeBlanks.header=Remover Páginas em Branco
@ -397,6 +469,7 @@ removeBlanks.whitePercent=Porcentagem de Branco (%):
removeBlanks.whitePercentDesc=Porcentagem da página que deve ser branca para ser removida
removeBlanks.submit=Remover Páginas em Branco
#compare
compare.title=Comparar
compare.header=Comparar PDFs
@ -404,6 +477,7 @@ compare.document.1=Documento 1
compare.document.2=Documento 2
compare.submit=Comparar
#sign
sign.title=Assinar
sign.header=Assinar PDFs
@ -413,16 +487,19 @@ sign.text=Inserir Texto
sign.clear=Limpar
sign.add=Adicionar
#repair
repair.title=Reparar
repair.header=Reparar PDFs
repair.submit=Reparar
#flatten
flatten.title=Achatar
flatten.header=Achatar PDFs
flatten.submit=Achatar
#ScannerImageSplit
ScannerImageSplit.selectText.1=Limite de Ângulo:
ScannerImageSplit.selectText.2=Define o ângulo absoluto mínimo necessário para que a imagem seja girada (padrão: 10).
@ -450,16 +527,22 @@ ocr.selectText.8=Normal (gerará um erro se o PDF já contiver texto)
ocr.selectText.9=Configurações adicionais
ocr.selectText.10=Modo OCR
ocr.selectText.11=Remover imagens após o OCR (remove TODAS as imagens, útil apenas como parte do processo de conversão)
##########################
### TODO: Translate ###
##########################
ocr.selectText.12=Render Type (Advanced)
ocr.help=Por favor, leia a documentação sobre como usar isso para outros idiomas e/ou fora do ambiente Docker
ocr.credit=Este serviço usa OCRmyPDF e Tesseract para OCR.
ocr.submit=Processar PDF com OCR
#extractImages
extractImages.title=Extrair Imagens
extractImages.header=Extrair Imagens
extractImages.selectText=Selecione o formato de imagem para converter as imagens extraídas
extractImages.submit=Extrair
#File to PDF
fileToPDF.title=Arquivo para PDF
fileToPDF.header=Converter Qualquer Arquivo para PDF
@ -467,6 +550,7 @@ fileToPDF.credit=Este serviço usa o LibreOffice e o Unoconv para conversão de
fileToPDF.supportedFileTypes=Os tipos de arquivo suportados devem incluir os listados abaixo. No entanto, para obter uma lista atualizada completa dos formatos suportados, consulte a documentação do LibreOffice.
fileToPDF.submit=Converter para PDF
#compress
compress.title=Comprimir
compress.header=Comprimir PDF
@ -478,6 +562,7 @@ compress.selectText.4=Modo Automático - Ajusta automaticamente a qualidade para
compress.selectText.5=Tamanho Esperado do PDF (por exemplo, 25 MB, 10,8 MB, 25 KB)
compress.submit=Comprimir
#Add image
addImage.title=Adicionar Imagem
addImage.header=Adicionar Imagem ao PDF
@ -485,32 +570,43 @@ addImage.everyPage=Para cada página?
addImage.upload=Enviar Imagem
addImage.submit=Adicionar Imagem
#merge
merge.title=Mesclar
merge.header=Mesclar Vários PDFs (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Mesclar
#pdfOrganiser
pdfOrganiser.title=Organizador de Páginas
pdfOrganiser.header=Organizador de Páginas PDF
pdfOrganiser.submit=Reorganizar Páginas
#multiTool
multiTool.title=Multiferramenta de PDF
multiTool.header=Multiferramenta de PDF
#pageRemover
pageRemover.title=Remover Página
pageRemover.header=Remover Páginas do PDF
pageRemover.pagesToDelete=Páginas a serem excluídas (insira uma lista separada por vírgulas de números de página):
pageRemover.submit=Excluir Páginas
#rotate
rotate.title=Girar PDF
rotate.header=Girar PDF
rotate.selectAngle=Selecione o ângulo de rotação (múltiplos de 90 graus):
rotate.submit=Girar
#merge
split.title=Dividir PDF
split.header=Dividir PDF
@ -525,6 +621,7 @@ split.desc.8=Documento Nº6: Páginas 9 e 10
split.splitPages=Digite as páginas para a divisão:
split.submit=Dividir
#merge
imageToPDF.title=Imagem para PDF
imageToPDF.header=Converter Imagem para PDF
@ -534,7 +631,8 @@ imageToPDF.selectText.2=Girar Automaticamente
imageToPDF.selectText.3=Lógica de Vários Arquivos (Ativada apenas ao trabalhar com várias imagens)
imageToPDF.selectText.4=Mesclar em um Único PDF
imageToPDF.selectText.5=Converter em PDFs Separados
#pdfToImage
pdfToImage.title=PDF para Imagem
pdfToImage.header=Converter PDF para Imagem
@ -570,6 +668,7 @@ addPassword.selectText.15=Restringe o que pode ser feito com o documento após a
addPassword.selectText.16=Restringe a abertura do próprio documento
addPassword.submit=Criptografar
#watermark
watermark.title=Adicionar Marca d'Água
watermark.header=Adicionar Marca d'Água
@ -584,12 +683,6 @@ watermark.selectText.8=Tipo de Marca d'Água
watermark.selectText.9=Imagem da Marca d'Água
watermark.submit=Adicionar Marca d'Água
#remove-watermark
remove-watermark.title=Remover Marca d'Água
remove-watermark.header=Remover Marca d'Água
remove-watermark.selectText.1=Selecione o PDF para Remover a Marca d'Água
remove-watermark.selectText.2=Texto da Marca d'Água
remove-watermark.submit=Remover Marca d'Água
#Change permissions
permissions.title=Alterar Permissões
@ -607,6 +700,7 @@ permissions.selectText.9=Impedir Impressão
permissions.selectText.10=Impedir Impressão de Formatos Diferentes
permissions.submit=Mudar
#remove password
removePassword.title=Remover Senha
removePassword.header=Remover Senha (Descriptografar)
@ -616,7 +710,7 @@ removePassword.submit=Remover
#changeMetadata
changeMetadata.title=Alterar Metadados
changeMetadata.title=Título:
changeMetadata.header=Alterar Metadados
changeMetadata.selectText.1=Edite as Variáveis que Deseja Alterar
changeMetadata.selectText.2=Excluir Todos os Metadados
@ -634,18 +728,21 @@ changeMetadata.selectText.4=Outros Metadados
changeMetadata.selectText.5=Adicionar Entrada de Metadados Personalizados
changeMetadata.submit=Mudar
#xlsToPdf
xlsToPdf.title=Excel para PDF
xlsToPdf.header=Excel para PDF
xlsToPdf.selectText.1=Selecione a Planilha Excel XLS ou XLSX para Converter
xlsToPdf.convert=Converter
#pdfToPDFA
pdfToPDFA.title=PDF para PDF/A
pdfToPDFA.header=PDF para PDF/A
pdfToPDFA.credit=Este serviço usa OCRmyPDF para Conversão de PDF/A
pdfToPDFA.submit=Converter
#PDFToWord
PDFToWord.title=PDF para Word
PDFToWord.header=PDF para Word
@ -653,6 +750,7 @@ PDFToWord.selectText.1=Formato do Arquivo de Saída
PDFToWord.credit=Este serviço usa o LibreOffice para Conversão de Arquivos.
PDFToWord.submit=Converter
#PDFToPresentation
PDFToPresentation.title=PDF para Apresentação
PDFToPresentation.header=PDF para Apresentação
@ -660,6 +758,7 @@ PDFToPresentation.selectText.1=Formato do Arquivo de Saída
PDFToPresentation.credit=Este serviço usa o LibreOffice para Conversão de Arquivos.
PDFToPresentation.submit=Converter
#PDFToText
PDFToText.title=PDF para Texto/RTF
PDFToText.header=PDF para Texto/RTF
@ -667,12 +766,14 @@ PDFToText.selectText.1=Formato do Arquivo de Saída
PDFToText.credit=Este serviço usa o LibreOffice para Conversão de Arquivos.
PDFToText.submit=Converter
#PDFToHTML
PDFToHTML.title=PDF para HTML
PDFToHTML.header=PDF para HTML
PDFToHTML.credit=Este serviço usa o LibreOffice para Conversão de Arquivos.
PDFToHTML.submit=Converter
#PDFToXML
PDFToXML.title=PDF para XML
PDFToXML.header=PDF para XML

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Deschide în aceeași fereastră
settings.downloadOption.2=Deschide într-o fereastră nouă
settings.downloadOption.3=Descarcă fișierul
settings.zipThreshold=Împachetează fișierele când numărul de fișiere descărcate depășește
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=Adăugare imagine
#merge
merge.title=Unire
merge.header=Unirea mai multor PDF-uri (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Unire
@ -626,17 +676,14 @@ watermark.selectText.4=Rotire (0-360):
watermark.selectText.5=widthSpacer (Spațiu între fiecare filigran pe orizontală):
watermark.selectText.6=heightSpacer (Spațiu între fiecare filigran pe verticală):
watermark.selectText.7=Opacitate (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Adăugați Filigran
#remove-watermark
remove-watermark.title=Eliminați Filigran
remove-watermark.header=Eliminați Filigran
remove-watermark.selectText.1=Selectați PDF-ul de la care să eliminați filigranul:
remove-watermark.selectText.2=Textul Filigranului:
remove-watermark.submit=Eliminați Filigran
#Change permissions
permissions.title=Schimbați Permisiunile
permissions.header=Schimbați Permisiunile

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Открыть в том же окне
settings.downloadOption.2=Открыть в новом окне
settings.downloadOption.3=Загрузить файл
settings.zipThreshold=Zip-файлы, когда количество загруженных файлов превышает
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=Добавить изображение
#merge
merge.title=Объединить
merge.header=Объединение нескольких PDF-файлов (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Объединить
@ -626,17 +676,14 @@ watermark.selectText.4=Поворот (0-360):
watermark.selectText.5=widthSpacer (пробел между каждым водяным знаком по горизонтали):
watermark.selectText.6=heightSpacer (пробел между каждым водяным знаком по вертикали):
watermark.selectText.7=Непрозрачность (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Добавить водяной знак
#remove-watermark
remove-watermark.title=Удалить водяной знак
remove-watermark.header=Удалить водяной знак
remove-watermark.selectText.1=Выберите PDF, чтобы удалить водяной знак из:
remove-watermark.selectText.2=Текст водяного знака:
remove-watermark.submit=Удалить водяной знак
#Change permissions
permissions.title=Изменить разрешения
permissions.header=Изменить разрешения

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=Öppnas i samma fönster
settings.downloadOption.2=Öppna i nytt fönster
settings.downloadOption.3=Ladda ner fil
settings.zipThreshold=Zip-filer när antalet nedladdade filer överskrider
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=Lägg till bild
#merge
merge.title=Sammanfoga
merge.header=Slå samman flera PDF-filer (2+)
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=Slå samman
@ -626,17 +676,14 @@ watermark.selectText.4=Rotation (0-360):
watermark.selectText.5=widthSpacer (mellanrum mellan varje vattenstämpel horisontellt):
watermark.selectText.6=heightSpacer (mellanrum mellan varje vattenstämpel vertikalt):
watermark.selectText.7=Opacitet (0% - 100%):
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=Lägg till vattenstämpel
#remove-watermark
remove-watermark.title=Ta bort vattenstämpel
remove-watermark.header=Ta bort vattenstämpel
remove-watermark.selectText.1=Välj PDF för att ta bort vattenstämpel från:
remove-watermark.selectText.2=Vattenstämpeltext:
remove-watermark.submit=Ta bort vattenstämpel
#Change permissions
permissions.title=Ändra behörigheter
permissions.header=Ändra behörigheter

View file

@ -31,7 +31,14 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
##########################
### TODO: Translate ###
##########################
delete=Delete
username=Username
password=Password
welcome=Welcome
=Property
#############
# NAVBAR #
@ -54,6 +61,50 @@ settings.downloadOption.1=在同一窗口打开
settings.downloadOption.2=在新窗口中打开
settings.downloadOption.3=下载文件
settings.zipThreshold=当下载的文件数量超过限制时,将文件压缩。
##########################
### TODO: Translate ###
##########################
settings.signOut=Sign Out
settings.accountSettings=Account Settings
##########################
### TODO: Translate ###
##########################
account.title=Account Settings
account.accountSettings=Account Settings
account.adminSettings=Admin Settings - View and Add Users
account.userControlSettings=User Control Settings
account.changeUsername=New Username
account.changeUsername=Change Username
account.password=Confirmation Password
account.oldPassword=Old password
account.newPassword=New Password
account.changePassword=Change Password
account.confirmNewPassword=Confirm New Password
account.signOut=Sign Out
account.yourApiKey=Your API Key
account.syncTitle=Sync browser settings with Account
account.settingsCompare=Settings Comparison:
account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser
##########################
### TODO: Translate ###
##########################
adminUserSettings.title=User Control Settings
adminUserSettings.header=Admin User Control Settings
adminUserSettings.admin=Admin
adminUserSettings.user=User
adminUserSettings.addUser=Add New User
adminUserSettings.roles=Roles
adminUserSettings.role=Role
adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.submit=Save User
#############
# HOME-PAGE #
@ -256,9 +307,6 @@ home.PdfToSinglePage.desc=Merges all PDF pages into one large single page
PdfToSinglePage.tags=single page
##########################
### TODO: Translate ###
##########################
home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS
@ -269,9 +317,6 @@ showJS.tags=JS
# #
###########################
#showJS
##########################
### TODO: Translate ###
##########################
showJS.title=Show Javascript
showJS.header=Show Javascript
showJS.downloadJS=Download Javascript
@ -526,6 +571,11 @@ addImage.submit=添加图片
#merge
merge.title=合并
merge.header=合并多个PDF2个以上
##########################
### TODO: Translate ###
##########################
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.submit=合并
@ -626,17 +676,14 @@ watermark.selectText.4=旋转0-360
watermark.selectText.5=widthSpacer水平方向上每个水印之间的空间
watermark.selectText.6=heightSpacer每个水印之间的垂直空间
watermark.selectText.7=透明度0% - 100%
##########################
### TODO: Translate ###
##########################
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.submit=添加水印
#remove-watermark
remove-watermark.title=去除水印
remove-watermark.header=去除水印
remove-watermark.selectText.1=选择要去除水印的PDF
remove-watermark.selectText.2=水印文本:
remove-watermark.submit=移除水印
#Change permissions
permissions.title=更改权限
permissions.header=改变权限

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash" viewBox="0 0 16 16">
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12-.708.708z"/>
</svg>

After

Width:  |  Height:  |  Size: 891 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 569 B

View file

@ -1,63 +1,113 @@
let currentSort = {
field: null,
descending: false
};
document.getElementById("fileInput-input").addEventListener("change", function() {
var files = this.files;
var list = document.getElementById("selectedFiles");
list.innerHTML = "";
for (var i = 0; i < files.length; i++) {
var item = document.createElement("li");
item.className = "list-group-item";
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center w-100">
<div class="filename">${files[i].name}</div>
<div class="arrows d-flex">
<button class="btn btn-secondary move-up"><span>&uarr;</span></button>
<button class="btn btn-secondary move-down"><span>&darr;</span></button>
</div>
</div>
`;
list.appendChild(item);
}
var files = this.files;
displayFiles(files);
});
var moveUpButtons = document.querySelectorAll(".move-up");
for (var i = 0; i < moveUpButtons.length; i++) {
moveUpButtons[i].addEventListener("click", function(event) {
event.preventDefault();
var parent = this.closest(".list-group-item");
var grandParent = parent.parentNode;
if (parent.previousElementSibling) {
grandParent.insertBefore(parent, parent.previousElementSibling);
updateFiles();
}
});
}
function displayFiles(files) {
var list = document.getElementById("selectedFiles");
list.innerHTML = "";
var moveDownButtons = document.querySelectorAll(".move-down");
for (var i = 0; i < moveDownButtons.length; i++) {
moveDownButtons[i].addEventListener("click", function(event) {
event.preventDefault();
var parent = this.closest(".list-group-item");
var grandParent = parent.parentNode;
if (parent.nextElementSibling) {
grandParent.insertBefore(parent.nextElementSibling, parent);
updateFiles();
}
});
}
for (var i = 0; i < files.length; i++) {
var item = document.createElement("li");
item.className = "list-group-item";
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center w-100">
<div class="filename">${files[i].name}</div>
<div class="arrows d-flex">
<button class="btn btn-secondary move-up"><span>&uarr;</span></button>
<button class="btn btn-secondary move-down"><span>&darr;</span></button>
</div>
</div>
`;
list.appendChild(item);
}
function updateFiles() {
var dataTransfer = new DataTransfer();
var liElements = document.querySelectorAll("#selectedFiles li");
attachMoveButtons();
}
for (var i = 0; i < liElements.length; i++) {
var fileNameFromList = liElements[i].querySelector(".filename").innerText;
var fileFromFiles;
for (var j = 0; j < files.length; j++) {
var file = files[j];
if (file.name === fileNameFromList) {
dataTransfer.items.add(file);
break;
}
}
}
document.getElementById("fileInput-input").files = dataTransfer.files;
}
});
function attachMoveButtons() {
var moveUpButtons = document.querySelectorAll(".move-up");
for (var i = 0; i < moveUpButtons.length; i++) {
moveUpButtons[i].addEventListener("click", function(event) {
event.preventDefault();
var parent = this.closest(".list-group-item");
var grandParent = parent.parentNode;
if (parent.previousElementSibling) {
grandParent.insertBefore(parent, parent.previousElementSibling);
updateFiles();
}
});
}
var moveDownButtons = document.querySelectorAll(".move-down");
for (var i = 0; i < moveDownButtons.length; i++) {
moveDownButtons[i].addEventListener("click", function(event) {
event.preventDefault();
var parent = this.closest(".list-group-item");
var grandParent = parent.parentNode;
if (parent.nextElementSibling) {
grandParent.insertBefore(parent.nextElementSibling, parent);
updateFiles();
}
});
}
}
document.getElementById("sortByNameBtn").addEventListener("click", function() {
if (currentSort.field === "name" && !currentSort.descending) {
currentSort.descending = true;
sortFiles((a, b) => b.name.localeCompare(a.name));
} else {
currentSort.field = "name";
currentSort.descending = false;
sortFiles((a, b) => a.name.localeCompare(b.name));
}
});
document.getElementById("sortByDateBtn").addEventListener("click", function() {
if (currentSort.field === "lastModified" && !currentSort.descending) {
currentSort.descending = true;
sortFiles((a, b) => b.lastModified - a.lastModified);
} else {
currentSort.field = "lastModified";
currentSort.descending = false;
sortFiles((a, b) => a.lastModified - b.lastModified);
}
});
function sortFiles(comparator) {
// Convert FileList to array and sort
const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator);
// Refresh displayed list
displayFiles(sortedFilesArray);
// Update the files property
const dataTransfer = new DataTransfer();
sortedFilesArray.forEach(file => dataTransfer.items.add(file));
document.getElementById("fileInput-input").files = dataTransfer.files;
}
function updateFiles() {
var dataTransfer = new DataTransfer();
var liElements = document.querySelectorAll("#selectedFiles li");
const files = document.getElementById("fileInput-input").files;
for (var i = 0; i < liElements.length; i++) {
var fileNameFromList = liElements[i].querySelector(".filename").innerText;
var fileFromFiles;
for (var j = 0; j < files.length; j++) {
var file = files[j];
if (file.name === fileNameFromList) {
dataTransfer.items.add(file);
break;
}
}
}
document.getElementById("fileInput-input").files = dataTransfer.files;
}

View file

@ -0,0 +1,295 @@
<!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{account.title})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-9">
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{account.accountSettings}">User Settings</h2>
<hr>
<!-- At the top of the user settings -->
<h3 class="text-center"><span th:text="#{welcome} + ' ' + ${username}">User</span>!</h3>
<!-- Change Username Form -->
<h4></h4>
<form action="/change-username" method="post">
<div class="form-group">
<label for="newUsername" th:text="#{account.changeUsername}">Change Username</label>
<input type="text" class="form-control" name="newUsername" id="newUsername" placeholder="New Username">
</div>
<div class="form-group">
<label for="currentPassword" th:text="#{password}">Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPassword" placeholder="Password">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" th:text="#{account.changeUsername}">Change Username</button>
</div>
</form>
<hr> <!-- Separator Line -->
<!-- Change Password Form -->
<h4 th:text="#{account.changePassword}">Change Password?</h4>
<form action="/change-password" method="post">
<div class="form-group">
<label for="currentPassword" th:text="#{account.oldPassword}">Old Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPassword" th:placeholder="#{account.oldPassword}">
</div>
<div class="form-group">
<label for="newPassword" th:text="#{account.newPassword}">New Password</label>
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{account.newPassword}">
</div>
<div class="form-group">
<label for="confirmNewPassword" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
<input type="password" class="form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" th:text="#{account.changePassword}">Change Password</button>
</div>
</form>
<hr>
<div class="card">
<div class="card-header" th:text="#{account.yourApiKey}">
</div>
<div class="card-body">
<div class="input-group mb-3">
<input type="password" class="form-control" id="apiKey" th:placeholder="#{account.yourApiKey}" readonly>
<div class="input-group-append">
<button class="btn btn-outline-secondary" id="copyBtn" type="button" onclick="copyToClipboard()">
<img src="images/clipboard.svg" alt="Copy" style="height:20px;">
</button>
<button class="btn btn-outline-secondary" id="showBtn" type="button" onclick="showApiKey()">
<img id="eyeIcon" src="images/eye.svg" alt="Toggle API Key Visibility" style="height:20px;">
</button>
<button class="btn btn-outline-secondary" id="refreshBtn" type="button" onclick="refreshApiKey()">
<img id="eyeIcon" src="images/arrow-clockwise.svg" alt="Refresh API-Key" style="height:20px;">
</button>
</div>
</div>
</div>
</div>
<script>
function copyToClipboard() {
const apiKeyElement = document.getElementById("apiKey");
apiKeyElement.select();
document.execCommand("copy");
}
function showApiKey() {
const apiKeyElement = document.getElementById("apiKey");
const copyBtn = document.getElementById("copyBtn");
const eyeIcon = document.getElementById("eyeIcon");
if (apiKeyElement.type === "password") {
apiKeyElement.type = "text";
eyeIcon.src = "images/eye-slash.svg";
copyBtn.disabled = false; // Enable copy button when API key is visible
} else {
apiKeyElement.type = "password";
eyeIcon.src = "images/eye.svg";
copyBtn.disabled = true; // Disable copy button when API key is hidden
}
}
document.addEventListener("DOMContentLoaded", async function() {
try {
let response = await fetch('/get-api-key', { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
} else {
manageUIState(null);
}
} catch (error) {
console.error('There was an error:', error);
}
});
async function refreshApiKey() {
try {
let response = await fetch('/update-api-key', { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
document.getElementById("apiKey").type = 'text';
document.getElementById("copyBtn").disabled = false;
} else {
alert('Error refreshing API key.');
}
} catch (error) {
console.error('There was an error:', error);
}
}
function manageUIState(apiKey) {
const apiKeyElement = document.getElementById("apiKey");
const showBtn = document.getElementById("showBtn");
const copyBtn = document.getElementById("copyBtn");
if (apiKey && apiKey.trim().length > 0) {
apiKeyElement.value = apiKey;
showBtn.disabled = false;
copyBtn.disabled = true;
} else {
apiKeyElement.value = "";
showBtn.disabled = true;
copyBtn.disabled = true;
}
}
</script>
<hr> <!-- Separator Line -->
<h4 th:text="#{account.syncTitle}">Sync browser settings with Account</h4>
<div class="container mt-4">
<h3 th:text="#{account.settingsCompare}">Settings Comparison:</h3>
<table id="settingsTable" class="table table-bordered table-sm table-striped">
<thead>
<tr>
<th th:text="#{account.property}">Property</th>
<th th:text="#{account.accountSettings}">Account Setting</th>
<th th:text="#{account.webBrowserSettings}">Web Browser Setting</th>
</tr>
</thead>
<tbody>
<!-- This will be dynamically populated by JavaScript -->
</tbody>
</table>
<div class="buttons-container mt-3 text-center">
<button id="syncToBrowser" class="btn btn-primary btn-sm" th:text="#{account.syncToBrowser}">Sync Account -> Browser</button>
<button id="syncToAccount" class="btn btn-secondary btn-sm" th:text="#{account.syncToAccount}">Sync Account <- Browser</button>
</div>
</div>
<style>
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.buttons-container {
margin-top: 20px;
text-align: center;
}
</style>
<script th:inline="javascript">
document.addEventListener("DOMContentLoaded", function() {
const settingsTableBody = document.querySelector("#settingsTable tbody");
/*<![CDATA[*/
var accountSettingsString = /*[[${settings}]]*/ {};
/*]]>*/
var accountSettings = JSON.parse(accountSettingsString);
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
allKeys.forEach(key => {
if(key === 'debug' || key === '0' || key === '1') return; // Ignoring specific keys
const accountValue = accountSettings[key] || '-';
const browserValue = localStorage.getItem(key) || '-';
const row = settingsTableBody.insertRow();
const propertyCell = row.insertCell(0);
const accountCell = row.insertCell(1);
const browserCell = row.insertCell(2);
propertyCell.textContent = key;
accountCell.textContent = accountValue;
browserCell.textContent = browserValue;
});
document.getElementById('syncToBrowser').addEventListener('click', function() {
// First, clear the local storage
localStorage.clear();
// Then, set the account settings to local storage
for (let key in accountSettings) {
if(key !== 'debug' && key !== '0' && key !== '1') { // Only sync non-ignored keys
localStorage.setItem(key, accountSettings[key]);
}
}
location.reload(); // Refresh the page after sync
});
document.getElementById('syncToAccount').addEventListener('click', function() {
let form = document.createElement("form");
form.method = "POST";
form.action = "/updateUserSettings"; // Your endpoint URL
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if(key !== 'debug' && key !== '0' && key !== '1') { // Only send non-ignored keys
let hiddenField = document.createElement("input");
hiddenField.type = "hidden";
hiddenField.name = key;
hiddenField.value = localStorage.getItem(key);
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
});
});
</script>
<div class="form-group mt-4">
<a href="/logout">
<button type="button" class="btn btn-danger" th:text="#{account.signOut}">Sign Out</button>
</a>
<a th:if="${role == 'ROLE_ADMIN'}" href="addUsers" target="_blank">
<button type="button" class="btn btn-info" th:text="#{account.adminSettings}">Admin Settings</button>
</a>
</div>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View file

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<!-- User Settings Title -->
<h2 class="text-center" th:text="#{adminUserSettings.header}">Admin User Control Settings</h2>
<table class="table">
<thead>
<tr>
<th th:text="#{username}">Username</th>
<th th:text="#{adminUserSettings.roles}">Roles</th>
<th th:text="#{adminUserSettings.actions}">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.username}"></td>
<td th:text="${user.getRolesAsString()}"></td>
<td>
<a th:href="@{'/admin/deleteUser/' + ${user.username}}" th:text="#{delete}">Delete</a>
</td>
</tr>
</tbody>
</table>
<h2 th:text="#{adminUserSettings.addUser}">Add New User</h2>
<form action="/admin/saveUser" method="post">
<div class="form-group">
<label for="username" th:text="#{username}">Username</label>
<input type="text" class="form-control" name="username" required>
</div>
<div class="form-group">
<label for="password" th:text="#{password}">Password</label>
<input type="password" class="form-control" name="password" required>
</div>
<div class="form-group">
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="form-control" required>
<option value="ROLE_ADMIN" th:text="#{adminUserSettings.admin}">Admin</option>
<option value="ROLE_USER" th:text="#{adminUserSettings.user}">User</option>
<option value="ROLE_LIMITED_API_USER" th:text="#{adminUserSettings.apiUser}">Limited API User</option>
<option value="ROLE_WEB_ONLY_USER" th:text="#{adminUserSettings.webOnlyUser}">Web Only User</option>
</select>
</div>
<!-- Add other fields as required -->
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
</form>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View file

@ -341,9 +341,18 @@
<label class="custom-control-label" for="boredWaiting" th:text="#{bored}"></label>
</div>
</div>
<a th:if="${@loginEnabled}" href="account" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary" th:text="#{settings.accountSettings}">Account Settings</button>
</a>
</div>
<div class="modal-footer">
<a th:if="${@loginEnabled}" href="/logout">
<button type="button" class="btn btn-danger" th:text="#{settings.signOut}">Sign Out</button>
</a>
<button type="button" class="btn btn-secondary" data-dismiss="modal" th:text="#{close}"></button>
</div>

View file

@ -0,0 +1,59 @@
<!-- Hi if you have been redirected here when using API then you might need to supply a X-API-KEY key in header to authenticate! -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<div class="container">
<div class="row justify-content-center align-items-center" style="height:100vh;">
<div class="col-md-4">
<div class="login-container card">
<div class="card-header text-center">
<img src="favicon.svg" alt="Logo" class="img-fluid" style="max-width: 100px;"> <!-- Adjust path and style as needed -->
<h4>Stirling-PDF Login</h4>
</div>
<div th:if="${logoutMessage}" class="alert alert-success" th:text="${logoutMessage}"></div>
<div class="card-body">
<form th:action="@{/login}" method="post">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" class="form-control" required="required" />
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" class="form-control" required="required" />
</div>
<div class="form-group text-center">
<input type="submit" value="Login" class="btn btn-primary" />
</div>
</form>
</div>
<div class="card-footer text-danger text-center">
<div th:if="${error == 'badcredentials'}">
Invalid username or password.
</div>
<div th:if="${error == 'locked'}">
Your account has been locked.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View file

@ -23,6 +23,8 @@
<ul id="selectedFiles" class="list-group"></ul>
</div>
<div class="form-group text-center">
<button type="button" id="sortByNameBtn" class="btn btn-info" th:text="#{merge.sortByName}"></button>
<button type="button" id="sortByDateBtn" class="btn btn-info" th:text="#{merge.sortByDate}"></button>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{merge.submit}"></button>
</div>
</form>