commit
620b954336
57 changed files with 3349 additions and 678 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -116,7 +116,7 @@ watchedFolders/
|
|||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
*.db
|
||||
/build
|
||||
|
||||
/.vscode
|
29
README.md
29
README.md
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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."));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
64
src/main/java/stirling/software/SPDF/model/Authority.java
Normal file
64
src/main/java/stirling/software/SPDF/model/Authority.java
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
50
src/main/java/stirling/software/SPDF/model/Role.java
Normal file
50
src/main/java/stirling/software/SPDF/model/Role.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
124
src/main/java/stirling/software/SPDF/model/User.java
Normal file
124
src/main/java/stirling/software/SPDF/model/User.java
Normal 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(", "));
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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=تغيير الأذونات
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=権限の変更
|
||||
|
|
|
@ -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=권한 변경
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=Изменить разрешения
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=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=改变权限
|
||||
|
|
4
src/main/resources/static/images/clipboard.svg
Normal file
4
src/main/resources/static/images/clipboard.svg
Normal 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 |
5
src/main/resources/static/images/eye-slash.svg
Normal file
5
src/main/resources/static/images/eye-slash.svg
Normal 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 |
4
src/main/resources/static/images/eye.svg
Normal file
4
src/main/resources/static/images/eye.svg
Normal 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 |
|
@ -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>↑</span></button>
|
||||
<button class="btn btn-secondary move-down"><span>↓</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>↑</span></button>
|
||||
<button class="btn btn-secondary move-down"><span>↓</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;
|
||||
}
|
||||
|
|
295
src/main/resources/templates/account.html
Normal file
295
src/main/resources/templates/account.html
Normal 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>
|
73
src/main/resources/templates/addUsers.html
Normal file
73
src/main/resources/templates/addUsers.html
Normal 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>
|
|
@ -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>
|
||||
|
||||
|
|
59
src/main/resources/templates/login.html
Normal file
59
src/main/resources/templates/login.html
Normal 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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue