#!/bin/bash # # Catalog sources — the ordered list of app-catalog URLs the App Center browses. # --------------------------------------------------------------------------- # SOURCE OF TRUTH = the CFG_CATALOG_1..9 config vars in configs/general/ # general_catalogs (edited from the WebUI Catalog Sources block, like domains). # registry_catalog.json is a GENERATED artifact derived from these + the official # source — rebuilt by webuiRegistryCatalogScan on config save + `updater check`. # # Source #1 is ALWAYS the official catalog, synthesized live from # CFG_RELEASE_BASE_URL / CFG_RELEASE_CHANNEL (pinned, NOT in general_catalogs). # Each extra slot CFG_CATALOG_ maps to source idx (a stable id the Add # dialog / `app add --source` can reference) and is UNVERIFIED third-party. # # Priority = official first, then slots 1..9 ascending; the highest-priority # source offering an app is the default (Add dialog can pick another). # # SCOPE: browse + add only. System updates/hotfixes still resolve from the # official CFG_RELEASE_BASE_URL alone (lpFetchIndex untouched) — a third-party # catalog can never become a system-update channel. CATALOG_MAX_SLOTS=9 # Extra (third-party) sources as a JSON array [{idx,name,base,channel}] in slot # order, skipping empty slots. A slot value is "url" or "url|channel". catalogExtraSourcesJson() { command -v jq >/dev/null 2>&1 || { echo '[]'; return 0; } local arr='[]' n var val url channel host for (( n=1; n<=CATALOG_MAX_SLOTS; n++ )); do var="CFG_CATALOG_${n}"; val="${!var:-}" [[ -z "$val" ]] && continue url="${val%%|*}" channel="stable"; [[ "$val" == *"|"* ]] && channel="${val##*|}" [[ "$url" =~ ^https?://[^[:space:]/]+ ]] || continue url="${url%/}" host="${url#*://}"; host="${host%%/*}" arr="$(jq -c --argjson i "$(( n + 1 ))" --arg name "$host" --arg base "$url" --arg ch "$channel" \ '. + [ { idx:$i, name:$name, base:$base, channel:$ch } ]' <<<"$arr")" done printf '%s' "$arr" } # The full ordered source list as a JSON array. Each: # {idx,name,base,channel,official,trust,enabled}. The official source can be # switched off for browsing via CFG_CATALOG_OFFICIAL_ENABLED (default on) — # extras are always enabled (an empty slot is simply absent). catalogSourcesJson() { local off_base off_channel extras off_enabled off_base="$(lpReleaseBaseUrl)"; off_base="${off_base%/}" off_channel="$(lpReleaseChannel)" off_enabled="true"; [[ "${CFG_CATALOG_OFFICIAL_ENABLED:-true}" == "false" ]] && off_enabled="false" extras="$(catalogExtraSourcesJson)" jq -cn --arg base "$off_base" --arg channel "$off_channel" --argjson oe "$off_enabled" --argjson extras "$extras" ' [ { idx: 1, name: "LibrePortal Official", base: $base, channel: $channel, official: true, trust: "official", enabled: $oe } ] + ( $extras | map( . + { official: false, trust: "community", enabled: true } ) )' } # One ENABLED source per line (compact JSON), priority order — for `while read`. catalogEnabledSources() { catalogSourcesJson | jq -c '.[] | select(.enabled)'; } # Human-readable listing for the CLI. catalogSourceList() { catalogSourcesJson | jq -r '.[] | "[\(.idx)] \(.name) — \(.base)/\(.channel) (\(if .official then "official ✓" else "unverified" end)\(if .enabled then "" else ", off" end))"' } # Fetch a THIRD-PARTY catalog index. Deliberately NOT verified against the # official key and does NOT touch the anti-rollback serial — community sources # are UNVERIFIED (their payloads are still sha256-pinned at add time by the pin # carried in this index). Echoes the index JSON; non-zero on failure. catalogFetchCommunityIndex() { local base="$1" channel="$2" tmp idx json base="${base%/}" [[ -n "$base" && -n "$channel" ]] || return 1 command -v jq >/dev/null 2>&1 || return 1 [[ -n "$(_lpFetchTool)" ]] || return 1 tmp="$(mktemp -d)"; idx="$tmp/index.json" if ! _lpDownload "$base/$channel/index.json" "$idx" 2>/dev/null; then rm -rf "$tmp"; return 1; fi json="$(cat "$idx")"; rm -rf "$tmp" jq empty <<<"$json" 2>/dev/null || return 1 printf '%s' "$json" } # --- Mutations: write the CFG_CATALOG_ vars via the config system --------- # catalogSourceAdd [channel] — into the first free slot. catalogSourceAdd() { local url="$1" channel="$2" if [[ ! "$url" =~ ^https?://[^[:space:]/]+ ]]; then isError "catalog: source URL must be http(s)://…"; return 1 fi url="${url%/}" local stored="$url"; [[ -n "$channel" && "$channel" != "stable" ]] && stored="$url|$channel" if catalogSourcesJson | jq -e --arg b "$url" 'any(.[]; .base==$b)' >/dev/null 2>&1; then isNotice "catalog: '$url' is already a source."; return 0 fi local n var for (( n=1; n<=CATALOG_MAX_SLOTS; n++ )); do var="CFG_CATALOG_${n}" if [[ -z "${!var:-}" ]]; then updateConfigOption "$var" "$stored" >/dev/null || { isError "catalog: failed to write $var."; return 1; } isSuccessful "catalog: added source '$url' (slot $n)." catalogRefresh return 0 fi done isError "catalog: all $CATALOG_MAX_SLOTS catalog slots are full — remove one first."; return 1 } # catalogSourceRemove — idx 1 = official (fixed); idx N+1 = CFG_CATALOG_N. catalogSourceRemove() { local idx="$1" [[ "$idx" =~ ^[0-9]+$ ]] || { isError "catalog: must be a number."; return 1; } (( idx == 1 )) && { isError "catalog: source 1 is the official catalog and can't be removed."; return 1; } local slot=$(( idx - 1 )) var="CFG_CATALOG_$(( idx - 1 ))" (( slot < 1 || slot > CATALOG_MAX_SLOTS )) && { isError "catalog: no source with idx $idx."; return 1; } [[ -z "${!var:-}" ]] && { isError "catalog: no source with idx $idx."; return 1; } updateConfigOption "$var" "" >/dev/null || { isError "catalog: failed to clear $var."; return 1; } isSuccessful "catalog: removed source $idx." catalogRefresh } # Re-run the App Center browse scan so a source change reflects immediately. catalogRefresh() { declare -F webuiRegistryCatalogScan >/dev/null 2>&1 || \ source "${install_scripts_dir}webui/data/generators/apps/webui_registry_scan.sh" 2>/dev/null declare -F webuiRegistryCatalogScan >/dev/null 2>&1 && webuiRegistryCatalogScan }