diff --git a/.github/README.md b/.github/README.md deleted file mode 100644 index 97cb44086..000000000 --- a/.github/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# CI Configuration - -## CI Lite Mode - -Skip non-essential CI workflows by setting a repository variable: - -**Settings → Secrets and variables → Actions → Variables → New repository variable** - -- Name: `CI_PROFILE` -- Value: `lite` - -Skips resource-intensive builds, releases, and OSS-specific workflows. Useful for deployment-only forks or faster CI runs. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a049bb90..a5df3e9ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -262,7 +262,13 @@ jobs: strategy: fail-fast: false matrix: - docker-rev: ["docker/embedded/Dockerfile", "docker/embedded/Dockerfile.ultra-lite", "docker/embedded/Dockerfile.fat"] + include: + - docker-rev: docker/embedded/Dockerfile + artifact-suffix: Dockerfile + - docker-rev: docker/embedded/Dockerfile.ultra-lite + artifact-suffix: Dockerfile.ultra-lite + - docker-rev: docker/embedded/Dockerfile.fat + artifact-suffix: Dockerfile.fat steps: - name: Harden Runner uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 @@ -272,6 +278,13 @@ jobs: - name: Checkout Repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Free disk space on runner + run: | + echo "Disk space before cleanup:" && df -h + sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost + docker system prune -af || true + echo "Disk space after cleanup:" && df -h + - name: Set up JDK 17 uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: @@ -313,7 +326,7 @@ jobs: if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: reports-docker-${{ matrix.docker-rev }} + name: reports-docker-${{ matrix.artifact-suffix }} path: | build/reports/tests/ build/test-results/ diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index f1763e431..acad6f4a9 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -26,6 +26,7 @@ public class RequestUriUtils { || normalizedUri.startsWith("/public/") || normalizedUri.startsWith("/pdfjs/") || normalizedUri.startsWith("/pdfjs-legacy/") + || normalizedUri.startsWith("/pdfium/") || normalizedUri.startsWith("/assets/") || normalizedUri.startsWith("/locales/") || normalizedUri.startsWith("/Login/") @@ -61,7 +62,8 @@ public class RequestUriUtils { || normalizedUri.endsWith(".css") || normalizedUri.endsWith(".mjs") || normalizedUri.endsWith(".html") - || normalizedUri.endsWith(".toml"); + || normalizedUri.endsWith(".toml") + || normalizedUri.endsWith(".wasm"); } public static boolean isFrontendRoute(String contextPath, String requestURI) { @@ -125,11 +127,13 @@ public class RequestUriUtils { || requestURI.endsWith("popularity.txt") || requestURI.endsWith(".js") || requestURI.endsWith(".toml") + || requestURI.endsWith(".wasm") || requestURI.contains("swagger") || requestURI.startsWith("/api/v1/info") || requestURI.startsWith("/site.webmanifest") || requestURI.startsWith("/fonts") - || requestURI.startsWith("/pdfjs")); + || requestURI.startsWith("/pdfjs") + || requestURI.startsWith("/pdfium")); } /** diff --git a/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java index b7c121ab3..409615e68 100644 --- a/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java +++ b/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java @@ -24,6 +24,9 @@ public class RequestUriUtilsTest { assertTrue( RequestUriUtils.isStaticResource("/pdfjs/pdf.worker.js"), "PDF.js files should be static"); + assertTrue( + RequestUriUtils.isStaticResource("/pdfium/pdfium.wasm"), + "PDFium wasm should be static"); assertTrue( RequestUriUtils.isStaticResource("/api/v1/info/status"), "API status should be static"); @@ -110,7 +113,8 @@ public class RequestUriUtilsTest { "/downloads/document.png", "/assets/brand.ico", "/any/path/with/image.svg", - "/deep/nested/folder/icon.png" + "/deep/nested/folder/icon.png", + "/pdfium/pdfium.wasm" }) void testIsStaticResourceWithFileExtensions(String path) { assertTrue( @@ -148,6 +152,9 @@ public class RequestUriUtilsTest { assertFalse( RequestUriUtils.isTrackableResource("/script.js"), "JS files should not be trackable"); + assertFalse( + RequestUriUtils.isTrackableResource("/pdfium/pdfium.wasm"), + "PDFium wasm should not be trackable"); assertFalse( RequestUriUtils.isTrackableResource("/swagger/index.html"), "Swagger files should not be trackable"); @@ -224,7 +231,8 @@ public class RequestUriUtilsTest { "/api/v1/info/health", "/site.webmanifest", "/fonts/roboto.woff", - "/pdfjs/viewer.js" + "/pdfjs/viewer.js", + "/pdfium/pdfium.wasm" }) void testNonTrackableResources(String path) { assertFalse( diff --git a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 514b9231c..c85e78b73 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -66,7 +66,8 @@ public class OpenApiConfig { if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) { server = new Server().url(swaggerServerUrl).description("API Server"); } else { - // Use relative path so Swagger uses the current browser origin to avoid CORS issues when accessing via different ports + // Use relative path so Swagger uses the current browser origin to avoid CORS issues + // when accessing via different ports server = new Server().url("/").description("Current Server"); } openAPI.addServersItem(server); diff --git a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index 0703708f5..8eac8fa80 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -1,10 +1,14 @@ package stirling.software.SPDF.config; +import java.util.concurrent.TimeUnit; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import lombok.RequiredArgsConstructor; @@ -25,6 +29,20 @@ public class WebMvcConfig implements WebMvcConfigurer { registry.addInterceptor(endpointInterceptor); } + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // Cache hashed assets (JS/CSS with content hashes) for 1 year + // These files have names like index-ChAS4tCC.js that change when content changes + registry.addResourceHandler("/assets/**") + .addResourceLocations("classpath:/static/assets/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic()); + + // Don't cache index.html - it needs to be fresh to reference latest hashed assets + registry.addResourceHandler("/index.html") + .addResourceLocations("classpath:/static/") + .setCacheControl(CacheControl.noCache().mustRevalidate()); + } + @Override public void addCorsMappings(CorsRegistry registry) { // Check if running in Tauri mode diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index 2a0cd5734..257d243ea 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -197,7 +197,6 @@ public class SecurityConfiguration { http.csrf(CsrfConfigurer::disable); if (loginEnabledValue) { - boolean v2Enabled = appConfig.v2Enabled(); http.addFilterBefore( userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) @@ -205,19 +204,9 @@ public class SecurityConfiguration { .addFilterBefore(jwtAuthenticationFilter, UserAuthenticationFilter.class); http.sessionManagement( - sessionManagement -> { - if (v2Enabled) { + sessionManagement -> sessionManagement.sessionCreationPolicy( - SessionCreationPolicy.STATELESS); - } else { - sessionManagement - .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) - .maximumSessions(10) - .maxSessionsPreventsLogin(false) - .sessionRegistry(sessionRegistry) - .expiredUrl("/login?logout=true"); - } - }); + SessionCreationPolicy.STATELESS)); http.authenticationProvider(daoAuthenticationProvider()); http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())); @@ -300,18 +289,7 @@ public class SecurityConfiguration { if (securityProperties.isOauth2Active()) { http.oauth2Login( oauth2 -> { - // v1: Use /oauth2 as login page for Thymeleaf templates - if (!v2Enabled) { - oauth2.loginPage("/oauth2"); - } - - // v2: Don't set loginPage, let default OAuth2 flow handle it - oauth2 - /* - This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database. - If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser' - is set as true, else login fails with an error message advising the same. - */ + oauth2.loginPage("/login") .successHandler( new CustomOAuth2AuthenticationSuccessHandler( loginAttemptService, @@ -345,12 +323,8 @@ public class SecurityConfiguration { .saml2Login( saml2 -> { try { - // Only set login page for v1/Thymeleaf mode - if (!v2Enabled) { - saml2.loginPage("/saml2"); - } - - saml2.relyingPartyRegistrationRepository( + saml2.loginPage("/login") + .relyingPartyRegistrationRepository( saml2RelyingPartyRegistrations) .authenticationManager( new ProviderManager(authenticationProvider)) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java index d4ecf8161..3ae079d0f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java @@ -226,7 +226,8 @@ public class EmailService { @Async public void sendPasswordChangedNotification( - String to, String username, String newPassword, String loginUrl) throws MessagingException { + String to, String username, String newPassword, String loginUrl) + throws MessagingException { String subject = "Your Stirling PDF password has been updated"; String passwordSection = diff --git a/build.gradle b/build.gradle index 64908ee4a..90fde88e5 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,8 @@ plugins { } import com.github.jk1.license.render.* +import groovy.json.JsonOutput +import groovy.json.JsonSlurper ext { springBootVersion = "3.5.6" @@ -57,7 +59,7 @@ repositories { allprojects { group = 'stirling.software' - version = '2.1.2' + version = '2.1.3' configurations.configureEach { exclude group: 'commons-logging', module: 'commons-logging' @@ -65,6 +67,51 @@ allprojects { } } +def writeIfChanged(File targetFile, String newContent) { + if (targetFile.getText('UTF-8') != newContent) { + targetFile.write(newContent, 'UTF-8') + } +} + +def updateTauriConfigVersion(String version) { + File tauriConfig = file('frontend/src-tauri/tauri.conf.json') + def parsed = new JsonSlurper().parse(tauriConfig) + parsed.version = version + + def formatted = JsonOutput.prettyPrint(JsonOutput.toJson(parsed)) + System.lineSeparator() + writeIfChanged(tauriConfig, formatted) +} + +def updateSimulationVersion(File fileToUpdate, String version) { + def content = fileToUpdate.getText('UTF-8') + def matcher = content =~ /(appVersion:\s*')([^']*)(')/ + + if (!matcher.find()) { + throw new GradleException("Could not locate appVersion in ${fileToUpdate} for synchronization") + } + + def updatedContent = matcher.replaceFirst("${matcher.group(1)}${version}${matcher.group(3)}") + writeIfChanged(fileToUpdate, updatedContent) +} + +tasks.register('syncAppVersion') { + group = 'versioning' + description = 'Synchronizes app version across desktop and simulation configs.' + + doLast { + def appVersion = project.version.toString() + println "Synchronizing application version to ${appVersion}" + updateTauriConfigVersion(appVersion) + + [ + 'frontend/src/core/testing/serverExperienceSimulations.ts', + 'frontend/src/proprietary/testing/serverExperienceSimulations.ts' + ].each { path -> + updateSimulationVersion(file(path), appVersion) + } + } +} + tasks.register('writeVersion', WriteProperties) { destinationFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties') println "Writing version.properties to ${destinationFile.get().asFile.path}" @@ -314,7 +361,7 @@ tasks.named('bootRun') { tasks.named('build') { group = 'build' description = 'Delegates to :stirling-pdf:bootJar' - dependsOn ':stirling-pdf:bootJar', 'buildRestartHelper' + dependsOn ':stirling-pdf:bootJar', 'buildRestartHelper', 'syncAppVersion' doFirst { println "Delegating to :stirling-pdf:bootJar" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8d90d9658..f788474bb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -105,6 +105,7 @@ "typescript": "^5.9.2", "typescript-eslint": "^8.44.1", "vite": "^7.1.7", + "vite-plugin-static-copy": "^3.1.4", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4" } @@ -11093,6 +11094,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", @@ -14503,6 +14517,25 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-plugin-static-copy": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz", + "integrity": "sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "p-map": "^7.0.3", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.15" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/vite-tsconfig-paths": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5489f6b46..914b7bb1f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -152,6 +152,7 @@ "typescript": "^5.9.2", "typescript-eslint": "^8.44.1", "vite": "^7.1.7", + "vite-plugin-static-copy": "^3.1.4", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4" }, diff --git a/frontend/public/locales/de-DE/translation.toml b/frontend/public/locales/de-DE/translation.toml index 274de764e..d48c424f4 100644 --- a/frontend/public/locales/de-DE/translation.toml +++ b/frontend/public/locales/de-DE/translation.toml @@ -6131,8 +6131,8 @@ tags = "text,anmerkung,beschriftung" applySignatures = "Text anwenden" [addText.text] -name = "Textinhalt" -placeholder = "Geben Sie den hinzuzufügenden Text ein" +name = "Text" +placeholder = "Text eingeben" fontLabel = "Schriftart" fontSizeLabel = "Schriftgröße" fontSizePlaceholder = "Schriftgröße eingeben oder wählen (8-200)" diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index e77d29171..c75f0b65f 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -435,6 +435,24 @@ latestVersion = "Latest Version" checkForUpdates = "Check for Updates" viewDetails = "View Details" +[settings.security] +title = "Security" +description = "Update your password to keep your account secure." + +[settings.security.password] +subtitle = "Change your password. You will be logged out after updating." +required = "All fields are required." +mismatch = "New passwords do not match." +error = "Unable to update password. Please verify your current password and try again." +success = "Password updated successfully. Please sign in again." +current = "Current password" +currentPlaceholder = "Enter your current password" +new = "New password" +newPlaceholder = "Enter a new password" +confirm = "Confirm new password" +confirmPlaceholder = "Re-enter your new password" +update = "Update password" + [settings.hotkeys] title = "Keyboard Shortcuts" description = "Customize keyboard shortcuts for quick tool access. Click \"Change shortcut\" and press a new key combination. Press Esc to cancel." @@ -493,6 +511,10 @@ oldPassword = "Current Password" newPassword = "New Password" confirmNewPassword = "Confirm New Password" submit = "Submit Changes" +credsUpdated = "Account updated" +description = "Changes saved. Please log in again." +error = "Unable to update username. Please verify your password and try again." +changeUsername = "Update your username. You will be logged out after updating." [account] title = "Account Settings" @@ -5072,6 +5094,7 @@ loading = "Loading..." back = "Back" continue = "Continue" error = "Error" +save = "Save" [config.overview] title = "Application Configuration" @@ -5571,6 +5594,28 @@ contactSales = "Contact Sales" contactToUpgrade = "Contact us to upgrade or customize your plan" maxUsers = "Max Users" upTo = "Up to" +getLicense = "Get Server License" +upgradeToEnterprise = "Upgrade to Enterprise" +selectPeriod = "Select Billing Period" +monthlyBilling = "Monthly Billing" +yearlyBilling = "Yearly Billing" +checkoutOpened = "Checkout Opened" +checkoutInstructions = "Complete your purchase in the Stripe tab. After payment, return here and refresh the page to activate your license. You will also receive an email with your license key." +activateLicense = "Activate Your License" + +[plan.static.licenseActivation] +checkoutOpened = "Checkout Opened in New Tab" +instructions = "Complete your purchase in the Stripe tab. Once your payment is complete, you will receive an email with your license key." +enterKey = "Enter your license key below to activate your plan:" +keyDescription = "Paste the license key from your email" +activate = "Activate License" +doLater = "I'll do this later" +success = "License Activated!" +successMessage = "Your license has been successfully activated. You can now close this window." + +[plan.static.billingPortal] +title = "Email Verification Required" +message = "You will need to verify your email address in the Stripe billing portal. Check your email for a login link." [plan.period] month = "month" diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index cb8faf04b..a08587cc1 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -152,15 +152,27 @@ pub fn run() { } #[cfg(target_os = "macos")] RunEvent::Opened { urls } => { + use urlencoding::decode; + add_log(format!("📂 Tauri file opened event: {:?}", urls)); let mut added_files = false; for url in urls { let url_str = url.as_str(); if url_str.starts_with("file://") { - let file_path = url_str.strip_prefix("file://").unwrap_or(url_str); + let encoded_path = url_str.strip_prefix("file://").unwrap_or(url_str); + + // Decode URL-encoded characters (%20 -> space, etc.) + let file_path = match decode(encoded_path) { + Ok(decoded) => decoded.into_owned(), + Err(e) => { + add_log(format!("⚠️ Failed to decode file path: {} - {}", encoded_path, e)); + encoded_path.to_string() // Fallback to encoded path + } + }; + add_log(format!("📂 Processing opened file: {}", file_path)); - add_opened_file(file_path.to_string()); + add_opened_file(file_path); added_files = true; } } diff --git a/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx b/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx index c93a89be0..f3fecba7e 100644 --- a/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx +++ b/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; import { Stack, TextInput, Select, Combobox, useCombobox, Group, Box } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; import { ColorPicker } from '@app/components/annotation/shared/ColorPicker'; interface TextInputWithFontProps { @@ -13,8 +12,12 @@ interface TextInputWithFontProps { textColor?: string; onTextColorChange?: (color: string) => void; disabled?: boolean; - label?: string; - placeholder?: string; + label: string; + placeholder: string; + fontLabel: string; + fontSizeLabel: string; + fontSizePlaceholder: string; + colorLabel?: string; onAnyChange?: () => void; } @@ -30,9 +33,12 @@ export const TextInputWithFont: React.FC = ({ disabled = false, label, placeholder, + fontLabel, + fontSizeLabel, + fontSizePlaceholder, + colorLabel, onAnyChange }) => { - const { t } = useTranslation(); const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString()); const fontSizeCombobox = useCombobox(); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); @@ -66,8 +72,8 @@ export const TextInputWithFont: React.FC = ({ return ( { onTextChange(e.target.value); @@ -79,7 +85,7 @@ export const TextInputWithFont: React.FC = ({ {/* Font Selection */}