From f1a16ddec014f0864606553e8a82ad7998abbf25 Mon Sep 17 00:00:00 2001 From: Tilen Komel Date: Mon, 5 Oct 2026 14:42:50 +0200 Subject: [PATCH] feat: add initial implementation of eSodisce / GURS batch helper extension --- .gitea/workflows/publish.yml | 454 +++++++++++++++++++++++++++++++++++ README.md | 16 +- background.js | 5 + content-script.js | 125 ++++++++++ example-input.txt | 6 + manifest.json | 36 +++ popup.css | 14 ++ popup.html | 32 +++ popup.js | 75 ++++++ updates.json | 7 + 10 files changed, 769 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/publish.yml create mode 100644 background.js create mode 100644 content-script.js create mode 100644 example-input.txt create mode 100644 manifest.json create mode 100644 popup.css create mode 100644 popup.html create mode 100644 popup.js create mode 100644 updates.json diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..90e1dcd --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,454 @@ +name: Publish Firefox extension + +on: + push: + branches: + - main + # The workflow's own update-manifest-only commit must not start another run. + paths-ignore: + - updates.json + +permissions: + contents: write + +concurrency: + group: publish-main + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + env: + API_BASE: https://git.vahta.net/api/v1 + REPOSITORY: komelt/zemljiska-knjiga-vticnik + RUN_NUMBER: ${{ github.run_number }} + TARGET_COMMIT: ${{ github.sha }} + + steps: + - name: Check out source + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITEA_TOKEN }} + persist-credentials: true + + - name: Validate and stage extension + shell: bash + run: | + set -Eeuo pipefail + + command -v jq >/dev/null + command -v node >/dev/null + command -v curl >/dev/null + command -v npx >/dev/null + command -v sha256sum >/dev/null + + jq -e . manifest.json >/dev/null + SOURCE_VERSION="$(jq -er '.version | strings' manifest.json)" + if [[ ! "$SOURCE_VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then + echo "manifest.json version must contain one to three numeric components" >&2 + exit 1 + fi + if [[ ! "$RUN_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "The Actions run number is missing or is not a positive integer" >&2 + exit 1 + fi + + VERSION="${SOURCE_VERSION}.${RUN_NUMBER}" + if [[ ! "$VERSION" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]]; then + echo "Computed Firefox version is invalid: $VERSION" >&2 + exit 1 + fi + + STAGING_DIR="$RUNNER_TEMP/zemljiska-knjiga-vticnik-$VERSION" + SIGNED_XPI="$RUNNER_TEMP/zemljiska-knjiga-vticnik-$VERSION.xpi" + rm -rf "$STAGING_DIR" + mkdir -p "$STAGING_DIR" + RUNTIME_FILES=(manifest.json background.js content-script.js popup.html popup.css popup.js) + cp -- "${RUNTIME_FILES[@]}" "$STAGING_DIR/" + + jq --arg version "$VERSION" '.version = $version' \ + "$STAGING_DIR/manifest.json" > "$STAGING_DIR/manifest.json.tmp" + mv -- "$STAGING_DIR/manifest.json.tmp" "$STAGING_DIR/manifest.json" + + jq -e . "$STAGING_DIR/manifest.json" >/dev/null + for javascript_file in background.js content-script.js popup.js; do + node --check "$STAGING_DIR/$javascript_file" + done + + printf 'VERSION=%s\nSTAGING_DIR=%s\nSIGNED_XPI=%s\n' \ + "$VERSION" "$STAGING_DIR" "$SIGNED_XPI" >> "$GITHUB_ENV" + + - name: Reuse an existing Gitea release asset when available + shell: bash + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + set -Eeuo pipefail + ASSET_NAME="zemljiska-knjiga-vticnik-$VERSION.xpi" + RELEASE_RESPONSE="$RUNNER_TEMP/gitea-release-$VERSION.json" + RELEASE_URL="$API_BASE/repos/$REPOSITORY/releases/tags/v$VERSION" + + if ! RELEASE_STATUS="$(curl --silent --show-error \ + --header "Authorization: token $GITEA_TOKEN" \ + --write-out '%{http_code}' --output "$RELEASE_RESPONSE" \ + "$RELEASE_URL")"; then + echo 'Could not query the Gitea release.' >&2 + exit 1 + fi + case "$RELEASE_STATUS" in + 404) + printf 'RELEASE_ID=\nRELEASE_ASSET_EXISTS=false\n' >> "$GITHUB_ENV" + exit 0 + ;; + 2??) + ;; + *) + echo "Gitea release lookup returned HTTP $RELEASE_STATUS" >&2 + cat "$RELEASE_RESPONSE" >&2 + exit 1 + ;; + esac + + RELEASE_ID="$(jq -er '.id | numbers' "$RELEASE_RESPONSE")" + matching_assets="$(jq -c --arg name "$ASSET_NAME" \ + '[.assets[]? | select(.name == $name)]' "$RELEASE_RESPONSE")" + asset_count="$(jq -er 'length' <<< "$matching_assets")" + if [[ "$asset_count" -gt 1 ]]; then + echo "Gitea release contains multiple assets named $ASSET_NAME" >&2 + exit 1 + fi + if [[ "$asset_count" -eq 0 ]]; then + printf 'RELEASE_ID=%s\nRELEASE_ASSET_EXISTS=false\n' "$RELEASE_ID" >> "$GITHUB_ENV" + exit 0 + fi + + asset_size="$(jq -r '.[0].size // empty' <<< "$matching_assets")" + asset_url="$(jq -r '.[0].browser_download_url // empty' <<< "$matching_assets")" + if [[ ! "$asset_url" =~ ^https://[^[:space:]]+$ ]]; then + echo 'The existing release asset has a missing or invalid HTTPS browser download URL.' >&2 + exit 1 + fi + rm -f -- "$SIGNED_XPI" + curl --fail --silent --show-error --location \ + --header "Authorization: token $GITEA_TOKEN" \ + --output "$SIGNED_XPI" \ + "$asset_url" + [[ -s "$SIGNED_XPI" ]] || { + echo 'The existing release asset downloaded as an empty file.' >&2 + exit 1 + } + if [[ -n "$asset_size" && "$(wc -c < "$SIGNED_XPI")" != "$asset_size" ]]; then + echo 'The downloaded release asset size does not match Gitea metadata.' >&2 + exit 1 + fi + XPI_SHA256="$(sha256sum "$SIGNED_XPI" | awk '{print $1}')" + [[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || { + echo 'Could not calculate a valid SHA-256 digest for the existing release asset.' >&2 + exit 1 + } + expected_hash="$(jq -r --arg version "$VERSION" \ + '[.addons[]?.updates[]? | select(.version == $version) | .update_hash] | unique | if length == 1 then .[0] else "" end' \ + updates.json 2>/dev/null || true)" + if [[ -n "$expected_hash" && "$expected_hash" != "sha256:$XPI_SHA256" ]]; then + echo 'The existing release asset does not match the update manifest hash.' >&2 + exit 1 + fi + + printf 'RELEASE_ID=%s\nRELEASE_ASSET_EXISTS=true\nXPI_SHA256=%s\n' \ + "$RELEASE_ID" "$XPI_SHA256" >> "$GITHUB_ENV" + + - name: Sign unlisted build through AMO + if: env.RELEASE_ASSET_EXISTS != 'true' + shell: bash + env: + AMO_JWT_ISSUER: ${{ secrets.AMO_JWT_ISSUER }} + AMO_JWT_SECRET: ${{ secrets.AMO_JWT_SECRET }} + run: | + set -Eeuo pipefail + ARTIFACTS_DIR="$RUNNER_TEMP/web-ext-artifacts-$VERSION" + rm -rf "$ARTIFACTS_DIR" "$SIGNED_XPI" + mkdir -p "$ARTIFACTS_DIR" + + npx --yes web-ext@10.7.0 sign \ + --channel=unlisted \ + --source-dir "$STAGING_DIR" \ + --artifacts-dir "$ARTIFACTS_DIR" \ + --api-key "$AMO_JWT_ISSUER" \ + --api-secret "$AMO_JWT_SECRET" + + mapfile -t signed_files < <(find "$ARTIFACTS_DIR" -maxdepth 1 -type f -name '*.xpi' -print) + if [[ "${#signed_files[@]}" -ne 1 ]]; then + echo "Expected exactly one signed XPI, found ${#signed_files[@]}" >&2 + exit 1 + fi + cp -- "${signed_files[0]}" "$SIGNED_XPI" + XPI_SHA256="$(sha256sum "$SIGNED_XPI" | awk '{print $1}')" + [[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || { + echo 'Could not calculate a valid SHA-256 digest for the signed XPI.' >&2 + exit 1 + } + printf 'XPI_SHA256=%s\n' "$XPI_SHA256" >> "$GITHUB_ENV" + + - name: Create public Gitea release and upload XPI + shell: bash + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + set -Eeuo pipefail + ASSET_NAME="zemljiska-knjiga-vticnik-$VERSION.xpi" + RELEASE_RESPONSE="$RUNNER_TEMP/gitea-release-$VERSION.json" + + download_asset() { + local asset_url="$1" + local asset_size="$2" + if [[ ! "$asset_url" =~ ^https://[^[:space:]]+$ ]]; then + echo 'The existing release asset has a missing or invalid HTTPS browser download URL.' >&2 + exit 1 + fi + rm -f -- "$SIGNED_XPI" + curl --fail --silent --show-error --location \ + --header "Authorization: token $GITEA_TOKEN" \ + --output "$SIGNED_XPI" \ + "$asset_url" + [[ -s "$SIGNED_XPI" ]] || { + echo 'The existing release asset downloaded as an empty file.' >&2 + exit 1 + } + if [[ -n "$asset_size" && "$(wc -c < "$SIGNED_XPI")" != "$asset_size" ]]; then + echo 'The downloaded release asset size does not match Gitea metadata.' >&2 + exit 1 + fi + XPI_SHA256="$(sha256sum "$SIGNED_XPI" | awk '{print $1}')" + [[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || { + echo 'Could not calculate a valid SHA-256 digest for the existing release asset.' >&2 + exit 1 + } + local expected_hash + expected_hash="$(jq -r --arg version "$VERSION" \ + '[.addons[]?.updates[]? | select(.version == $version) | .update_hash] | unique | if length == 1 then .[0] else "" end' \ + updates.json 2>/dev/null || true)" + if [[ -n "$expected_hash" && "$expected_hash" != "sha256:$XPI_SHA256" ]]; then + echo 'The existing release asset does not match the update manifest hash.' >&2 + exit 1 + fi + RELEASE_ASSET_EXISTS=true + } + + inspect_assets() { + local asset_count asset_url asset_size + asset_count="$(jq -er --arg name "$ASSET_NAME" \ + '[.assets[]? | select(.name == $name)] | length' "$RELEASE_RESPONSE")" + if [[ "$asset_count" -gt 1 ]]; then + echo "Gitea release contains multiple assets named $ASSET_NAME" >&2 + exit 1 + fi + if [[ "$asset_count" -eq 1 ]]; then + asset_size="$(jq -r --arg name "$ASSET_NAME" \ + '.assets[] | select(.name == $name) | .size // empty' "$RELEASE_RESPONSE")" + asset_url="$(jq -r --arg name "$ASSET_NAME" \ + '.assets[] | select(.name == $name) | .browser_download_url // empty' "$RELEASE_RESPONSE")" + download_asset "$asset_url" "$asset_size" + fi + } + + fetch_release_by_tag() { + local status + if ! status="$(curl --silent --show-error \ + --header "Authorization: token $GITEA_TOKEN" \ + --write-out '%{http_code}' --output "$RELEASE_RESPONSE" \ + "$API_BASE/repos/$REPOSITORY/releases/tags/v$VERSION")"; then + echo 'Could not query the Gitea release after a concurrent change.' >&2 + exit 1 + fi + [[ "$status" == 2?? ]] || { + echo "Gitea release lookup returned HTTP $status" >&2 + cat "$RELEASE_RESPONSE" >&2 + exit 1 + } + RELEASE_ID="$(jq -er '.id | numbers' "$RELEASE_RESPONSE")" + } + + if [[ -z "${RELEASE_ID:-}" ]]; then + RELEASE_PAYLOAD="$(jq -n \ + --arg tag "v$VERSION" \ + --arg name "zemljiska-knjiga-vticnik $VERSION" \ + --arg target "$TARGET_COMMIT" \ + '{tag_name: $tag, name: $name, target_commitish: $target, draft: false, prerelease: false}')" + if ! CREATE_STATUS="$(curl --silent --show-error \ + --request POST \ + --header "Authorization: token $GITEA_TOKEN" \ + --header 'Content-Type: application/json' \ + --data "$RELEASE_PAYLOAD" \ + --write-out '%{http_code}' --output "$RELEASE_RESPONSE" \ + "$API_BASE/repos/$REPOSITORY/releases")"; then + echo 'Could not create the Gitea release.' >&2 + exit 1 + fi + case "$CREATE_STATUS" in + 2??) + RELEASE_ID="$(jq -er '.id | numbers' "$RELEASE_RESPONSE")" + ;; + 409|422) + fetch_release_by_tag + ;; + *) + echo "Gitea release creation returned HTTP $CREATE_STATUS" >&2 + cat "$RELEASE_RESPONSE" >&2 + exit 1 + ;; + esac + fi + + if [[ "${RELEASE_ASSET_EXISTS:-false}" != 'true' ]]; then + curl --fail --silent --show-error \ + --header "Authorization: token $GITEA_TOKEN" \ + --output "$RELEASE_RESPONSE" \ + "$API_BASE/repos/$REPOSITORY/releases/$RELEASE_ID" + inspect_assets + fi + + if [[ "${RELEASE_ASSET_EXISTS:-false}" != 'true' ]]; then + UPLOAD_RESPONSE="$RUNNER_TEMP/gitea-upload-$VERSION.json" + if ! UPLOAD_STATUS="$(curl --silent --show-error \ + --request POST \ + --header "Authorization: token $GITEA_TOKEN" \ + --header 'Content-Type: application/x-xpinstall' \ + --data-binary "@$SIGNED_XPI" \ + --write-out '%{http_code}' --output "$UPLOAD_RESPONSE" \ + "$API_BASE/repos/$REPOSITORY/releases/$RELEASE_ID/assets?name=$ASSET_NAME")"; then + echo 'Could not upload the signed XPI to the Gitea release.' >&2 + exit 1 + fi + case "$UPLOAD_STATUS" in + 2??) + ;; + 409|422) + curl --fail --silent --show-error \ + --header "Authorization: token $GITEA_TOKEN" \ + --output "$RELEASE_RESPONSE" \ + "$API_BASE/repos/$REPOSITORY/releases/$RELEASE_ID" + inspect_assets + [[ "${RELEASE_ASSET_EXISTS:-false}" == 'true' ]] || { + echo 'Gitea rejected the upload but the expected asset is still absent.' >&2 + exit 1 + } + ;; + *) + echo "Gitea asset upload returned HTTP $UPLOAD_STATUS" >&2 + cat "$UPLOAD_RESPONSE" >&2 + exit 1 + ;; + esac + fi + + [[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || { + echo 'Could not calculate a valid SHA-256 digest for the signed XPI.' >&2 + exit 1 + } + UPDATE_LINK="https://git.vahta.net/komelt/zemljiska-knjiga-vticnik/releases/download/v$VERSION/$ASSET_NAME" + UPDATE_MANIFEST="$RUNNER_TEMP/zemljiska-knjiga-vticnik-$VERSION-updates.json" + jq -n \ + --arg id 'eprostor-gurs-batch@example.invalid' \ + --arg version "$VERSION" \ + --arg update_link "$UPDATE_LINK" \ + --arg update_hash "sha256:$XPI_SHA256" \ + '{addons: {($id): {updates: [{version: $version, update_link: $update_link, update_hash: $update_hash}]}}}' \ + > "$UPDATE_MANIFEST" + jq -e . "$UPDATE_MANIFEST" >/dev/null + printf 'XPI_SHA256=%s\nUPDATE_MANIFEST=%s\n' "$XPI_SHA256" "$UPDATE_MANIFEST" >> "$GITHUB_ENV" + + - name: Commit update manifest + shell: bash + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + set -Eeuo pipefail + export LC_ALL=C + + normalize_component() { + local component="$1" + component="${component#${component%%[!0]*}}" + printf '%s\n' "${component:-0}" + } + + compare_firefox_versions() { + local left="$1" + local right="$2" + local left_part right_part left_normalized right_normalized + local max_parts i + local -a left_parts right_parts + IFS='.' read -r -a left_parts <<< "$left" + IFS='.' read -r -a right_parts <<< "$right" + max_parts="${#left_parts[@]}" + [[ "${#right_parts[@]}" -gt "$max_parts" ]] && max_parts="${#right_parts[@]}" + for ((i = 0; i < max_parts; i += 1)); do + left_part="${left_parts[i]:-0}" + right_part="${right_parts[i]:-0}" + left_normalized="$(normalize_component "$left_part")" + right_normalized="$(normalize_component "$right_part")" + if [[ "${#left_normalized}" -lt "${#right_normalized}" ]]; then + printf '%s\n' -1 + return + fi + if [[ "${#left_normalized}" -gt "${#right_normalized}" ]]; then + printf '%s\n' 1 + return + fi + if [[ "$left_normalized" < "$right_normalized" ]]; then + printf '%s\n' -1 + return + fi + if [[ "$left_normalized" > "$right_normalized" ]]; then + printf '%s\n' 1 + return + fi + done + printf '%s\n' 0 + } + + compare_current_manifest_versions() { + local current_version comparison + [[ -f updates.json ]] || return 0 + jq -e . updates.json >/dev/null + while IFS= read -r current_version; do + [[ -z "$current_version" ]] && continue + if [[ ! "$current_version" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then + echo "Current updates.json contains an invalid Firefox version: $current_version" >&2 + exit 1 + fi + comparison="$(compare_firefox_versions "$VERSION" "$current_version")" + if [[ "$comparison" != '1' ]]; then + echo "Current update version $current_version is newer than or equal to $VERSION; skipping manifest commit." + return 1 + fi + done < <(jq -er '.addons[]?.updates[]?.version // empty' updates.json) + return 0 + } + + for attempt in 1 2 3; do + git fetch --prune origin main + git reset --hard origin/main + + if ! compare_current_manifest_versions; then + exit 0 + fi + cp -- "$UPDATE_MANIFEST" updates.json + jq -e . updates.json >/dev/null + + git config user.name 'gitea-actions[bot]' + git config user.email 'gitea-actions[bot]@users.noreply.git.vahta.net' + git add -- updates.json + if git diff --cached --quiet; then + echo 'updates.json is already current; nothing to commit.' + exit 0 + fi + git commit --only updates.json -m "chore: publish Firefox update $VERSION" + if git push origin HEAD:main; then + exit 0 + fi + echo "The main branch advanced during manifest publication; retrying ($attempt/3)." >&2 + git reset --hard origin/main + sleep "$((attempt * 2))" + done + echo 'Could not publish updates.json after three bounded retries.' >&2 + exit 1 diff --git a/README.md b/README.md index dbec877..24f763c 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ Neuradna Firefox WebExtension za pomoč pri ponavljajočem se izpolnjevanju obra - Izrecno dejanje uporabnika za zagon je potrebno v trenutnem zavihku. - Razširitev je omejena na `https://esodisce.si/*`. - Razširitev ne uporablja Firefox Downloads API-ja in ne shranjuje PDF-datotek; samo izpolni obrazec in pritisne gumb `btn_pdf`. +- Vhodno datoteko hrani samo v lokalni shrambi razširitve; ne zbira in ne pošilja telemetrije ali podatkov razvijalcu. -## Namestitev v Firefox +## Razvojna/testna namestitev v Firefox 1. Odprite `about:debugging`. 2. Izberite **This Firefox** / **Ta Firefox**. @@ -21,6 +22,19 @@ Neuradna Firefox WebExtension za pomoč pri ponavljajočem se izpolnjevanju obra Za spremembe datotek razširitev odstranite in jo znova naložite v `about:debugging`. Sidebar ostane odprt med uporabo strani in izbiro datoteke; zadnja uspešno prebrana datoteka se shrani v lokalno shrambo razširitve. +## Produkcijska namestitev in samodejne posodobitve + +Produkcijske izdaje objavlja Gitea Actions v javnem repozitoriju [komelt/zemljiska-knjiga-vticnik](https://git.vahta.net/komelt/zemljiska-knjiga-vticnik). Za produkcijsko uporabo prenesite in namestite podpisani XPI iz javnega Gitea Release-a. Ne nameščajte ga prek `about:debugging`, ker je taka namestitev začasna in nima stabilnega mehanizma za samodejne posodobitve. + +Za nastavitev repozitorija mora biti repozitorij javen; javno morajo biti berljivi tudi Release asseti in surove datoteke, zlasti `updates.json` na stabilnem naslovu. V Gitea omogočite Actions in nastavite vgrajeni Actions token tega opravila na pravice `contents: write` (za potisk veje `main` in Release API). **Ne ustvarjajte skrivnosti `GITEA_TOKEN`**: `secrets.GITEA_TOKEN` je vgrajeni token opravila, ne uporabniško določena skrivnost. Ustvarite samo naslednji skrivnosti: + +- `AMO_JWT_ISSUER` – Mozilla AMO JWT issuer; +- `AMO_JWT_SECRET` – Mozilla AMO JWT secret. + +Vsak potisk v `main` ustvari podpisano AMO unlisted izdajo, objavi XPI v javnem Release-u in posodobi `updates.json`; samodejni potisk samo te datoteke ne sproži nove izdaje. Razširitev uporablja `browser_specific_settings.gecko.update_url` do surove datoteke na veji `main`, zato Firefox preverja nove izdaje po namestitvi podpisanega XPI. Osnovna različica je v izvornem `manifest.json`, akcija pa ji samodejno doda številko zagona, na primer `0.2.0` postane `0.2.0.17`. + +Za izvajanje je potreben registriran Gitea runner z oznako `ubuntu-latest`. Na runnerju morajo biti na voljo `git`, `bash`, Node.js/npm/npx, `jq`, `curl` in `sha256sum`. Strežnik `git.vahta.net` mora imeti HTTPS-certifikat, ki mu zaupata Firefox in runnerji; sicer namestitev ter preverjanje in samodejne posodobitve ne bodo delovali. + ## Vhodna datoteka Vsaka vrstica vsebuje dve polji: **številčno šifro KO** in **parcelno številko**. Podprta so ločila `;`, tabulator in vejica. Prvo polje mora biti pozitivno celo število; vodilne ničle se ohranijo kot del niza. Drugo polje ostane niz, zato so podprte oblike, kot je `901/29`. diff --git a/background.js b/background.js new file mode 100644 index 0000000..47eef75 --- /dev/null +++ b/background.js @@ -0,0 +1,5 @@ +browser.action.onClicked.addListener(() => { + browser.sidebarAction.toggle().catch((error) => { + console.error('[eProstor GURS] Sidebar ni bilo mogoče preklopiti.', error); + }); +}); diff --git a/content-script.js b/content-script.js new file mode 100644 index 0000000..4c1d1c2 --- /dev/null +++ b/content-script.js @@ -0,0 +1,125 @@ +(() => { + if (window.__eprostorGursHelperLoaded) return; + window.__eprostorGursHelperLoaded = true; + + const SELECTORS = { + koNumber: 'idZnakNep.katastrskaObcina.idsrcsifrant', + parcel: 'idZnakNep.parcelnaStevilka' + }; + const LOG_PREFIX = '[eProstor GURS]'; + let stopRequested = false; + let running = false; + + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const report = (text) => browser.runtime.sendMessage({ type: 'BATCH_STATUS', text }).catch(() => {}); + const log = (...args) => console.log(LOG_PREFIX, ...args); + const logError = (...args) => console.error(LOG_PREFIX, ...args); + + function findField(selector) { + // getElementById avoids CSS escaping issues because the literal IDs contain dots. + return document.getElementById(selector); + } + + function setNativeValue(element, value) { + element.focus(); + const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value')?.set; + if (setter) setter.call(element, value); else element.value = value; + element.dispatchEvent(new Event('input', { bubbles: true })); + element.dispatchEvent(new Event('change', { bubbles: true })); + } + + async function processRecord(record, index, total, confirmMode) { + if (stopRequested) return false; + log(`Začetek zapisa ${index + 1}/${total}`, { + koNumber: record.koNumber, + parcel: record.parcel + }); + const koNumber = findField(SELECTORS.koNumber); + const parcel = findField(SELECTORS.parcel); + log('Polji najdeni', { + koField: SELECTORS.koNumber, + koFieldFound: Boolean(koNumber), + parcelField: SELECTORS.parcel, + parcelFieldFound: Boolean(parcel) + }); + if (!koNumber || !parcel) throw new Error('Polj z ID-jema ni mogoče najti; preverite selectorje v content-script.js.'); + + if (confirmMode === 'manual' && !window.confirm(`Obdelam ${index + 1}/${total}: KO ${record.koNumber} / parcela ${record.parcel}?`)) { + log(`Zapis ${index + 1}/${total} preskočen na ročno zahtevo`); + return true; + } + log('Neposreden vnos KO', { recordNumber: index + 1, koNumber: record.koNumber }); + setNativeValue(koNumber, record.koNumber); + log('Neposreden vnos parcele', { recordNumber: index + 1, parcel: record.parcel }); + setNativeValue(parcel, record.parcel); + + const button = document.getElementById('btn_pdf'); + log('btn_pdf preverjen', { recordNumber: index + 1, found: Boolean(button) }); + if (!button) throw new Error('Gumba z ID-jem »btn_pdf« ni mogoče najti.'); + button.click(); + log('btn_pdf kliknjen', { recordNumber: index + 1 }); + report(`Zapis ${index + 1}/${total}: vrednosti vnesene in potrjene z gumbom btn_pdf.`); + log(`Zapis ${index + 1}/${total} uspešno obdelan`); + return true; + } + + function describeError(error) { + const isObject = error !== null && (typeof error === 'object' || typeof error === 'function'); + return { + message: isObject && error.message !== undefined ? String(error.message) : String(error), + name: isObject && error.name !== undefined ? String(error.name) : typeof error, + cause: isObject ? error.cause : undefined, + stack: isObject ? error.stack : undefined + }; + } + + function getDomDiagnostics() { + return { + koFieldPresent: Boolean(findField(SELECTORS.koNumber)), + parcelFieldPresent: Boolean(findField(SELECTORS.parcel)), + btnPdfPresent: Boolean(document.getElementById('btn_pdf')) + }; + } + + browser.runtime.onMessage.addListener(async (message) => { + if (message.type === 'STOP_BATCH') { + stopRequested = true; + log('Zahtev za ustavitev prejet', { state: 'stop-requested' }); + return { message: 'Ustavljanje…' }; + } + if (message.type !== 'START_BATCH') return undefined; + if (running) return { message: 'Obdelava že teče.' }; + running = true; + stopRequested = false; + const { records, confirmMode = 'manual', delayMs = 1500 } = message; + log('Batch začet', { total: records.length, confirmMode, delayMs }); + try { + for (let i = 0; i < records.length && !stopRequested; i += 1) { + try { + await processRecord(records[i], i, records.length, confirmMode); + } catch (error) { + const errorDetails = describeError(error); + logError(`Napaka pri zapisu ${i + 1}/${records.length}`, { + recordNumber: i + 1, + total: records.length, + record: records[i], + error: errorDetails, + dom: getDomDiagnostics() + }); + report(`Zapis ${i + 1}: ${errorDetails.message}`); + if (confirmMode === 'manual' && !window.confirm(`Napaka pri zapisu ${i + 1}: ${errorDetails.message}\nNadaljujem?`)) break; + } + await sleep(delayMs); + } + const messageText = stopRequested ? 'Obdelava ustavljena.' : 'Obdelava končana.'; + log(messageText, { + state: stopRequested ? 'stopped' : 'completed', + total: records.length + }); + report(messageText); + return { message: messageText }; + } finally { + running = false; + } + }); +})(); diff --git a/example-input.txt b/example-input.txt new file mode 100644 index 0000000..f607d18 --- /dev/null +++ b/example-input.txt @@ -0,0 +1,6 @@ +Šifra KO;Parcelna številka +2491;901/29 +2491;901/30 +0107;1820 +0107;1861 +1535;1192/1 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..c058b76 --- /dev/null +++ b/manifest.json @@ -0,0 +1,36 @@ +{ + "manifest_version": 3, + "name": "eSodisce / GURS batch helper", + "version": "0.2.0", + "description": "Assists with repeated eSodisce/GURS parcel form entries. It never handles certificates, keys, PINs or PDF downloads.", + "permissions": ["activeTab", "storage"], + "host_permissions": ["https://esodisce.si/*"], + "background": { + "scripts": ["background.js"] + }, + "action": { + "default_title": "eSodisce GURS batch helper" + }, + "sidebar_action": { + "default_title": "eSodisce / GURS batch helper", + "default_panel": "popup.html", + "open_at_install": false + }, + "content_scripts": [ + { + "matches": ["https://esodisce.si/*"], + "js": ["content-script.js"], + "run_at": "document_idle" + } + ], + "browser_specific_settings": { + "gecko": { + "id": "eprostor-gurs-batch@example.invalid", + "strict_min_version": "109.0", + "update_url": "https://git.vahta.net/komelt/zemljiska-knjiga-vticnik/raw/branch/main/updates.json", + "data_collection_permissions": { + "required": ["none"] + } + } + } +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..c7ab486 --- /dev/null +++ b/popup.css @@ -0,0 +1,14 @@ +:root { color-scheme: light dark; font: 14px system-ui, sans-serif; } +body { width: 100%; box-sizing: border-box; margin: 0; background: Canvas; color: CanvasText; } +main { padding: 14px; } +h1 { font-size: 18px; margin: 0 0 4px; } +.muted { color: GrayText; font-size: 12px; } +label, legend { display: block; margin-top: 10px; font-weight: 600; } +input[type=file], input[type=number] { box-sizing: border-box; width: 100%; margin-top: 4px; } +fieldset { margin: 12px 0; padding: 8px; } +fieldset label { margin-top: 5px; font-weight: 400; } +button { width: 100%; margin-top: 14px; padding: 8px; border: 0; border-radius: 5px; background: #1769aa; color: white; cursor: pointer; } +button:disabled { opacity: .5; cursor: not-allowed; } +button.secondary { background: #666; } +#status { min-height: 2.5em; margin-top: 10px; white-space: pre-wrap; } +details { margin-top: 10px; font-size: 12px; } diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..58d3d01 --- /dev/null +++ b/popup.html @@ -0,0 +1,32 @@ + + + + + + eSodisce / GURS batch helper + + + +
+

eSodisce / GURS

+

Pomočnik za zaporedno izpolnjevanje obrazca po šifri KO.

+ + +
Datoteka še ni izbrana.
+ +
+ Potrjevanje + + +
+ + + + + + +
Pripravljeno.
+
+ + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..484ff0b --- /dev/null +++ b/popup.js @@ -0,0 +1,75 @@ +(() => { + const inputFile = document.querySelector('#inputFile'); + const fileInfo = document.querySelector('#fileInfo'); + const startButton = document.querySelector('#startButton'); + const stopButton = document.querySelector('#stopButton'); + const delayInput = document.querySelector('#delayMs'); + const status = document.querySelector('#status'); + let records = []; + + function updateLoadedFile(fileName) { + fileInfo.textContent = `${fileName}: ${records.length} veljavnih zapisov`; + startButton.disabled = records.length === 0; + } + + function setStatus(text) { status.textContent = text; } + + function parseLine(line) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) return null; + // Prefer tab, then semicolon, then comma; exactly two fields are required. + const separator = trimmed.includes('\t') ? '\t' : (trimmed.includes(';') ? ';' : ','); + const fields = trimmed.split(separator).map((field) => field.trim().replace(/^"|"$/g, '')); + if (fields.length !== 2 || !fields[0] || !fields[1]) return null; + const header = `${fields[0]} ${fields[1]}`.toLocaleLowerCase('sl'); + if (header.includes('katastr') && header.includes('parcel')) return null; + if (!/^\d+$/.test(fields[0]) || /^0+$/.test(fields[0])) return null; + return { koNumber: fields[0], parcel: fields[1] }; + } + + inputFile.addEventListener('change', async () => { + const file = inputFile.files?.[0]; + if (!file) return; + const lines = (await file.text()).split(/\r?\n/); + records = lines.map(parseLine).filter(Boolean); + await browser.storage.local.set({ batchInput: { fileName: file.name, records } }); + updateLoadedFile(file.name); + setStatus(records.length ? 'Pripravljeno za trenutni zavihek.' : 'Ni veljavnih zapisov.'); + }); + + browser.storage.local.get('batchInput').then(({ batchInput }) => { + if (!batchInput || !Array.isArray(batchInput.records) || !batchInput.records.length) return; + records = batchInput.records; + updateLoadedFile(batchInput.fileName || 'Shranjena vhodna datoteka'); + setStatus('Naložena je zadnja izbrana vhodna datoteka.'); + }); + + startButton.addEventListener('click', async () => { + if (!records.length) return; + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + if (!tab?.id) { setStatus('Ciljnega zavihka ni mogoče najti.'); return; } + const confirmMode = document.querySelector('input[name="confirmMode"]:checked').value; + const delayMs = Math.max(500, Number.parseInt(delayInput.value, 10) || 1500); + try { + const response = await browser.tabs.sendMessage(tab.id, { type: 'START_BATCH', records, confirmMode, delayMs }); + setStatus(response?.message || 'Obdelava zagnana.'); + } catch (error) { + setStatus(`Napaka: odprite ciljno stran in poskusite znova. ${error.message}`); + } + }); + + stopButton.addEventListener('click', async () => { + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + if (!tab?.id) { setStatus('Ciljnega zavihka ni mogoče najti.'); return; } + try { + const response = await browser.tabs.sendMessage(tab.id, { type: 'STOP_BATCH' }); + setStatus(response?.message || 'Zahteva za ustavitev poslana.'); + } catch (error) { + setStatus(`Napaka: odprite ciljno stran in poskusite znova. ${error.message}`); + } + }); + + browser.runtime.onMessage.addListener((message) => { + if (message.type === 'BATCH_STATUS') setStatus(message.text); + }); +})(); diff --git a/updates.json b/updates.json new file mode 100644 index 0000000..8bfa34a --- /dev/null +++ b/updates.json @@ -0,0 +1,7 @@ +{ + "addons": { + "eprostor-gurs-batch@example.invalid": { + "updates": [] + } + } +}