Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74a1d83f8d | ||
|
|
7a431f509e | ||
|
|
e1a320ac37 | ||
|
|
f879f5d533 | ||
|
|
e49ca245e5 | ||
|
|
198fc1ced3 |
1
.github/scripts/requirements_sync_readme.in
vendored
1
.github/scripts/requirements_sync_readme.in
vendored
@@ -1 +0,0 @@
|
|||||||
tomlkit
|
|
||||||
10
.github/scripts/requirements_sync_readme.txt
vendored
10
.github/scripts/requirements_sync_readme.txt
vendored
@@ -1,10 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is autogenerated by pip-compile with Python 3.10
|
|
||||||
# by the following command:
|
|
||||||
#
|
|
||||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' '.github\scripts\requirements_sync_readme.in'
|
|
||||||
#
|
|
||||||
tomlkit==0.13.2 \
|
|
||||||
--hash=sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde \
|
|
||||||
--hash=sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79
|
|
||||||
# via -r .github\scripts\requirements_sync_readme.in
|
|
||||||
256
.github/workflows/multiOSReleases.yml
vendored
256
.github/workflows/multiOSReleases.yml
vendored
@@ -9,140 +9,24 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
read_versions:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
|
||||||
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
|
|
||||||
# Get version number
|
|
||||||
- name: Get version number
|
|
||||||
id: versionNumber
|
|
||||||
run: |
|
|
||||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
|
||||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Get version number mac
|
|
||||||
id: versionNumberMac
|
|
||||||
run: |
|
|
||||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
|
||||||
CURRENT_YEAR=$(date +'%Y')
|
|
||||||
IFS='.' read -r -a VERSION_PARTS <<< "$VERSION"
|
|
||||||
MAC_VERSION="$CURRENT_YEAR.${VERSION_PARTS[1]:-0}.${VERSION_PARTS[2]:-0}"
|
|
||||||
echo "versionNumberMac=$MAC_VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
build-portable:
|
|
||||||
needs: read_versions
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
enable_security: [true, false]
|
|
||||||
include:
|
|
||||||
- enable_security: true
|
|
||||||
file_suffix: "with-login-"
|
|
||||||
- enable_security: false
|
|
||||||
file_suffix: ""
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
|
|
||||||
- name: Set up JDK 21
|
|
||||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
|
||||||
with:
|
|
||||||
java-version: "21"
|
|
||||||
distribution: "temurin"
|
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
|
||||||
with:
|
|
||||||
gradle-version: 8.12
|
|
||||||
|
|
||||||
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
|
||||||
run: ./gradlew clean createExe
|
|
||||||
env:
|
|
||||||
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
|
||||||
STIRLING_PDF_DESKTOP_UI: false
|
|
||||||
|
|
||||||
- name: Rename binaries
|
|
||||||
run: |
|
|
||||||
mv ./build/launch4j/Stirling-PDF.exe ./win-Stirling-PDF-portable-Server-${{ matrix.file_suffix }}${{ needs.read_versions.outputs.version }}.exe
|
|
||||||
mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./Stirling-PDF-${{ matrix.file_suffix }}${{ needs.read_versions.outputs.version }}.jar
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
|
||||||
with:
|
|
||||||
retention-days: 1
|
|
||||||
if-no-files-found: error
|
|
||||||
name: stirling-${{ matrix.file_suffix }}binaries
|
|
||||||
path: |
|
|
||||||
./win-Stirling-PDF-portable-Server-${{ matrix.file_suffix }}${{ needs.read_versions.outputs.version }}.exe
|
|
||||||
./Stirling-PDF-${{ matrix.file_suffix }}${{ needs.read_versions.outputs.version }}.jar
|
|
||||||
|
|
||||||
sign_verify-portable:
|
|
||||||
needs: [build-portable, read_versions]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
enable_security: [true, false]
|
|
||||||
include:
|
|
||||||
- enable_security: true
|
|
||||||
file_suffix: "with-login-"
|
|
||||||
- enable_security: false
|
|
||||||
file_suffix: ""
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: Download build artifacts
|
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
|
||||||
with:
|
|
||||||
name: stirling-${{ matrix.file_suffix }}binaries
|
|
||||||
|
|
||||||
- name: Display structure of downloaded files
|
|
||||||
run: ls -R
|
|
||||||
|
|
||||||
- name: Upload signed artifacts
|
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
|
||||||
with:
|
|
||||||
retention-days: 1
|
|
||||||
if-no-files-found: error
|
|
||||||
name: stirling-${{ matrix.file_suffix }}signed
|
|
||||||
path: |
|
|
||||||
./*
|
|
||||||
!cosign.*
|
|
||||||
|
|
||||||
build-installers:
|
build-installers:
|
||||||
needs: read_versions
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
extra: "-installer"
|
platform: win
|
||||||
platform: win-
|
|
||||||
ext: exe
|
ext: exe
|
||||||
# - os: macos-latest
|
#- os: macos-latest
|
||||||
# extra: ""
|
# platform: mac
|
||||||
# platform: mac-
|
# ext: dmg
|
||||||
# ext: dmg
|
#- os: ubuntu-latest
|
||||||
# - os: ubuntu-latest
|
# platform: linux
|
||||||
# extra: ""
|
# ext: deb
|
||||||
# platform: linux-
|
|
||||||
# ext: deb
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
@@ -168,6 +52,24 @@ jobs:
|
|||||||
curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe
|
curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe
|
||||||
.\wix.exe /install /quiet
|
.\wix.exe /install /quiet
|
||||||
|
|
||||||
|
# Install Linux dependencies
|
||||||
|
- name: Install Linux Dependencies
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y fakeroot rpm
|
||||||
|
|
||||||
|
# Get version number
|
||||||
|
- name: Get version number
|
||||||
|
id: versionNumber
|
||||||
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Get version number mac
|
||||||
|
id: versionNumberMac
|
||||||
|
run: echo "versionNumberMac=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
# Build installer
|
# Build installer
|
||||||
- name: Build Installer
|
- name: Build Installer
|
||||||
run: ./gradlew build jpackage -x test --info
|
run: ./gradlew build jpackage -x test --info
|
||||||
@@ -181,107 +83,23 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.os }}" = "windows-latest" ]; then
|
if [ "${{ matrix.os }}" = "windows-latest" ]; then
|
||||||
mv "./build/jpackage/Stirling-PDF-${{ needs.read_versions.outputs.version }}.exe" "${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}"
|
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.exe" "Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}"
|
||||||
elif [ "${{ matrix.os }}" = "macos-latest" ]; then
|
elif [ "${{ matrix.os }}" = "macos-latest" ]; then
|
||||||
mv "./build/jpackage/Stirling-PDF-${{ needs.read_versions.outputs.versionMac }}.dmg" "${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}"
|
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumberMac.outputs.versionNumberMac }}.dmg" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
|
||||||
else
|
else
|
||||||
mv "./build/jpackage/stirling-pdf_${{ needs.read_versions.outputs.version }}-1_amd64.deb" "${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}"
|
mv "build/jpackage/stirling-pdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload build artifacts
|
# Upload installer as artifact for testing
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
- name: Upload Installer Artifact
|
||||||
|
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||||
with:
|
with:
|
||||||
|
name: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
||||||
|
path: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
name: ${{ matrix.platform }}binaries
|
|
||||||
path: |
|
|
||||||
./${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}
|
|
||||||
|
|
||||||
sign_verify:
|
- name: Upload binaries to release
|
||||||
needs: [read_versions, build-installers]
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: windows-latest
|
|
||||||
extra: "-installer"
|
|
||||||
platform: win-
|
|
||||||
ext: exe
|
|
||||||
# - os: macos-latest
|
|
||||||
# extra: ""
|
|
||||||
# platform: mac-
|
|
||||||
# ext: dmg
|
|
||||||
# - os: ubuntu-latest
|
|
||||||
# extra: ""
|
|
||||||
# platform: linux-
|
|
||||||
# ext: deb
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: Download build artifacts
|
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.platform }}binaries
|
|
||||||
|
|
||||||
- name: Display structure of downloaded files
|
|
||||||
run: ls -R
|
|
||||||
|
|
||||||
- name: Install Cosign
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
|
||||||
|
|
||||||
- name: Generate key pair
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
run: cosign generate-key-pair
|
|
||||||
|
|
||||||
- name: Sign and generate attestations
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
run: |
|
|
||||||
cosign sign-blob \
|
|
||||||
--key ./cosign.key \
|
|
||||||
--yes \
|
|
||||||
--output-signature ./${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}.sig \
|
|
||||||
./${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}
|
|
||||||
|
|
||||||
cosign attest-blob \
|
|
||||||
--predicate - \
|
|
||||||
--key ./cosign.key \
|
|
||||||
--yes \
|
|
||||||
--output-attestation ./${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}.intoto.jsonl \
|
|
||||||
./${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}
|
|
||||||
|
|
||||||
cosign verify-blob \
|
|
||||||
--key ./cosign.pub \
|
|
||||||
--signature ./${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}.sig \
|
|
||||||
./${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.${{ matrix.ext }}
|
|
||||||
|
|
||||||
- name: Upload signed artifacts
|
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
|
||||||
with:
|
|
||||||
retention-days: 1
|
|
||||||
if-no-files-found: error
|
|
||||||
name: ${{ matrix.platform }}signed
|
|
||||||
path: |
|
|
||||||
./${{ matrix.platform }}Stirling-PDF${{ matrix.extra }}-${{ needs.read_versions.outputs.version }}.*
|
|
||||||
!cosign.*
|
|
||||||
|
|
||||||
create-release:
|
|
||||||
needs: [read_versions, sign_verify, sign_verify-portable]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Download signed artifacts
|
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
|
||||||
- name: Display structure of downloaded files
|
|
||||||
run: ls -R
|
|
||||||
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
|
||||||
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ needs.read_versions.outputs.version }}
|
files: ./Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
||||||
generate_release_notes: true
|
|
||||||
files: |
|
|
||||||
./*signed/*
|
|
||||||
|
|||||||
159
.github/workflows/releaseArtifacts.yml
vendored
159
.github/workflows/releaseArtifacts.yml
vendored
@@ -9,8 +9,11 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
enable_security: [true, false]
|
enable_security: [true, false]
|
||||||
@@ -19,8 +22,6 @@ jobs:
|
|||||||
file_suffix: "-with-login"
|
file_suffix: "-with-login"
|
||||||
- enable_security: false
|
- enable_security: false
|
||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
outputs:
|
|
||||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
@@ -47,134 +48,38 @@ jobs:
|
|||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
run: |
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
|
||||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Rename binaries
|
- name: Rename binarie
|
||||||
run: |
|
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
mv ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
|
||||||
mv ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
|
||||||
|
|
||||||
- name: Debug build artifacts
|
- name: Upload Assets binarie
|
||||||
run: |
|
|
||||||
echo "Current Directory: $(pwd)"
|
|
||||||
ls -R ./build/libs
|
|
||||||
ls -R ./build/launch4j
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||||
with:
|
with:
|
||||||
name: binaries${{ matrix.file_suffix }}
|
path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
path: |
|
name: Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.*
|
overwrite: true
|
||||||
./build/libs/Stirling-PDF${{ matrix.file_suffix }}.*
|
retention-days: 1
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
sign_verify:
|
- name: Upload binaries to release
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
enable_security: [true, false]
|
|
||||||
include:
|
|
||||||
- enable_security: true
|
|
||||||
file_suffix: "-with-login"
|
|
||||||
- enable_security: false
|
|
||||||
file_suffix: ""
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: Download build artifacts
|
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
|
||||||
with:
|
|
||||||
name: binaries${{ matrix.file_suffix }}
|
|
||||||
- name: Display structure of downloaded files
|
|
||||||
run: ls -R
|
|
||||||
|
|
||||||
- name: Install Cosign
|
|
||||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
|
||||||
|
|
||||||
- name: Generate key pair
|
|
||||||
run: cosign generate-key-pair
|
|
||||||
|
|
||||||
- name: Sign and generate attestations
|
|
||||||
run: |
|
|
||||||
cosign sign-blob \
|
|
||||||
--key ./cosign.key \
|
|
||||||
--yes \
|
|
||||||
--output-signature ./libs/Stirling-PDF${{ matrix.file_suffix }}.jar.sig \
|
|
||||||
./libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
|
||||||
|
|
||||||
cosign attest-blob \
|
|
||||||
--predicate - \
|
|
||||||
--key ./cosign.key \
|
|
||||||
--yes \
|
|
||||||
--output-attestation ./libs/Stirling-PDF${{ matrix.file_suffix }}.jar.intoto.jsonl \
|
|
||||||
./libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
|
||||||
|
|
||||||
cosign verify-blob \
|
|
||||||
--key ./cosign.pub \
|
|
||||||
--signature ./libs/Stirling-PDF${{ matrix.file_suffix }}.jar.sig \
|
|
||||||
./libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
|
||||||
|
|
||||||
cosign sign-blob \
|
|
||||||
--key ./cosign.key \
|
|
||||||
--yes \
|
|
||||||
--output-signature ./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe.sig \
|
|
||||||
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
|
||||||
|
|
||||||
cosign attest-blob \
|
|
||||||
--predicate - \
|
|
||||||
--key ./cosign.key \
|
|
||||||
--yes \
|
|
||||||
--output-attestation ./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe.intoto.jsonl \
|
|
||||||
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
|
||||||
|
|
||||||
cosign verify-blob \
|
|
||||||
--key ./cosign.pub \
|
|
||||||
--signature ./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe.sig \
|
|
||||||
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
|
||||||
|
|
||||||
- name: Upload signed artifacts
|
|
||||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
|
||||||
with:
|
|
||||||
name: signed${{ matrix.file_suffix }}
|
|
||||||
path: |
|
|
||||||
./libs/Stirling-PDF${{ matrix.file_suffix }}.*
|
|
||||||
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.*
|
|
||||||
|
|
||||||
release:
|
|
||||||
needs: [build, sign_verify]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
enable_security: [true, false]
|
|
||||||
include:
|
|
||||||
- enable_security: true
|
|
||||||
file_suffix: "-with-login"
|
|
||||||
- enable_security: false
|
|
||||||
file_suffix: ""
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@c95a14d0e5bab51a9f56296a4eb0e416910cd350 # v2.10.3
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: Download signed artifacts
|
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
|
||||||
with:
|
|
||||||
name: signed${{ matrix.file_suffix }}
|
|
||||||
|
|
||||||
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
|
||||||
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ needs.build.outputs.version }}
|
files: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
generate_release_notes: true
|
|
||||||
files: |
|
- name: Rename jar binaries
|
||||||
./libs/Stirling-PDF*
|
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
./launch4j/Stirling-PDF-Server*
|
|
||||||
|
- name: Upload Assets jar binaries
|
||||||
|
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||||
|
with:
|
||||||
|
path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
name: Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
overwrite: true
|
||||||
|
retention-days: 1
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload jar binaries to release
|
||||||
|
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||||
|
with:
|
||||||
|
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
|||||||
2
.github/workflows/sync_files.yml
vendored
2
.github/workflows/sync_files.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
|
run: pip install tomlkit
|
||||||
- name: Sync README
|
- name: Sync README
|
||||||
run: python scripts/counter_translation.py
|
run: python scripts/counter_translation.py
|
||||||
- name: Set up git config
|
- name: Set up git config
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -14,16 +14,12 @@ local.properties
|
|||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
version.properties
|
version.properties
|
||||||
|
|
||||||
#### Stirling-PDF Files ###
|
|
||||||
pipeline/watchedFolders/
|
pipeline/watchedFolders/
|
||||||
pipeline/finishedFolders/
|
pipeline/finishedFolders/
|
||||||
|
#### Stirling-PDF Files ###
|
||||||
customFiles/
|
customFiles/
|
||||||
configs/
|
configs/
|
||||||
watchedFolders/
|
watchedFolders/
|
||||||
!cucumber/
|
|
||||||
!cucumber/exampleFiles/
|
|
||||||
!cucumber/exampleFiles/example_html.zip
|
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
.gradle
|
.gradle
|
||||||
@@ -115,7 +111,6 @@ watchedFolders/
|
|||||||
*.war
|
*.war
|
||||||
*.nar
|
*.nar
|
||||||
*.ear
|
*.ear
|
||||||
*.zip
|
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
*.db
|
*.db
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ repos:
|
|||||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
files: \.(properties|html|css|js|py|md)$
|
files: \.(properties|html|css|js|py|md)$
|
||||||
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile)
|
||||||
- repo: https://github.com/gitleaks/gitleaks
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
rev: v8.22.0
|
rev: v8.22.0
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.21.2@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099
|
FROM alpine:3.20.3
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ RUN DOCKER_ENABLE_SECURITY=true \
|
|||||||
./gradlew clean build
|
./gradlew clean build
|
||||||
|
|
||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.21.2@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099
|
FROM alpine:3.20.3
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# use alpine
|
# use alpine
|
||||||
FROM alpine:3.21.2@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099
|
FROM alpine:3.21.0@sha256:21dc6063fd678b478f57c0e13f47560d0ea4eeba26dfc947b2a4f81f686b9f45
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ Stirling-PDF currently supports 38 languages!
|
|||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Irish (Gaeilge) (ga_IE) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
@@ -142,7 +142,7 @@ Stirling-PDF currently supports 38 languages!
|
|||||||
| Persian (فارسی) (fa_IR) |  |
|
| Persian (فارسی) (fa_IR) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
| Russian (Русский) (ru_RU) |  |
|
| Russian (Русский) (ru_RU) |  |
|
||||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
@@ -152,7 +152,7 @@ Stirling-PDF currently supports 38 languages!
|
|||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Tibetan (བོད་ཡིག་) (zh_BO) |  |
|
| Tibetan (བོད་ཡིག་) (zh_BO) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ plugins {
|
|||||||
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
||||||
id "io.swagger.swaggerhub" version "1.3.2"
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
id "edu.sc.seis.launch4j" version "3.0.6"
|
id "edu.sc.seis.launch4j" version "3.0.6"
|
||||||
id "com.diffplug.spotless" version "7.0.1"
|
id "com.diffplug.spotless" version "6.25.0"
|
||||||
id "com.github.jk1.dependency-license-report" version "2.9"
|
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||||
//id "nebula.lint" version "19.0.3"
|
//id "nebula.lint" version "19.0.3"
|
||||||
id("org.panteleyev.jpackageplugin") version "1.6.0"
|
id("org.panteleyev.jpackageplugin") version "1.6.0"
|
||||||
@@ -27,7 +27,7 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.37.0"
|
version = "0.36.6"
|
||||||
|
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|||||||
@@ -8,3 +8,4 @@
|
|||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
from behave import given, when, then
|
from behave import given, when, then
|
||||||
from pypdf import PdfWriter, PdfReader
|
from PyPDF2 import PdfWriter, PdfReader
|
||||||
from pypdf.errors import PdfReadError
|
|
||||||
import io
|
import io
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
@@ -346,7 +345,7 @@ def step_check_response_pdf_page_count(context, page_count):
|
|||||||
def step_check_response_zip_file_count(context, file_count):
|
def step_check_response_zip_file_count(context, file_count):
|
||||||
response_file = io.BytesIO(context.response.content)
|
response_file = io.BytesIO(context.response.content)
|
||||||
with zipfile.ZipFile(io.BytesIO(response_file.getvalue())) as zip_file:
|
with zipfile.ZipFile(io.BytesIO(response_file.getvalue())) as zip_file:
|
||||||
actual_file_count = len(zip_file.namelist())
|
actual_file_count = len(zip_file.namelist())
|
||||||
assert actual_file_count == file_count, f"Expected {file_count} files but got {actual_file_count} files"
|
assert actual_file_count == file_count, f"Expected {file_count} files but got {actual_file_count} files"
|
||||||
|
|
||||||
@then('the response ZIP file should contain {doc_count:d} documents each having {pages_per_doc:d} pages')
|
@then('the response ZIP file should contain {doc_count:d} documents each having {pages_per_doc:d} pages')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
behave
|
behave
|
||||||
requests
|
requests
|
||||||
pypdf
|
PyPDF2
|
||||||
reportlab
|
reportlab
|
||||||
PyCryptodome
|
PyCryptodome
|
||||||
|
|||||||
@@ -231,9 +231,9 @@ pycryptodome==3.21.0 \
|
|||||||
--hash=sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297 \
|
--hash=sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297 \
|
||||||
--hash=sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58
|
--hash=sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58
|
||||||
# via -r cucumber\requirements.in
|
# via -r cucumber\requirements.in
|
||||||
pypdf==5.1.0 \
|
pypdf2==3.0.1 \
|
||||||
--hash=sha256:3bd4f503f4ebc58bae40d81e81a9176c400cbbac2ba2d877367595fb524dfdfc \
|
--hash=sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440 \
|
||||||
--hash=sha256:425a129abb1614183fd1aca6982f650b47f8026867c0ce7c4b9f281c443d2740
|
--hash=sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928
|
||||||
# via -r cucumber\requirements.in
|
# via -r cucumber\requirements.in
|
||||||
reportlab==4.2.5 \
|
reportlab==4.2.5 \
|
||||||
--hash=sha256:5cf35b8fd609b68080ac7bbb0ae1e376104f7d5f7b2d3914c7adc63f2593941f \
|
--hash=sha256:5cf35b8fd609b68080ac7bbb0ae1e376104f7d5f7b2d3914c7adc63f2593941f \
|
||||||
@@ -249,10 +249,6 @@ six==1.17.0 \
|
|||||||
# via
|
# via
|
||||||
# behave
|
# behave
|
||||||
# parse-type
|
# parse-type
|
||||||
typing-extensions==4.12.2 \
|
|
||||||
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
|
|
||||||
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
|
|
||||||
# via pypdf
|
|
||||||
urllib3==2.3.0 \
|
urllib3==2.3.0 \
|
||||||
--hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
|
--hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
|
||||||
--hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d
|
--hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# Enables parallel execution of tasks, allowing multiple tasks to run simultaneously
|
|
||||||
org.gradle.parallel=true
|
|
||||||
|
|
||||||
# Enables build caching to reuse outputs from previous builds for faster execution
|
|
||||||
# org.gradle.caching=true
|
|
||||||
|
|
||||||
org.gradle.build-scan=true
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
37
gradlew
vendored
37
gradlew
vendored
@@ -15,8 +15,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
@@ -57,7 +55,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -82,11 +80,13 @@ do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# This is normally unused
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
# shellcheck disable=SC2034
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -133,29 +133,22 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD=java
|
||||||
if ! command -v java >/dev/null 2>&1
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@@ -200,15 +193,11 @@ if "$cygwin" || "$msys" ; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
# Collect all arguments for the java command:
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|||||||
23
gradlew.bat
vendored
23
gradlew.bat
vendored
@@ -13,8 +13,6 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@@ -28,7 +26,6 @@ if "%OS%"=="Windows_NT" setlocal
|
|||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@@ -45,11 +42,11 @@ set JAVA_EXE=java.exe
|
|||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
@@ -59,11 +56,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo. 1>&2
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ public class InstallationPathConfig {
|
|||||||
// Pipeline paths
|
// Pipeline paths
|
||||||
private static final String PIPELINE_WATCHED_FOLDERS_PATH;
|
private static final String PIPELINE_WATCHED_FOLDERS_PATH;
|
||||||
private static final String PIPELINE_FINISHED_FOLDERS_PATH;
|
private static final String PIPELINE_FINISHED_FOLDERS_PATH;
|
||||||
private static final String PIPELINE_DEFAULT_WEB_UI_CONFIGS;
|
|
||||||
|
|
||||||
// Custom file paths
|
// Custom file paths
|
||||||
private static final String STATIC_PATH;
|
private static final String STATIC_PATH;
|
||||||
@@ -46,7 +45,6 @@ public class InstallationPathConfig {
|
|||||||
// Initialize pipeline paths
|
// Initialize pipeline paths
|
||||||
PIPELINE_WATCHED_FOLDERS_PATH = PIPELINE_PATH + "watchedFolders" + File.separator;
|
PIPELINE_WATCHED_FOLDERS_PATH = PIPELINE_PATH + "watchedFolders" + File.separator;
|
||||||
PIPELINE_FINISHED_FOLDERS_PATH = PIPELINE_PATH + "finishedFolders" + File.separator;
|
PIPELINE_FINISHED_FOLDERS_PATH = PIPELINE_PATH + "finishedFolders" + File.separator;
|
||||||
PIPELINE_DEFAULT_WEB_UI_CONFIGS = PIPELINE_PATH + "defaultWebUIConfigs" + File.separator;
|
|
||||||
|
|
||||||
// Initialize custom file paths
|
// Initialize custom file paths
|
||||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||||
@@ -120,10 +118,6 @@ public class InstallationPathConfig {
|
|||||||
return PIPELINE_FINISHED_FOLDERS_PATH;
|
return PIPELINE_FINISHED_FOLDERS_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getPipelineDefaultWebUIConfigsDir() {
|
|
||||||
return PIPELINE_DEFAULT_WEB_UI_CONFIGS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getStaticPath() {
|
public static String getStaticPath() {
|
||||||
return STATIC_PATH;
|
return STATIC_PATH;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,16 +329,12 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public boolean isUsernameValid(String username) {
|
public boolean isUsernameValid(String username) {
|
||||||
// Checks whether the simple username is formatted correctly
|
// Checks whether the simple username is formatted correctly
|
||||||
// Regular expression for user name: Min. 3 characters, max. 50 characters
|
|
||||||
boolean isValidSimpleUsername =
|
boolean isValidSimpleUsername =
|
||||||
username.matches("^[a-zA-Z0-9](?!.*[-@._+]{2,})[a-zA-Z0-9@._+-]{1,48}[a-zA-Z0-9]$");
|
username.matches("^[a-zA-Z0-9][a-zA-Z0-9@._+-]*[a-zA-Z0-9]$");
|
||||||
|
|
||||||
// Checks whether the email address is formatted correctly
|
// Checks whether the email address is formatted correctly
|
||||||
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
|
|
||||||
boolean isValidEmail =
|
boolean isValidEmail =
|
||||||
username.matches(
|
username.matches(
|
||||||
"^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\\\\.[A-Za-z0-9-]+)*(?:\\\\.[A-Za-z]{2,})$");
|
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
|
||||||
|
|
||||||
List<String> notAllowedUserList = new ArrayList<>();
|
List<String> notAllowedUserList = new ArrayList<>();
|
||||||
notAllowedUserList.add("ALL_USERS".toLowerCase());
|
notAllowedUserList.add("ALL_USERS".toLowerCase());
|
||||||
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
|
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package stirling.software.SPDF.config.security.database;
|
package stirling.software.SPDF.config.security.database;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
@@ -11,7 +9,6 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
@@ -20,8 +17,8 @@ import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class DatabaseConfig {
|
public class DatabaseConfig {
|
||||||
|
|
||||||
public final String DATASOURCE_DEFAULT_URL;
|
public static final String DATASOURCE_DEFAULT_URL =
|
||||||
|
"jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
|
||||||
public static final String DATASOURCE_URL_TEMPLATE = "jdbc:%s://%s:%4d/%s";
|
public static final String DATASOURCE_URL_TEMPLATE = "jdbc:%s://%s:%4d/%s";
|
||||||
public static final String DEFAULT_DRIVER = "org.h2.Driver";
|
public static final String DEFAULT_DRIVER = "org.h2.Driver";
|
||||||
public static final String DEFAULT_USERNAME = "sa";
|
public static final String DEFAULT_USERNAME = "sa";
|
||||||
@@ -33,7 +30,6 @@ public class DatabaseConfig {
|
|||||||
public DatabaseConfig(
|
public DatabaseConfig(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@Qualifier("runningEE") boolean runningEE) {
|
@Qualifier("runningEE") boolean runningEE) {
|
||||||
DATASOURCE_DEFAULT_URL = "jdbc:h2:file:" + InstallationPathConfig.getConfigPath() + File.separator + "stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.runningEE = runningEE;
|
this.runningEE = runningEE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import org.springframework.jdbc.datasource.init.ScriptException;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.exception.BackupNotFoundException;
|
import stirling.software.SPDF.model.exception.BackupNotFoundException;
|
||||||
@@ -38,14 +37,12 @@ public class DatabaseService implements DatabaseInterface {
|
|||||||
|
|
||||||
public static final String BACKUP_PREFIX = "backup_";
|
public static final String BACKUP_PREFIX = "backup_";
|
||||||
public static final String SQL_SUFFIX = ".sql";
|
public static final String SQL_SUFFIX = ".sql";
|
||||||
private final Path BACKUP_DIR;
|
private static final String BACKUP_DIR = "configs/db/backup/";
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
|
|
||||||
public DatabaseService(ApplicationProperties applicationProperties, DataSource dataSource) {
|
public DatabaseService(ApplicationProperties applicationProperties, DataSource dataSource) {
|
||||||
this.BACKUP_DIR =
|
|
||||||
Paths.get(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
}
|
}
|
||||||
@@ -58,9 +55,9 @@ public class DatabaseService implements DatabaseInterface {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean hasBackup() {
|
public boolean hasBackup() {
|
||||||
createBackupDirectory();
|
Path filePath = Paths.get(BACKUP_DIR);
|
||||||
|
|
||||||
if (Files.exists(BACKUP_DIR)) {
|
if (Files.exists(filePath)) {
|
||||||
return !getBackupList().isEmpty();
|
return !getBackupList().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +74,11 @@ public class DatabaseService implements DatabaseInterface {
|
|||||||
List<FileInfo> backupFiles = new ArrayList<>();
|
List<FileInfo> backupFiles = new ArrayList<>();
|
||||||
|
|
||||||
if (isH2Database()) {
|
if (isH2Database()) {
|
||||||
createBackupDirectory();
|
Path backupPath = Paths.get(BACKUP_DIR);
|
||||||
|
|
||||||
try (DirectoryStream<Path> stream =
|
try (DirectoryStream<Path> stream =
|
||||||
Files.newDirectoryStream(
|
Files.newDirectoryStream(
|
||||||
BACKUP_DIR,
|
backupPath,
|
||||||
path ->
|
path ->
|
||||||
path.getFileName().toString().startsWith(BACKUP_PREFIX)
|
path.getFileName().toString().startsWith(BACKUP_PREFIX)
|
||||||
&& path.getFileName()
|
&& path.getFileName()
|
||||||
@@ -113,17 +110,6 @@ public class DatabaseService implements DatabaseInterface {
|
|||||||
return backupFiles;
|
return backupFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createBackupDirectory() {
|
|
||||||
if (!Files.exists(BACKUP_DIR)) {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(BACKUP_DIR);
|
|
||||||
log.debug("create backup directory: {}", BACKUP_DIR);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error create backup directory: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void importDatabase() {
|
public void importDatabase() {
|
||||||
if (!hasBackup()) throw new BackupNotFoundException("No backup scripts were found.");
|
if (!hasBackup()) throw new BackupNotFoundException("No backup scripts were found.");
|
||||||
@@ -269,8 +255,7 @@ public class DatabaseService implements DatabaseInterface {
|
|||||||
* @return the <code>Path</code> object for the given file name
|
* @return the <code>Path</code> object for the given file name
|
||||||
*/
|
*/
|
||||||
public Path getBackupFilePath(String fileName) {
|
public Path getBackupFilePath(String fileName) {
|
||||||
createBackupDirectory();
|
Path filePath = Paths.get(BACKUP_DIR, fileName).normalize();
|
||||||
Path filePath = BACKUP_DIR.resolve(fileName).normalize();
|
|
||||||
if (!filePath.startsWith(BACKUP_DIR)) {
|
if (!filePath.startsWith(BACKUP_DIR)) {
|
||||||
throw new SecurityException("Path traversal detected");
|
throw new SecurityException("Path traversal detected");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ public class GeneralWebController {
|
|||||||
model.addAttribute("currentPage", "pipeline");
|
model.addAttribute("currentPage", "pipeline");
|
||||||
List<String> pipelineConfigs = new ArrayList<>();
|
List<String> pipelineConfigs = new ArrayList<>();
|
||||||
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||||
if (new File(InstallationPathConfig.getPipelineDefaultWebUIConfigsDir()).exists()) {
|
if (new File("./pipeline/defaultWebUIConfigs/").exists()) {
|
||||||
try (Stream<Path> paths = Files.walk(Paths.get(InstallationPathConfig.getPipelineDefaultWebUIConfigsDir()))) {
|
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
|
||||||
List<Path> jsonFiles =
|
List<Path> jsonFiles =
|
||||||
paths.filter(Files::isRegularFile)
|
paths.filter(Files::isRegularFile)
|
||||||
.filter(p -> p.toString().endsWith(".json"))
|
.filter(p -> p.toString().endsWith(".json"))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -82,7 +82,7 @@ pages=頁面
|
|||||||
loading=載入中...
|
loading=載入中...
|
||||||
addToDoc=新增至文件
|
addToDoc=新增至文件
|
||||||
reset=重設
|
reset=重設
|
||||||
apply=套用
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=隱私權政策
|
legal.privacy=隱私權政策
|
||||||
legal.terms=使用條款
|
legal.terms=使用條款
|
||||||
@@ -249,7 +249,7 @@ database.backupCreated=資料庫備份成功
|
|||||||
database.fileNotFound=找不到檔案
|
database.fileNotFound=找不到檔案
|
||||||
database.fileNullOrEmpty=檔案不得為空或空白
|
database.fileNullOrEmpty=檔案不得為空或空白
|
||||||
database.failedImportFile=匯入檔案失敗
|
database.failedImportFile=匯入檔案失敗
|
||||||
database.notSupported=您的資料庫連線不支援此功能。
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=您的工作階段已過期。請重新整理頁面並再試一次。
|
session.expired=您的工作階段已過期。請重新整理頁面並再試一次。
|
||||||
session.refreshPage=重新整理頁面
|
session.refreshPage=重新整理頁面
|
||||||
@@ -278,7 +278,7 @@ home.split.desc=將 PDF 分割為多個文件
|
|||||||
split.tags=頁面操作,劃分,多頁,剪下,伺服器端
|
split.tags=頁面操作,劃分,多頁,剪下,伺服器端
|
||||||
|
|
||||||
home.rotate.title=旋轉
|
home.rotate.title=旋轉
|
||||||
home.rotate.desc=輕鬆旋轉您的 PDF。
|
home.rotate.desc=輕鬆旋轉你的 PDF。
|
||||||
rotate.tags=伺服器端
|
rotate.tags=伺服器端
|
||||||
|
|
||||||
|
|
||||||
@@ -300,24 +300,24 @@ home.addImage.desc=在 PDF 的指定位置新增圖片
|
|||||||
addImage.tags=img,jpg,圖片,照片
|
addImage.tags=img,jpg,圖片,照片
|
||||||
|
|
||||||
home.watermark.title=新增浮水印
|
home.watermark.title=新增浮水印
|
||||||
home.watermark.desc=在您的 PDF 檔案中新增自訂浮水印。
|
home.watermark.desc=在你的 PDF 檔案中新增自訂浮水印。
|
||||||
watermark.tags=文字,重複,標籤,自有,版權,商標,img,jpg,圖片,照片
|
watermark.tags=文字,重複,標籤,自有,版權,商標,img,jpg,圖片,照片
|
||||||
|
|
||||||
home.permissions.title=修改權限
|
home.permissions.title=修改權限
|
||||||
home.permissions.desc=修改您的 PDF 檔案權限
|
home.permissions.desc=修改你的 PDF 檔案權限
|
||||||
permissions.tags=讀取,寫入,編輯,列印
|
permissions.tags=讀取,寫入,編輯,列印
|
||||||
|
|
||||||
|
|
||||||
home.removePages.title=移除
|
home.removePages.title=移除
|
||||||
home.removePages.desc=從您的 PDF 檔案中刪除不需要的頁面。
|
home.removePages.desc=從你的 PDF 檔案中刪除不需要的頁面。
|
||||||
removePages.tags=移除頁面,刪除頁面
|
removePages.tags=移除頁面,刪除頁面
|
||||||
|
|
||||||
home.addPassword.title=新增密碼
|
home.addPassword.title=新增密碼
|
||||||
home.addPassword.desc=用密碼加密您的 PDF 檔案。
|
home.addPassword.desc=用密碼加密你的 PDF 檔案。
|
||||||
addPassword.tags=安全,安全性
|
addPassword.tags=安全,安全性
|
||||||
|
|
||||||
home.removePassword.title=移除密碼
|
home.removePassword.title=移除密碼
|
||||||
home.removePassword.desc=從您的 PDF 檔案中移除密碼保護。
|
home.removePassword.desc=從你的 PDF 檔案中移除密碼保護。
|
||||||
removePassword.tags=安全,解密,安全性,取消密碼,刪除密碼
|
removePassword.tags=安全,解密,安全性,取消密碼,刪除密碼
|
||||||
|
|
||||||
home.compressPdfs.title=壓縮
|
home.compressPdfs.title=壓縮
|
||||||
@@ -476,9 +476,9 @@ home.autoRedact.title=自動塗黑
|
|||||||
home.autoRedact.desc=根據輸入的文字自動塗黑 PDF 中的文字
|
home.autoRedact.desc=根據輸入的文字自動塗黑 PDF 中的文字
|
||||||
autoRedact.tags=塗黑,隱藏,塗黑,黑色,標記,隱藏
|
autoRedact.tags=塗黑,隱藏,塗黑,黑色,標記,隱藏
|
||||||
|
|
||||||
home.redact.title=手動塗黑
|
home.redact.title=Manual Redaction
|
||||||
home.redact.desc=依據選取的文字、繪製的形狀和選取的頁面塗黑 PDF
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
redact.tags=塗黑,隱藏,黑色標記,黑色,標記,隱藏,手動
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF 轉 CSV
|
home.tableExtraxt.title=PDF 轉 CSV
|
||||||
home.tableExtraxt.desc=從 PDF 中提取表格並將其轉換為 CSV
|
home.tableExtraxt.desc=從 PDF 中提取表格並將其轉換為 CSV
|
||||||
@@ -556,21 +556,21 @@ login.header=登入
|
|||||||
login.signin=登入
|
login.signin=登入
|
||||||
login.rememberme=記住我
|
login.rememberme=記住我
|
||||||
login.invalid=使用者名稱或密碼無效。
|
login.invalid=使用者名稱或密碼無效。
|
||||||
login.locked=您的帳號已被鎖定。
|
login.locked=你的帳戶已被鎖定。
|
||||||
login.signinTitle=請登入
|
login.signinTitle=請登入
|
||||||
login.ssoSignIn=透過 SSO 單一登入
|
login.ssoSignIn=透過織網單一簽入
|
||||||
login.oauth2AutoCreateDisabled=OAuth 2.0 自動建立使用者功能已停用
|
login.oauth2AutoCreateDisabled=OAuth 2.0 自動建立使用者功能已停用
|
||||||
login.oauth2AdminBlockedUser=目前不允許未註冊的使用者註冊或登入。請聯絡系統管理員。
|
login.oauth2AdminBlockedUser=目前已封鎖非註冊使用者的註冊或登入。請聯絡系統管理員。
|
||||||
login.oauth2RequestNotFound=找不到驗證請求
|
login.oauth2RequestNotFound=找不到驗證請求
|
||||||
login.oauth2InvalidUserInfoResponse=使用者資訊回應無效
|
login.oauth2InvalidUserInfoResponse=無效的使用者資訊回應
|
||||||
login.oauth2invalidRequest=請求無效
|
login.oauth2invalidRequest=無效的回應
|
||||||
login.oauth2AccessDenied=存取被拒
|
login.oauth2AccessDenied=存取被拒
|
||||||
login.oauth2InvalidTokenResponse=無效的權杖回應
|
login.oauth2InvalidTokenResponse=無效的權杖回應
|
||||||
login.oauth2InvalidIdToken=無效的身分權杖
|
login.oauth2InvalidIdToken=無效的識別權杖
|
||||||
login.relyingPartyRegistrationNotFound=找不到任何信賴方註冊紀錄
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=使用者已停用,目前此使用者無法登入。請聯絡系統管理員。
|
login.userIsDisabled=使用者已停用,目前此使用者無法登入。請聯絡系統管理員。
|
||||||
login.alreadyLoggedIn=您已經登入了
|
login.alreadyLoggedIn=您已經登入了
|
||||||
login.alreadyLoggedIn2=部裝置。請先從這些裝置登出後再試一次。
|
login.alreadyLoggedIn2=個裝置。請登出其他裝置後再試一次。
|
||||||
login.toManySessions=您有太多使用中的工作階段
|
login.toManySessions=您有太多使用中的工作階段
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -586,30 +586,30 @@ autoRedact.convertPDFToImageLabel=將 PDF 轉換為 PDF-影像(用於移除方
|
|||||||
autoRedact.submitButton=送出
|
autoRedact.submitButton=送出
|
||||||
|
|
||||||
#redact
|
#redact
|
||||||
redact.title=手動塗黑
|
redact.title=Manual Redaction
|
||||||
redact.header=手動塗黑
|
redact.header=Manual Redaction
|
||||||
redact.submit=塗黑
|
redact.submit=Redact
|
||||||
redact.textBasedRedaction=以文字為基礎的塗黑
|
redact.textBasedRedaction=Text based Redaction
|
||||||
redact.pageBasedRedaction=以頁面為基礎的塗黑
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
redact.convertPDFToImageLabel=將 PDF 轉換為 PDF 影像(用於移除黑框後的文字)
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
redact.pageRedactionNumbers.title=頁面
|
redact.pageRedactionNumbers.title=Pages
|
||||||
redact.pageRedactionNumbers.placeholder=(例如 1,2,8 或 4,7,12-16 或 2n-1)
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
redact.redactionColor.title=塗黑顏色
|
redact.redactionColor.title=Redaction Color
|
||||||
redact.export=匯出
|
redact.export=Export
|
||||||
redact.upload=上傳
|
redact.upload=Upload
|
||||||
redact.boxRedaction=框選區域塗黑
|
redact.boxRedaction=Box draw redaction
|
||||||
redact.zoom=縮放
|
redact.zoom=Zoom
|
||||||
redact.zoomIn=放大
|
redact.zoomIn=Zoom in
|
||||||
redact.zoomOut=縮小
|
redact.zoomOut=Zoom out
|
||||||
redact.nextPage=下一頁
|
redact.nextPage=Next Page
|
||||||
redact.previousPage=上一頁
|
redact.previousPage=Previous Page
|
||||||
redact.toggleSidebar=切換側邊欄
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
redact.showThumbnails=顯示縮圖
|
redact.showThumbnails=Show Thumbnails
|
||||||
redact.showDocumentOutline=顯示文件大綱(按兩下可展開/摺疊所有項目)
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
redact.showAttatchments=顯示附件
|
redact.showAttatchments=Show Attachments
|
||||||
redact.showLayers=顯示圖層(按兩下可將所有圖層重設為預設狀態)
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
redact.colourPicker=顏色選擇器
|
redact.colourPicker=Colour Picker
|
||||||
redact.findCurrentOutlineItem=尋找目前的大綱項目
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=顯示 JavaScript
|
showJS.title=顯示 JavaScript
|
||||||
@@ -778,15 +778,15 @@ scalePages.submit=送出
|
|||||||
|
|
||||||
#certSign
|
#certSign
|
||||||
certSign.title=憑證簽章
|
certSign.title=憑證簽章
|
||||||
certSign.header=使用您的憑證簽章(進行中)
|
certSign.header=使用你的憑證簽章(進行中)
|
||||||
certSign.selectPDF=選擇要簽章的 PDF 檔案:
|
certSign.selectPDF=選擇要簽章的 PDF 檔案:
|
||||||
certSign.jksNote=注意:如果您的證書類型未被列在下方,請使用 keytool 命令列工具將其轉換為 Java Keystore (.jks) 檔案格式,然後選擇下面的 .jks 檔案選項。
|
certSign.jksNote=注意:如果你的證書類型未被列在下方,請使用 keytool 命令列工具將其轉換為 Java Keystore (.jks) 檔案格式,然後選擇下面的 .jks 檔案選項。
|
||||||
certSign.selectKey=選擇您的私鑰檔案(PKCS#8 格式,副檔名可能是 .pem 或 .der):
|
certSign.selectKey=選擇你的私鑰檔案(PKCS#8 格式,副檔名可能是 .pem 或 .der):
|
||||||
certSign.selectCert=選擇您的憑證檔案(X.509 格式,副檔名可能是 .pem 或 .der):
|
certSign.selectCert=選擇你的憑證檔案(X.509 格式,副檔名可能是 .pem 或 .der):
|
||||||
certSign.selectP12=選擇您的 PKCS#12 金鑰庫檔案(副檔名可能是 .p12 或 .pfx)(選填,如果有提供,則它應該包含您的私鑰和憑證):
|
certSign.selectP12=選擇你的 PKCS#12 金鑰庫檔案(副檔名可能是 .p12 或 .pfx)(選填,如果有提供,則它應該包含你的私鑰和憑證):
|
||||||
certSign.selectJKS=選擇您的 Java Keystore 檔案 (副檔名可能是 .jks 或 .keystore):
|
certSign.selectJKS=選擇你的 Java Keystore 檔案 (副檔名可能是 .jks 或 .keystore):
|
||||||
certSign.certType=憑證類型
|
certSign.certType=憑證類型
|
||||||
certSign.password=輸入您的金鑰庫或私鑰密碼(如果有的話):
|
certSign.password=輸入你的金鑰庫或私鑰密碼(如果有的話):
|
||||||
certSign.showSig=顯示簽章
|
certSign.showSig=顯示簽章
|
||||||
certSign.reason=原因
|
certSign.reason=原因
|
||||||
certSign.location=位置
|
certSign.location=位置
|
||||||
@@ -862,7 +862,7 @@ sign.first=第一頁
|
|||||||
sign.last=最後一頁
|
sign.last=最後一頁
|
||||||
sign.next=下一頁
|
sign.next=下一頁
|
||||||
sign.previous=上一頁
|
sign.previous=上一頁
|
||||||
sign.maintainRatio=切換維持長寬比
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
@@ -1319,8 +1319,8 @@ splitByChapters.submit=分割 PDF
|
|||||||
fileChooser.click=點選
|
fileChooser.click=點選
|
||||||
fileChooser.or=或
|
fileChooser.or=或
|
||||||
fileChooser.dragAndDrop=拖放檔案
|
fileChooser.dragAndDrop=拖放檔案
|
||||||
fileChooser.dragAndDropPDF=拖放 PDF 檔案
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
fileChooser.dragAndDropImage=拖放圖片檔案
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=將檔案拖放至此
|
fileChooser.hoveredDragAndDrop=將檔案拖放至此
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>rename</title>
|
|
||||||
<symbol id="icon-rename" viewBox="0 0 512 512">
|
|
||||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="Combined-Shape" fill="currentColor" transform="translate(42.666667, 64.000000)">
|
|
||||||
<path d="M362.666667,1.42108547e-14 L362.666667,21.3333333 L320,21.333 L320,362.666 L362.666667,362.666667 L362.666667,384 L320,383.999 L320,384 L298.666667,384 L298.666,383.999 L256,384 L256,362.666667 L298.666,362.666 L298.666,21.333 L256,21.3333333 L256,1.42108547e-14 L362.666667,1.42108547e-14 Z M426.666667,64 L426.666667,320 L341.333333,320 L341.333333,277.333333 L384,277.333333 L384,106.666667 L341.333333,106.666667 L341.333333,64 L426.666667,64 Z M277.333333,64 L277.333333,320 L3.55271368e-14,320 L3.55271368e-14,64 L277.333333,64 Z M179.2,89.6 L149.333333,89.6 L149.333333,234.666667 C149.333333,248 148.5,256.333333 147.875,264.354167 L147.792993,265.422171 L147.792993,265.422171 L147.714003,266.48894 C147.417695,270.579012 147.2,274.696296 147.2,279.466667 L147.2,279.466667 L177.066667,279.466667 L177.066667,260.266667 C184.941497,273.926888 199.708077,282.130544 215.466667,281.6 C229.540046,281.805757 242.921593,275.508559 251.733333,264.533333 C263.162478,248.989677 269.832496,230.461848 270.933333,211.2 C270.933333,170.666667 249.6,142.933333 217.6,142.933333 C202.507405,142.999748 188.308689,150.099106 179.2,162.133333 L179.2,162.133333 L179.2,89.6 Z M119.466667,162.133333 C107.961824,149.843793 91.4322333,143.546807 74.6666667,145.066667 C57.6785115,144.485924 40.8138255,148.15216 25.6,155.733333 L25.6,155.733333 L34.1333333,177.066667 C45.3979052,171.147831 57.7246848,167.522308 70.4,166.4 C78.5613135,165.511423 86.6853595,168.371259 92.4903835,174.176283 C98.2954074,179.981307 101.155244,188.105353 100.266667,196.266667 L100.266667,196.266667 L100.266667,198.4 L78.9333333,198.4 C65.8181975,197.679203 52.705771,199.864608 40.5333333,204.8 C26.2806563,210.950309 17.6507691,225.621117 19.2,241.066667 C19.0625857,252.057651 23.6679763,262.574827 31.8381493,269.927982 C40.0083223,277.281138 50.9508304,280.757072 61.8666667,279.466667 C77.2795695,280.291768 92.2192911,274.001359 102.4,262.4 L102.4,262.4 L102.4,277.333333 L130.133333,277.333333 C128.292479,266.054406 127.577851,254.620365 128,243.2 L128,243.2 L128,204.8 C129.999138,190.023932 126.995128,175.003882 119.466667,162.133333 Z M98.1333333,213.333333 L98.1333333,238.933333 C92.082572,249.988391 80.836024,257.218314 68.2666667,258.133333 C63.0655139,258.520242 57.9538681,256.621996 54.2659359,252.934064 C50.5780036,249.246132 48.6797582,244.134486 49.0666667,238.933333 C49.0666667,224 59.7333333,215.466667 85.3333333,213.333333 L85.3333333,213.333333 L98.1333333,213.333333 Z M209.066667,166.4 C226.133333,166.4 238.933333,183.466667 238.933333,211.2 C238.933333,238.933333 228.266667,256 211.2,256 C197.298049,255.69869 184.825037,247.383349 179.2,234.666667 L179.2,234.666667 L179.2,187.733333 C185.154203,176.240507 196.263981,168.304951 209.066667,166.4 Z">
|
|
||||||
</path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -39,3 +39,4 @@ document.getElementById("cacheInputs").addEventListener("change", function () {
|
|||||||
cacheInputs = this.checked ? "enabled" : "disabled";
|
cacheInputs = this.checked ? "enabled" : "disabled";
|
||||||
localStorage.setItem("cacheInputs", cacheInputs);
|
localStorage.setItem("cacheInputs", cacheInputs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -104,14 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
jQuery.validator.addMethod("usernamePattern", function(value, element) {
|
jQuery.validator.addMethod("usernamePattern", function(value, element) {
|
||||||
// Regular expression for user name: Min. 3 characters, max. 50 characters
|
return this.optional(element) || /^[a-zA-Z0-9][a-zA-Z0-9@._+-]*[a-zA-Z0-9]$|^(?=.{1,64}@)[A-Za-z0-9]+(\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(\.[A-Za-z]{2,})$/.test(value);
|
||||||
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
|
|
||||||
|
|
||||||
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
|
|
||||||
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
|
|
||||||
|
|
||||||
// Check if the field is optional or meets the requirements
|
|
||||||
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
|
|
||||||
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$.validator.addMethod("passwordMatch", function(value, element) {
|
$.validator.addMethod("passwordMatch", function(value, element) {
|
||||||
@@ -315,36 +308,37 @@
|
|||||||
|
|
||||||
document.getElementById('syncToAccount').addEventListener('click', async function() {
|
document.getElementById('syncToAccount').addEventListener('click', async function() {
|
||||||
/*<![CDATA[*/
|
/*<![CDATA[*/
|
||||||
const urlUpdateUserSettings = /*[[@{/api/v1/user/updateUserSettings}]]*/ "/api/v1/user/updateUserSettings";
|
const urlUpdateUserSettings = /*[[@{/api/v1/user/updateUserSettings}]]*/ "/api/v1/user/updateUserSettings";
|
||||||
/*]]>*/
|
/*]]>*/
|
||||||
|
|
||||||
let settings = {};
|
let settings = {};
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
const key = localStorage.key(i);
|
const key = localStorage.key(i);
|
||||||
if(key !== 'debug' && key !== '0' && key !== '1' && !key.includes('pdfjs') && !key.includes('posthog') && !key.includes('pageViews')) {
|
if(key !== 'debug' && key !== '0' && key !== '1' && !key.includes('pdfjs') && !key.includes('posthog') && !key.includes('pageViews')) {
|
||||||
settings[key] = localStorage.getItem(key);
|
settings[key] = localStorage.getItem(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await window.fetchWithCsrf(urlUpdateUserSettings, {
|
const response = await window.fetchWithCsrf(urlUpdateUserSettings, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(settings)
|
body: JSON.stringify(settings)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error syncing settings to account');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error syncing settings to account');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
alert('Error syncing settings to account');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error syncing settings to account');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<div class="mb-3 mt-4 text-center">
|
<div class="mb-3 mt-4 text-center">
|
||||||
|
|||||||
@@ -207,14 +207,7 @@
|
|||||||
|
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
jQuery.validator.addMethod("usernamePattern", function(value, element) {
|
jQuery.validator.addMethod("usernamePattern", function(value, element) {
|
||||||
// Regular expression for user name: Min. 3 characters, max. 50 characters
|
return this.optional(element) || /^[a-zA-Z0-9][a-zA-Z0-9@._+-]*[a-zA-Z0-9]$|^(?=.{1,64}@)[A-Za-z0-9]+(\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(\.[A-Za-z]{2,})$/.test(value);
|
||||||
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
|
|
||||||
|
|
||||||
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
|
|
||||||
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
|
|
||||||
|
|
||||||
// Check if the field is optional or meets the requirements
|
|
||||||
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
|
|
||||||
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
<!-- powered by section -->
|
<!-- powered by section -->
|
||||||
<div class="footer-powered-by">
|
<div class="footer-powered-by">
|
||||||
<span th:text="#{poweredBy}"></span>
|
<span th:text="#{poweredBy}"></span>
|
||||||
<a href="https://stirlingpdf.com" class="stirling-link">Stirling PDF</a>
|
<a href="https://stirlingpdf.com" class="stirling-link">Stirling PDF</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -82,62 +82,61 @@
|
|||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-single-page', 'looks_one', 'home.PdfToSinglePage.title', 'home.PdfToSinglePage.desc', 'PdfToSinglePage.tags', 'organize')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-single-page', 'looks_one', 'home.PdfToSinglePage.title', 'home.PdfToSinglePage.desc', 'PdfToSinglePage.tags', 'organize')}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="stacked" class="navbar-item col-lg-2 col-sm-6 py px-xl-1 px-2"
|
<div id="stacked" class="navbar-item col-lg-2 col-sm-6 py px-xl-1 px-2"style="display:flex; flex-direction: column;">
|
||||||
style="display:flex; flex-direction: column;">
|
<!-- Convert to PDF menu items -->
|
||||||
<!-- Convert to PDF menu items -->
|
<div class="navbar-item py px-xl-1 px-2" style="margin-bottom: 1rem;">
|
||||||
<div class="navbar-item py px-xl-1 px-2" style="margin-bottom: 1rem;">
|
<h6 class="menu-title" th:text="#{navbar.sections.convertTo}"></h6>
|
||||||
<h6 class="menu-title" th:text="#{navbar.sections.convertTo}"></h6>
|
<div
|
||||||
<div
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('img-to-pdf', 'image', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags', 'image')}">
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('img-to-pdf', 'picture_as_pdf', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags', 'image')}">
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('url-to-pdf', 'link', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('html-to-pdf', 'html', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('markdown-to-pdf', 'markdown', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('book-to-pdf', 'book', 'home.BookToPDF.title', 'home.BookToPDF.desc', 'BookToPDF.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Convert from PDF menu items -->
|
<div
|
||||||
<div class="navbar-item py px-xl-1">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convert')}">
|
||||||
<h6 class="menu-title" th:text="#{navbar.sections.convertFrom}"></h6>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-img', 'photo_library', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags', 'image')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('url-to-pdf', 'link', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags', 'convert')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-word', 'description', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags', 'word')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('html-to-pdf', 'html', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags', 'convert')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-presentation', 'slideshow', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc', 'PDFToPresentation.tags', 'ppt')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('markdown-to-pdf', 'markdown', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags', 'convert')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-text', 'text_fields', 'home.PDFToText.title', 'home.PDFToText.desc', 'PDFToText.tags', 'convert')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('book-to-pdf', 'book', 'home.BookToPDF.title', 'home.BookToPDF.desc', 'BookToPDF.tags', 'convert')}">
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-html', 'html', 'home.PDFToHTML.title', 'home.PDFToHTML.desc', 'PDFToHTML.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-xml', 'code', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-csv', 'csv', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'pdfToPDFA.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-book', 'book', 'home.PDFToBook.title', 'home.PDFToBook.desc', 'PDFToBook.tags', 'convert')}">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Convert from PDF menu items -->
|
||||||
|
<div class="navbar-item py px-xl-1">
|
||||||
|
<h6 class="menu-title" th:text="#{navbar.sections.convertFrom}"></h6>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-img', 'image', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags', 'image')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-word', 'description', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags', 'word')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-presentation', 'slideshow', 'home.PDFToPresentation.title', 'home.PDFToPresentation.desc', 'PDFToPresentation.tags', 'ppt')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-text', 'text_fields', 'home.PDFToText.title', 'home.PDFToText.desc', 'PDFToText.tags', 'convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-html', 'html', 'home.PDFToHTML.title', 'home.PDFToHTML.desc', 'PDFToHTML.tags', 'convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-xml', 'code', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags', 'convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-csv', 'csv', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'pdfToPDFA.tags', 'convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-book', 'book', 'home.PDFToBook.title', 'home.PDFToBook.desc', 'PDFToBook.tags', 'convert')}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Security menu items -->
|
<!-- Security menu items -->
|
||||||
<div class="navbar-item col-lg-2 col-sm-6 py px-xl-1 px-2">
|
<div class="navbar-item col-lg-2 col-sm-6 py px-xl-1 px-2">
|
||||||
<h6 class="menu-title" th:text="#{navbar.sections.security}"></h6>
|
<h6 class="menu-title" th:text="#{navbar.sections.security}"></h6>
|
||||||
@@ -160,8 +159,8 @@
|
|||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'workspace_premium', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags', 'security')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'workspace_premium', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags', 'security')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('validate-signature','verified','home.validateSignature.title','home.validateSignature.desc','validateSignature.tags','security')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('validate-signature','verified','home.validateSignature.title','home.validateSignature.desc','validateSignature.tags','security')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-cert-sign', 'remove_moderator', 'home.removeCertSign.title', 'home.removeCertSign.desc', 'removeCertSign.tags', 'security')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-cert-sign', 'remove_moderator', 'home.removeCertSign.title', 'home.removeCertSign.desc', 'removeCertSign.tags', 'security')}">
|
||||||
</div>
|
</div>
|
||||||
@@ -169,10 +168,10 @@
|
|||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'sanitizer', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags', 'security')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'sanitizer', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags', 'security')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-redact', 'contract_delete', 'home.autoRedact.title', 'home.autoRedact.desc', 'autoRedact.tags', 'security')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-redact', 'ink_eraser', 'home.autoRedact.title', 'home.autoRedact.desc', 'autoRedact.tags', 'security')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('redact', 'playlist_remove', 'home.redact.title', 'home.redact.desc', 'redact.tags', 'security')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('redact', 'ink_eraser', 'home.redact.title', 'home.redact.desc', 'redact.tags', 'security')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('stamp', 'approval', 'home.AddStampRequest.title', 'home.AddStampRequest.desc', 'AddStampRequest.tags', 'security')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('stamp', 'approval', 'home.AddStampRequest.title', 'home.AddStampRequest.desc', 'AddStampRequest.tags', 'security')}">
|
||||||
@@ -194,7 +193,7 @@
|
|||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'add_photo_alternate', 'home.addImage.title', 'home.addImage.desc', 'addImage.tags', 'other')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'add_photo_alternate', 'home.addImage.title', 'home.addImage.desc', 'addImage.tags', 'other')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-images', 'wallpaper', 'home.extractImages.title', 'home.extractImages.desc', 'extractImages.tags', 'other')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-images', 'photo_library', 'home.extractImages.title', 'home.extractImages.desc', 'extractImages.tags', 'other')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('flatten', 'layers_clear', 'home.flatten.title', 'home.flatten.desc', 'flatten.tags', 'other')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('flatten', 'layers_clear', 'home.flatten.title', 'home.flatten.desc', 'flatten.tags', 'other')}">
|
||||||
@@ -231,7 +230,7 @@
|
|||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('pipeline', 'family_history', 'home.pipeline.title', 'home.pipeline.desc', 'pipeline.tags', 'advance')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('pipeline', 'family_history', 'home.pipeline.title', 'home.pipeline.desc', 'pipeline.tags', 'advance')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntryCustom :: navbarEntry ('auto-rename', '/images/rename.svg#icon-rename', 'home.auto-rename.title', 'home.auto-rename.desc', 'auto-rename.tags', 'advance')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-rename', 'text_fields_alt', 'home.auto-rename.title', 'home.auto-rename.desc', 'auto-rename.tags', 'advance')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry ('repair', 'build', 'home.repair.title', 'home.repair.desc', 'repair.tags', 'advance')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry ('repair', 'build', 'home.repair.title', 'home.repair.desc', 'repair.tags', 'advance')}">
|
||||||
@@ -279,7 +278,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('pipeline')}">
|
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('pipeline')}" >
|
||||||
<a class="nav-link" href="#" th:href="@{'/pipeline'}"
|
<a class="nav-link" href="#" th:href="@{'/pipeline'}"
|
||||||
th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
|
th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
|
||||||
<span class="material-symbols-rounded">
|
<span class="material-symbols-rounded">
|
||||||
@@ -289,7 +288,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('compress-pdf')}">
|
<li class="nav-item" th:if="${@endpointConfiguration.isEndpointEnabled('compress-pdf')}" >
|
||||||
<a class="nav-link" href="#" title="#{home.compressPdfs.title}" th:href="@{'/compress-pdf'}"
|
<a class="nav-link" href="#" title="#{home.compressPdfs.title}" th:href="@{'/compress-pdf'}"
|
||||||
th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}">
|
th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}">
|
||||||
<span class="material-symbols-rounded">
|
<span class="material-symbols-rounded">
|
||||||
@@ -331,18 +330,17 @@
|
|||||||
<span class="icon-text icon-hide" th:data-text="#{navbar.favorite}" th:text="#{navbar.favorite}"></span>
|
<span class="icon-text icon-hide" th:data-text="#{navbar.favorite}" th:text="#{navbar.favorite}"></span>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-tp dropdown-mw-28" aria-labelledby="navbarDropdown-5">
|
<div class="dropdown-menu dropdown-menu-tp dropdown-mw-28" aria-labelledby="navbarDropdown-5">
|
||||||
<div class="dropdown-menu-wrapper px-xl-2 px-2" id="favoritesDropdown">
|
<div class="dropdown-menu-wrapper px-xl-2 px-2" id="favoritesDropdown" >
|
||||||
<!-- Dropdown items will be added here by JavaScript -->
|
<!-- Dropdown items will be added here by JavaScript -->
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" id="dark-mode-toggle" href="#" th:title="#{navbar.darkmode}">
|
<a class="nav-link" id="dark-mode-toggle" href="#" th:title="#{navbar.darkmode}">
|
||||||
<span class="material-symbols-rounded" id="dark-mode-icon">
|
<span class="material-symbols-rounded" id="dark-mode-icon">
|
||||||
dark_mode
|
dark_mode
|
||||||
</span>
|
</span>
|
||||||
<span class="icon-text icon-hide" id="dark-mode-text" th:data-text="#{navbar.darkmode}"
|
<span class="icon-text icon-hide" id="dark-mode-text" th:data-text="#{navbar.darkmode}" th:text="#{navbar.darkmode}"></span>
|
||||||
th:text="#{navbar.darkmode}"></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
@@ -363,8 +361,7 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link" href="#" id="searchDropdown" role="button" data-bs-toggle="dropdown"
|
<a class="nav-link" href="#" id="searchDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" th:title="#{navbar.search}">
|
||||||
aria-haspopup="true" aria-expanded="false" th:title="#{navbar.search}">
|
|
||||||
<span class="material-symbols-rounded">
|
<span class="material-symbols-rounded">
|
||||||
search
|
search
|
||||||
</span>
|
</span>
|
||||||
@@ -373,8 +370,7 @@
|
|||||||
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="searchDropdown">
|
<div class="dropdown-menu dropdown-menu-tp" aria-labelledby="searchDropdown">
|
||||||
<div class="dropdown-menu-wrapper px-xl-2 px-2">
|
<div class="dropdown-menu-wrapper px-xl-2 px-2">
|
||||||
<form th:action="@{''}" class="d-flex p-2 search-form" id="searchForm">
|
<form th:action="@{''}" class="d-flex p-2 search-form" id="searchForm">
|
||||||
<input class="form-control search-input" type="search" th:placeholder="#{navbar.search}"
|
<input class="form-control search-input" type="search" th:placeholder="#{navbar.search}" aria-label="Search" id="navbarSearchInput">
|
||||||
aria-label="Search" id="navbarSearchInput">
|
|
||||||
</form>
|
</form>
|
||||||
<!-- Search Results -->
|
<!-- Search Results -->
|
||||||
<div id="searchResults" class="search-results scrollable-y dropdown-mw-20"></div>
|
<div id="searchResults" class="search-results scrollable-y dropdown-mw-20"></div>
|
||||||
@@ -383,15 +379,13 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item" th:if="${!@runningEE}">
|
<li class="nav-item" th:if="${!@runningEE}">
|
||||||
<a href="https://stirlingpdf.com/pricing" class="nav-link go-pro-link" target="_blank"
|
<a href="https://stirlingpdf.com/pricing" class="nav-link go-pro-link" target="_blank" rel="noopener noreferrer">
|
||||||
rel="noopener noreferrer">
|
|
||||||
<span class="go-pro-badge" th:text="#{enterpriseEdition.button}"></span>
|
<span class="go-pro-badge" th:text="#{enterpriseEdition.button}"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<!-- Settings Button -->
|
<!-- Settings Button -->
|
||||||
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#settingsModal"
|
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#settingsModal" th:title="#{navbar.settings}">
|
||||||
th:title="#{navbar.settings}">
|
|
||||||
<span class="material-symbols-rounded">
|
<span class="material-symbols-rounded">
|
||||||
settings
|
settings
|
||||||
</span>
|
</span>
|
||||||
@@ -426,12 +420,10 @@
|
|||||||
th:title="#{visitGithub}">
|
th:title="#{visitGithub}">
|
||||||
<img th:src="@{'/images/github.svg'}" alt="github">
|
<img th:src="@{'/images/github.svg'}" alt="github">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://hub.docker.com/r/stirlingtools/stirling-pdf" class="mx-1" role="button" target="_blank"
|
<a href="https://hub.docker.com/r/stirlingtools/stirling-pdf" class="mx-1" role="button" target="_blank"th:title="#{seeDockerHub}">
|
||||||
th:title="#{seeDockerHub}">
|
|
||||||
<img th:src="@{'/images/docker.svg'}" alt="docker">
|
<img th:src="@{'/images/docker.svg'}" alt="docker">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/Cn8pWhQRxZ" class="mx-1" role="button" target="_blank"
|
<a href="https://discord.gg/Cn8pWhQRxZ" class="mx-1" role="button" target="_blank" th:title="#{joinDiscord}">
|
||||||
th:title="#{joinDiscord}">
|
|
||||||
<img th:src="@{'/images/discord.svg'}" alt="discord">
|
<img th:src="@{'/images/discord.svg'}" alt="discord">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -452,7 +444,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label><br />
|
<label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label><br/>
|
||||||
<input type="range" class="form-range" min="1" max="9" step="1" id="zipThreshold" value="4">
|
<input type="range" class="form-range" min="1" max="9" step="1" id="zipThreshold" value="4">
|
||||||
<span id="zipThresholdValue" class="ms-2"></span>
|
<span id="zipThresholdValue" class="ms-2"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<th:block th:fragment="navbarEntry(endpoint, toolIcon, titleKey, descKey, tagKey, toolGroup)"
|
|
||||||
th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
|
|
||||||
<a class="dropdown-item" href="#" th:href="@{${endpoint}}"
|
|
||||||
th:classappend="${endpoint.equals(currentPage)} ? ${toolGroup} + ' active' : '' + ${toolGroup}"
|
|
||||||
th:title="#{${descKey}}" th:data-bs-tags="#{${tagKey}}">
|
|
||||||
<div class="icon" alt="icon" th:class="@{${toolGroup}}">
|
|
||||||
<svg class="nav-icon" style="height: 2.5rem; width:2.5rem">
|
|
||||||
<use th:xlink:href="@{${toolIcon}}"></use>
|
|
||||||
</svg>
|
|
||||||
<span class="icon-text" th:text="#{${titleKey}}"></span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</th:block>
|
|
||||||
@@ -71,21 +71,21 @@
|
|||||||
|
|
||||||
<div id="popularTools" class="feature-group">
|
<div id="popularTools" class="feature-group">
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.popular})}">
|
th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.popular})}">
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-group-container">
|
<div class="feature-group-container">
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', toolIcon='menu_book', tags=#{viewPdf.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', toolIcon='menu_book', tags=#{viewPdf.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
|
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature-group-container">
|
<div class="feature-group-container">
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', toolIcon='add_to_photos', tags=#{merge.tags}, toolGroup='organize')}">
|
th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', toolIcon='add_to_photos', tags=#{merge.tags}, toolGroup='organize')}">
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature-group-container">
|
<div class="feature-group-container">
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='picture_as_pdf', toolIcon='picture_as_pdf', tags=#{imageToPdf.tags}, toolGroup='image')}">
|
th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', toolIcon='image', tags=#{imageToPdf.tags}, toolGroup='image')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='file-to-pdf', cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', toolIcon='draft', tags=#{fileToPDF.tags}, toolGroup='convert')}">
|
th:replace="~{fragments/card :: card(id='file-to-pdf', cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', toolIcon='draft', tags=#{fileToPDF.tags}, toolGroup='convert')}">
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="feature-group-container">
|
<div class="feature-group-container">
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', toolIcon='photo_library', tags=#{pdfToImage.tags}, toolGroup='image')}">
|
th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', toolIcon='image', tags=#{pdfToImage.tags}, toolGroup='image')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='pdf-to-pdfa', cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', toolIcon='picture_as_pdf', tags=#{pdfToPDFA.tags}, toolGroup='convert')}">
|
th:replace="~{fragments/card :: card(id='pdf-to-pdfa', cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', toolIcon='picture_as_pdf', tags=#{pdfToPDFA.tags}, toolGroup='convert')}">
|
||||||
@@ -215,9 +215,9 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', toolIcon='workspace_premium', tags=#{certSign.tags}, toolGroup='security')}">
|
th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', toolIcon='workspace_premium', tags=#{certSign.tags}, toolGroup='security')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='validate-signature', cardTitle=#{home.validateSignature.title}, cardText=#{home.validateSignature.desc}, cardLink='validate-signature', toolIcon='verified', tags=#{validateSignature.tags}, toolGroup='security')}">
|
th:replace="~{fragments/card :: card(id='validate-signature', cardTitle=#{home.validateSignature.title}, cardText=#{home.validateSignature.desc}, cardLink='validate-signature', toolIcon='verified', tags=#{validateSignature.tags}, toolGroup='security')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='remove-cert-sign', cardTitle=#{home.removeCertSign.title}, cardText=#{home.removeCertSign.desc}, cardLink='remove-cert-sign', toolIcon='remove_moderator', tags=#{removeCertSign.tags}, toolGroup='security')}">
|
th:replace="~{fragments/card :: card(id='remove-cert-sign', cardTitle=#{home.removeCertSign.title}, cardText=#{home.removeCertSign.desc}, cardLink='remove-cert-sign', toolIcon='remove_moderator', tags=#{removeCertSign.tags}, toolGroup='security')}">
|
||||||
</div>
|
</div>
|
||||||
@@ -228,7 +228,7 @@
|
|||||||
th:replace="~{fragments/card :: card(id='auto-redact', cardTitle=#{home.autoRedact.title}, cardText=#{home.autoRedact.desc}, cardLink='auto-redact', toolIcon='ink_eraser', tags=#{autoRedact.tags}, toolGroup='security')}">
|
th:replace="~{fragments/card :: card(id='auto-redact', cardTitle=#{home.autoRedact.title}, cardText=#{home.autoRedact.desc}, cardLink='auto-redact', toolIcon='ink_eraser', tags=#{autoRedact.tags}, toolGroup='security')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='redact', cardTitle=#{home.redact.title}, cardText=#{home.redact.desc}, cardLink='redact', toolIcon='playlist_remove', tags=#{redact.tags}, toolGroup='security')}">
|
th:replace="~{fragments/card :: card(id='redact', cardTitle=#{home.redact.title}, cardText=#{home.redact.desc}, cardLink='redact', toolIcon='ink_eraser', tags=#{redact.tags}, toolGroup='security')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='stamp', cardTitle=#{home.AddStampRequest.title}, cardText=#{home.AddStampRequest.desc}, cardLink='stamp', toolIcon='approval', tags=#{AddStampRequest.tags}, toolGroup='security')}">
|
th:replace="~{fragments/card :: card(id='stamp', cardTitle=#{home.AddStampRequest.title}, cardText=#{home.AddStampRequest.desc}, cardLink='stamp', toolIcon='approval', tags=#{AddStampRequest.tags}, toolGroup='security')}">
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', toolIcon='123', tags=#{add-page-numbers.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', toolIcon='123', tags=#{add-page-numbers.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', toolIcon='add_photo_alternate', tags=#{addImage.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', toolIcon='text_fields', tags=#{addImage.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -260,7 +260,7 @@
|
|||||||
th:replace="~{fragments/card :: card(id='ocr-pdf', cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', toolIcon='quick_reference_all', tags=#{ocr.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='ocr-pdf', cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', toolIcon='quick_reference_all', tags=#{ocr.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', toolIcon='wallpaper', tags=#{extractImages.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', toolIcon='photo_library', tags=#{extractImages.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', toolIcon='layers_clear', tags=#{flatten.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', toolIcon='layers_clear', tags=#{flatten.tags}, toolGroup='other')}">
|
||||||
@@ -281,13 +281,14 @@
|
|||||||
th:replace="~{fragments/card :: card(id='remove-image-pdf', cardTitle=#{home.removeImagePdf.title}, cardText=#{home.removeImagePdf.desc}, cardLink='remove-image-pdf', toolIcon='remove_selection', tags=#{removeImagePdf.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='remove-image-pdf', cardTitle=#{home.removeImagePdf.title}, cardText=#{home.removeImagePdf.desc}, cardLink='remove-image-pdf', toolIcon='remove_selection', tags=#{removeImagePdf.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='replace-color-pdf', cardTitle=#{home.replaceColorPdf.title}, cardText=#{home.replaceColorPdf.desc}, cardLink='replace-and-invert-color-pdf', toolIcon='format_color_fill', tags=#{replaceColorPdf.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='replace-color-pdf', cardTitle=#{home.replaceColorPdf.title}, cardText=#{home.replaceColorPdf.desc}, cardLink='replace-and-invert-color-pdf', toolIcon='format_color_fill', tags=#{replaceColorPdf.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="groupAdvanced" class="feature-group">
|
<div id="groupAdvanced" class="feature-group">
|
||||||
<div
|
<div
|
||||||
|
|
||||||
th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.advance})}">
|
th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.advance})}">
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-group-container">
|
<div class="feature-group-container">
|
||||||
@@ -301,7 +302,7 @@
|
|||||||
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', toolIcon='scanner', tags=#{ScannerImageSplit.tags}, toolGroup='advance')}">
|
th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', toolIcon='scanner', tags=#{ScannerImageSplit.tags}, toolGroup='advance')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', toolIcon='build', tags=#{repair.tags}, toolGroup='advance')}">
|
th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', toolIcon='build', tags=#{repair.tags}, toolGroup='advance')}">
|
||||||
@@ -337,8 +338,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Survey Modal -->
|
<!-- Survey Modal -->
|
||||||
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
|
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel" aria-hidden="true">
|
||||||
aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -346,16 +346,12 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p><span th:text="#{survey.changes}">Stirling-PDF has changed since the last survey! To find out more please
|
<p><span th:text="#{survey.changes}">Stirling-PDF has changed since the last survey! To find out more please check our blog post here: </span><a href="https://www.stirlingpdf.com/blog/stirling-pdf-future" target="_blank"> Stirling PDF</a></p>
|
||||||
check our blog post here: </span><a href="https://www.stirlingpdf.com/blog/stirling-pdf-future"
|
|
||||||
target="_blank"> Stirling PDF</a></p>
|
|
||||||
|
|
||||||
<p th:text="#{survey.changes2}">With these changes we are getting paid business support and funding</p>
|
<p th:text="#{survey.changes2}">With these changes we are getting paid business support and funding</p>
|
||||||
<p th:text="#{survey.please}">Please consider taking our survey!</p>
|
<p th:text="#{survey.please}">Please consider taking our survey!</p>
|
||||||
<p th:text="#{survey.disabled}">Survey popup will be disabled in following updates but available at foot of
|
<p th:text="#{survey.disabled}">Survey popup will be disabled in following updates but available at foot of page)</p>
|
||||||
page)</p>
|
<a href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu" target="_blank" class="btn btn-primary" id="takeSurvey"th:text="#{survey.button}" >Take Survey</a>
|
||||||
<a href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu" target="_blank" class="btn btn-primary"
|
|
||||||
id="takeSurvey" th:text="#{survey.button}">Take Survey</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
@@ -372,29 +368,22 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Analytics Modal -->
|
<!-- Analytics Modal -->
|
||||||
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
|
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel" aria-hidden="true" th:if="${@analyticsPrompt}">
|
||||||
aria-hidden="true" th:if="${@analyticsPrompt}">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="analyticsModalLabel" th:text="#{analytics.title}">Do you want make Stirling PDF
|
<h5 class="modal-title" id="analyticsModalLabel" th:text="#{analytics.title}">Do you want make Stirling PDF better?</h5>
|
||||||
better?</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p th:text="#{analytics.paragraph1}">Stirling PDF has opt in analytics to help us improve the product. We do
|
<p th:text="#{analytics.paragraph1}">Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.</p>
|
||||||
not track any personal information or file contents.</p>
|
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.</p>
|
||||||
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow
|
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file</p>
|
||||||
us to understand our users better.</p>
|
|
||||||
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer justify-content-between">
|
<div class="modal-footer justify-content-between">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)"
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)" th:text="#{analytics.disable}">Disable analytics</button>
|
||||||
th:text="#{analytics.disable}">Disable analytics</button>
|
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}" onclick="setAnalytics(true)">Enable analytics</button>
|
||||||
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}"
|
</div>
|
||||||
onclick="setAnalytics(true)">Enable analytics</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -407,7 +396,7 @@
|
|||||||
/*<![CDATA[*/
|
/*<![CDATA[*/
|
||||||
const analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
const analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (analyticsPromptBoolean) {
|
if (analyticsPromptBoolean) {
|
||||||
const analyticsModal = new bootstrap.Modal(document.getElementById('analyticsModal'));
|
const analyticsModal = new bootstrap.Modal(document.getElementById('analyticsModal'));
|
||||||
analyticsModal.show();
|
analyticsModal.show();
|
||||||
@@ -422,76 +411,76 @@
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(enabled)
|
body: JSON.stringify(enabled)
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
console.log('Analytics setting updated successfully');
|
console.log('Analytics setting updated successfully');
|
||||||
bootstrap.Modal.getInstance(document.getElementById('analyticsModal')).hide();
|
bootstrap.Modal.getInstance(document.getElementById('analyticsModal')).hide();
|
||||||
} else if (response.status === 208) {
|
} else if (response.status === 208) {
|
||||||
console.log('Analytics setting has already been set. Please edit /config/settings.yml to change it.', response);
|
console.log('Analytics setting has already been set. Please edit /config/settings.yml to change it.', response);
|
||||||
alert('Analytics setting has already been set. Please edit /config/settings.yml to change it.');
|
alert('Analytics setting has already been set. Please edit /config/settings.yml to change it.');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unexpected response status: ' + response.status);
|
throw new Error('Unexpected response status: ' + response.status);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error updating analytics setting:', error);
|
console.error('Error updating analytics setting:', error);
|
||||||
alert('An error occurred while updating the analytics setting. Please try again.');
|
alert('An error occurred while updating the analytics setting. Please try again.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const surveyVersion = "2.0";
|
const surveyVersion = "2.0";
|
||||||
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
||||||
const dontShowAgain = document.getElementById('dontShowAgain');
|
const dontShowAgain = document.getElementById('dontShowAgain');
|
||||||
const takeSurveyButton = document.getElementById('takeSurvey');
|
const takeSurveyButton = document.getElementById('takeSurvey');
|
||||||
|
|
||||||
const viewThresholds = [5, 10, 15, 22, 30, 50, 75, 100, 150, 200];
|
const viewThresholds = [5, 10, 15, 22, 30, 50, 75, 100, 150, 200];
|
||||||
|
|
||||||
// Check if survey version changed and reset page views if it did
|
// Check if survey version changed and reset page views if it did
|
||||||
const storedVersion = localStorage.getItem('surveyVersion');
|
const storedVersion = localStorage.getItem('surveyVersion');
|
||||||
if (storedVersion && storedVersion !== surveyVersion) {
|
if (storedVersion && storedVersion !== surveyVersion) {
|
||||||
localStorage.setItem('pageViews', '0');
|
localStorage.setItem('pageViews', '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
||||||
|
|
||||||
pageViews++;
|
pageViews++;
|
||||||
localStorage.setItem('pageViews', pageViews.toString());
|
localStorage.setItem('pageViews', pageViews.toString());
|
||||||
|
|
||||||
function shouldShowSurvey() {
|
function shouldShowSurvey() {
|
||||||
if (localStorage.getItem('dontShowSurvey') === 'true' ||
|
if (localStorage.getItem('dontShowSurvey') === 'true' ||
|
||||||
localStorage.getItem('surveyTaken') === 'true') {
|
localStorage.getItem('surveyTaken') === 'true') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If survey version changed and we hit a threshold, show the survey
|
// If survey version changed and we hit a threshold, show the survey
|
||||||
if (localStorage.getItem('surveyVersion') !== surveyVersion &&
|
if (localStorage.getItem('surveyVersion') !== surveyVersion &&
|
||||||
viewThresholds.includes(pageViews)) {
|
viewThresholds.includes(pageViews)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewThresholds.includes(pageViews);
|
return viewThresholds.includes(pageViews);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldShowSurvey()) {
|
if (shouldShowSurvey()) {
|
||||||
modal.show();
|
modal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
dontShowAgain.addEventListener('change', function () {
|
dontShowAgain.addEventListener('change', function() {
|
||||||
if (this.checked) {
|
if (this.checked) {
|
||||||
localStorage.setItem('dontShowSurvey', 'true');
|
localStorage.setItem('dontShowSurvey', 'true');
|
||||||
localStorage.setItem('surveyVersion', surveyVersion);
|
localStorage.setItem('surveyVersion', surveyVersion);
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('dontShowSurvey');
|
localStorage.removeItem('dontShowSurvey');
|
||||||
localStorage.removeItem('surveyVersion');
|
localStorage.removeItem('surveyVersion');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
takeSurveyButton.addEventListener('click', function () {
|
takeSurveyButton.addEventListener('click', function() {
|
||||||
localStorage.setItem('surveyTaken', 'true');
|
localStorage.setItem('surveyTaken', 'true');
|
||||||
localStorage.setItem('surveyVersion', surveyVersion);
|
localStorage.setItem('surveyVersion', surveyVersion);
|
||||||
modal.hide();
|
modal.hide();
|
||||||
|
|||||||
@@ -12,87 +12,87 @@
|
|||||||
<div class="container-flex">
|
<div class="container-flex">
|
||||||
<main class="form-signin">
|
<main class="form-signin">
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
const redirectAttempts = parseInt(localStorage.getItem('ssoRedirectAttempts') || '0');
|
const redirectAttempts = parseInt(localStorage.getItem('ssoRedirectAttempts') || '0');
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const hasRedirectError = urlParams.has('error');
|
const hasRedirectError = urlParams.has('error');
|
||||||
const hasLogout = urlParams.has('logout');
|
const hasLogout = urlParams.has('logout');
|
||||||
const hasMessage = urlParams.has('message');
|
const hasMessage = urlParams.has('message');
|
||||||
const MAX_REDIRECT_ATTEMPTS = 3;
|
const MAX_REDIRECT_ATTEMPTS = 3;
|
||||||
|
|
||||||
document.addEventListener('modeChanged', function(e) {
|
document.addEventListener('modeChanged', function(e) {
|
||||||
var mode = e.detail;
|
var mode = e.detail;
|
||||||
|
|
||||||
document.body.classList.remove("light-mode", "dark-mode", "rainbow-mode"); // remove all mode classes first
|
document.body.classList.remove("light-mode", "dark-mode", "rainbow-mode"); // remove all mode classes first
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "on":
|
case "on":
|
||||||
document.body.classList.add("dark-mode");
|
document.body.classList.add("dark-mode");
|
||||||
break;
|
break;
|
||||||
case "off":
|
case "off":
|
||||||
document.body.classList.add("light-mode");
|
document.body.classList.add("light-mode");
|
||||||
break;
|
break;
|
||||||
case "rainbow":
|
case "rainbow":
|
||||||
document.body.classList.add("rainbow-mode");
|
document.body.classList.add("rainbow-mode");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
const runningEE = /*[[${@runningEE}]]*/ false;
|
const runningEE = [[${@runningEE}]];
|
||||||
const SSOAutoLogin = /*[[${@SSOAutoLogin}]]*/ false;
|
const SSOAutoLogin = [[${@SSOAutoLogin}]];
|
||||||
const loginMethod = /*[[${loginMethod}]]*/ 'normal';
|
const loginMethod = [[${loginMethod}]];
|
||||||
const providerList = /*[[${providerlist}]]*/ {};
|
const providerList = [[${providerlist}]];
|
||||||
const shouldAutoRedirect = !hasRedirectError &&
|
const shouldAutoRedirect = !hasRedirectError &&
|
||||||
!hasLogout &&
|
!hasLogout &&
|
||||||
!hasMessage &&
|
!hasMessage &&
|
||||||
redirectAttempts < MAX_REDIRECT_ATTEMPTS &&
|
redirectAttempts < MAX_REDIRECT_ATTEMPTS &&
|
||||||
loginMethod !== 'normal' && runningEE && SSOAutoLogin;
|
loginMethod !== 'normal' && runningEE && SSOAutoLogin;
|
||||||
|
|
||||||
console.log('Should redirect:', shouldAutoRedirect, {
|
console.log('Should redirect:', shouldAutoRedirect, {
|
||||||
'No error': !hasRedirectError,
|
'No error': !hasRedirectError,
|
||||||
'No logout': !hasLogout,
|
'No logout': !hasLogout,
|
||||||
'No message': !hasMessage,
|
'No message': !hasMessage,
|
||||||
'Under max attempts': redirectAttempts < MAX_REDIRECT_ATTEMPTS,
|
'Under max attempts': redirectAttempts < MAX_REDIRECT_ATTEMPTS,
|
||||||
'Is only OAuth2': loginMethod === 'oauth2'
|
'Is OAuth2': loginMethod === 'oauth2'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shouldAutoRedirect && providerList && Object.keys(providerList).length > 0) {
|
if (shouldAutoRedirect && providerList && Object.keys(providerList).length > 0) {
|
||||||
localStorage.setItem('ssoRedirectAttempts', redirectAttempts + 1);
|
localStorage.setItem('ssoRedirectAttempts', redirectAttempts + 1);
|
||||||
const firstProvider = Object.keys(providerList)[0];
|
const firstProvider = Object.keys(providerList)[0];
|
||||||
window.location.href = firstProvider;
|
window.location.href = firstProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset redirect attempts if successful login or after 1 hour
|
// Reset redirect attempts if successful login or after 1 hour
|
||||||
const lastAttemptTime = parseInt(localStorage.getItem('lastRedirectAttempt') || '0');
|
const lastAttemptTime = parseInt(localStorage.getItem('lastRedirectAttempt') || '0');
|
||||||
if (Date.now() - lastAttemptTime > 3600000) { // 1 hour
|
if (Date.now() - lastAttemptTime > 3600000) { // 1 hour
|
||||||
localStorage.setItem('ssoRedirectAttempts', '0');
|
localStorage.setItem('ssoRedirectAttempts', '0');
|
||||||
}
|
}
|
||||||
localStorage.setItem('lastRedirectAttempt', Date.now().toString());
|
localStorage.setItem('lastRedirectAttempt', Date.now().toString());
|
||||||
|
|
||||||
|
|
||||||
const defaultLocale = getStoredOrDefaultLocale();
|
const defaultLocale = getStoredOrDefaultLocale();
|
||||||
checkUserLanguage(defaultLocale);
|
checkUserLanguage(defaultLocale);
|
||||||
|
|
||||||
const dropdownItems = document.querySelectorAll('.lang_dropdown-item');
|
const dropdownItems = document.querySelectorAll('.lang_dropdown-item');
|
||||||
let activeItem;
|
let activeItem;
|
||||||
|
|
||||||
for (let i = 0; i < dropdownItems.length; i++) {
|
for (let i = 0; i < dropdownItems.length; i++) {
|
||||||
const item = dropdownItems[i];
|
const item = dropdownItems[i];
|
||||||
item.classList.remove('active');
|
item.classList.remove('active');
|
||||||
if (item.dataset.bsLanguageCode === defaultLocale) {
|
if (item.dataset.bsLanguageCode === defaultLocale) {
|
||||||
item.classList.add('active');
|
item.classList.add('active');
|
||||||
activeItem = item;
|
activeItem = item;
|
||||||
}
|
}
|
||||||
item.addEventListener('click', handleDropdownItemClick);
|
item.addEventListener('click', handleDropdownItemClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropdown = document.getElementById('languageDropdown');
|
const dropdown = document.getElementById('languageDropdown');
|
||||||
|
|
||||||
if (activeItem) {
|
if (activeItem) {
|
||||||
dropdown.innerHTML = activeItem.innerHTML; // This will set the dropdown button's content to the active language's flag and name
|
dropdown.innerHTML = activeItem.innerHTML; // This will set the dropdown button's content to the active language's flag and name
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144">
|
<img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144">
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
<span id="currentVersion" th:text="${@appVersion}"></span>
|
<span id="currentVersion" th:text="${@appVersion}"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
<span th:text="#{releases.note}">All release notes are only available in English</span>
|
<span th:text="#{releases.note}">All release notes are only available in English</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading" class="text-center my-4">
|
<div id="loading" class="text-center my-4">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
@@ -107,14 +107,14 @@
|
|||||||
/*<![CDATA[*/
|
/*<![CDATA[*/
|
||||||
|
|
||||||
// Get the current version from the appVersion bean
|
// Get the current version from the appVersion bean
|
||||||
const appVersion = /*[[${@appVersion}]]*/ '';
|
const appVersion = [[${@appVersion}]];
|
||||||
|
|
||||||
// GitHub API configuration
|
// GitHub API configuration
|
||||||
const REPO_OWNER = 'Stirling-Tools';
|
const REPO_OWNER = 'Stirling-Tools';
|
||||||
const REPO_NAME = 'Stirling-PDF';
|
const REPO_NAME = 'Stirling-PDF';
|
||||||
const GITHUB_API = 'https://api.github.com/repos/' + REPO_OWNER + '/' + REPO_NAME;
|
const GITHUB_API = 'https://api.github.com/repos/' + REPO_OWNER + '/' + REPO_NAME;
|
||||||
const GITHUB_URL = 'https://github.com/' + REPO_OWNER + '/' + REPO_NAME;
|
const GITHUB_URL = 'https://github.com/' + REPO_OWNER + '/' + REPO_NAME;
|
||||||
const MAX_RELEASES = 8;
|
const MAX_RELEASES = 8;
|
||||||
|
|
||||||
// Secure element creation helper
|
// Secure element creation helper
|
||||||
function createElement(tag, attributes = {}, children = []) {
|
function createElement(tag, attributes = {}, children = []) {
|
||||||
@@ -134,6 +134,7 @@
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ALLOWED_TAGS = {
|
const ALLOWED_TAGS = {
|
||||||
'a': ['href', 'target', 'rel', 'class'],
|
'a': ['href', 'target', 'rel', 'class'],
|
||||||
'img': ['src', 'alt', 'width', 'height', 'style'],
|
'img': ['src', 'alt', 'width', 'height', 'style'],
|
||||||
@@ -274,31 +275,9 @@
|
|||||||
function formatText(text) {
|
function formatText(text) {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
|
|
||||||
if (!text || typeof text !== 'string') {
|
|
||||||
console.error('Invalid input to formatText:', text);
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
let textWithoutComments;
|
|
||||||
try {
|
|
||||||
textWithoutComments = text.replace(
|
|
||||||
/<!-- Release notes generated using configuration in .github\/release\.yml at main -->/,
|
|
||||||
''
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in replace operation:', error);
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split the text into lines
|
// Split the text into lines
|
||||||
let lines;
|
const textWithoutComments = text.replace(/<!--[\s\S]*?-->/g, '');
|
||||||
try {
|
const lines = textWithoutComments.split('\n');
|
||||||
lines = textWithoutComments.split('\n');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in split operation:', error);
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentList = null;
|
let currentList = null;
|
||||||
|
|
||||||
lines.forEach(line => {
|
lines.forEach(line => {
|
||||||
@@ -412,26 +391,10 @@ async function loadReleases() {
|
|||||||
while (container.firstChild) {
|
while (container.firstChild) {
|
||||||
container.removeChild(container.firstChild);
|
container.removeChild(container.firstChild);
|
||||||
}
|
}
|
||||||
const cachedReleases = sessionStorage.getItem('releases');
|
|
||||||
|
|
||||||
let releases;
|
const response = await fetch(GITHUB_API + '/releases');
|
||||||
if (cachedReleases) {
|
if (!response.ok) throw new Error('Failed to fetch releases');
|
||||||
releases = JSON.parse(cachedReleases);
|
const releases = await response.json();
|
||||||
console.log("Read from storage");
|
|
||||||
} else {
|
|
||||||
const response = await fetch(GITHUB_API + '/releases');
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 403) {
|
|
||||||
throw new Error('API rate limit exceeded');
|
|
||||||
}
|
|
||||||
if (response.status === 404) {
|
|
||||||
throw new Error('Repository not found');
|
|
||||||
}
|
|
||||||
throw new Error('Failed to fetch releases');
|
|
||||||
}
|
|
||||||
releases = await response.json();
|
|
||||||
sessionStorage.setItem('releases', JSON.stringify(releases));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort releases by version number (descending)
|
// Sort releases by version number (descending)
|
||||||
releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
|
releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, remoteCall='false', accept='application/pdf')}"></div>
|
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, remoteCall='false', accept='application/pdf')}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label th:text="#{validateSignature.selectCustomCert}" ></label>
|
<label th:text="#{validateSignature.selectCustomCert}" ></label>
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='certFile', multipleInputsForSingleRequest=false, notRequired=true, remoteCall='false', accept='.cer,.crt,.pem')}"></div>
|
<div th:replace="~{fragments/common :: fileSelector(name='certFile', multipleInputsForSingleRequest=false, notRequired=true, remoteCall='false', accept='.cer,.crt,.pem')}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 text-left">
|
<div class="mb-3 text-left">
|
||||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{validateSignature.submit}"></button>
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{validateSignature.submit}"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user