diff --git a/.github/semgrep/strip-suppressed.py b/.github/semgrep/strip-suppressed.py new file mode 100644 index 0000000..17437e5 --- /dev/null +++ b/.github/semgrep/strip-suppressed.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Remove do SARIF os achados já suprimidos in-source (`nosemgrep`). + +Por que isto existe +------------------- +Um comentário `nosemgrep` na linha tira o achado do exit code do semgrep — +o gate bloqueante do PR nunca reprova por causa dele. Mas o semgrep ainda +emite o achado no SARIF, marcado com `"suppressions": [{"kind": +"inSource"}]`. O GitHub code scanning entende esse campo, porém o bot de +review (`github-advanced-security`) posta comentário de PR mesmo assim. +O resultado é uma enxurrada de alerta em linha que já carrega +justificativa escrita ao lado — ruído que treina o revisor a ignorar +alerta de segurança, que é exatamente o oposto do que a camada serve. + +Filtramos apenas a cópia que vai para o code scanning. O SARIF íntegro, +com os suprimidos, continua sendo publicado como artefato do job (ver +`.github/workflows/security.yml`), então o histórico de "o que foi +suprimido e onde" não se perde. + +Falha aberta de propósito: se o arquivo não existir, não for JSON válido +ou tiver formato inesperado, copiamos a entrada para a saída sem filtrar. +Perder o upload de segurança inteiro por causa de um erro de parsing +seria pior do que publicar achados a mais. + +Uso: + python3 strip-suppressed.py entrada.sarif saida.sarif +""" + +from __future__ import annotations + +import json +import shutil +import sys + + +def strip_suppressed(src: str, dst: str) -> int: + try: + with open(src, encoding="utf-8") as handle: + document = json.load(handle) + except (OSError, ValueError) as exc: + print(f"{src}: ilegível ({exc}) — copiado sem filtrar", file=sys.stderr) + try: + shutil.copyfile(src, dst) + except OSError: + pass + return 0 + + runs = document.get("runs") + if not isinstance(runs, list): + print(f"{src}: sem `runs` — copiado sem filtrar", file=sys.stderr) + shutil.copyfile(src, dst) + return 0 + + removed = 0 + for run in runs: + if not isinstance(run, dict): + continue + results = run.get("results") + if not isinstance(results, list): + continue + kept = [r for r in results if not (isinstance(r, dict) and r.get("suppressions"))] + removed += len(results) - len(kept) + run["results"] = kept + + with open(dst, "w", encoding="utf-8") as handle: + json.dump(document, handle) + + print(f"{src}: {removed} achado(s) suprimido(s) removido(s) do upload") + return removed + + +def main(argv: list[str]) -> int: + if len(argv) != 3: + print(__doc__, file=sys.stderr) + return 2 + strip_suppressed(argv[1], argv[2]) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 191866a..7c09079 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -65,6 +65,18 @@ jobs: ls -l semgrep-generic.sarif exit 0 + # Achado com `nosemgrep` na linha já não entra no exit code do + # semgrep, mas ainda vai para o SARIF marcado como `inSource`. O bot + # de review do GitHub anota mesmo assim, enchendo o PR de alerta em + # linha que carrega a justificativa ao lado. Filtramos só a cópia + # destinada ao code scanning; o SARIF íntegro segue no artefato. + - name: Filtrar achados suprimidos do upload — genérico + if: always() + run: | + IN=semgrep-generic.sarif + OUT=semgrep-generic-clean.sarif + python3 .github/semgrep/strip-suppressed.py "$IN" "$OUT" || cp "$IN" "$OUT" + - name: Upload SARIF — genérico if: always() # Code scanning pode não estar habilitado (repo privado sem GitHub @@ -73,7 +85,7 @@ jobs: continue-on-error: true uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: - sarif_file: semgrep-generic.sarif + sarif_file: semgrep-generic-clean.sarif category: semgrep # ---- Camada 2: regras específicas de Flarum v2 -------------------- @@ -111,12 +123,20 @@ jobs: [ "$BLOCK" = "1" ] && exit $EXIT exit 0 + # Mesmo motivo da camada 1 — ver o comentário lá. + - name: Filtrar achados suprimidos do upload — Flarum v2 + if: always() + run: | + IN=semgrep-flarum.sarif + OUT=semgrep-flarum-clean.sarif + python3 .github/semgrep/strip-suppressed.py "$IN" "$OUT" || cp "$IN" "$OUT" + - name: Upload SARIF — Flarum v2 if: always() continue-on-error: true uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: - sarif_file: semgrep-flarum.sarif + sarif_file: semgrep-flarum-clean.sarif category: flarum-v2-security # Canal de resultados independente do code scanning estar ligado: @@ -129,5 +149,7 @@ jobs: path: | semgrep-generic.sarif semgrep-flarum.sarif + semgrep-generic-clean.sarif + semgrep-flarum-clean.sarif retention-days: 30 if-no-files-found: warn diff --git a/extend.php b/extend.php index c105783..1299230 100644 --- a/extend.php +++ b/extend.php @@ -21,6 +21,7 @@ use Ramon\Backup\Console\ExportCommand; use Ramon\Backup\Console\ImportCommand; use Ramon\Backup\Console\PruneStaleJobsCommand; +use Ramon\Backup\Settings\SettingsPreserver; return [ (new Extend\Frontend('admin')) @@ -46,7 +47,15 @@ // Same trust model as ramon/verified — the matching PRIVATE key is // pasted into config.php under `backup-private-key`, never stored // in the database. - ->default('ramon-backup.encryption_public_key', ''), + ->default('ramon-backup.encryption_public_key', '') + /* + * Padrões extra de chaves de `settings` que um restore não pode + * sobrescrever, um por linha (sufixo `*` casa por prefixo). O + * baseline — SMTP, fila, tokens desta instalação — está em + * SettingsPreserver::BASELINE; esta chave existe para o operador + * acrescentar o que for específico do ambiente dele. + */ + ->default(SettingsPreserver::EXTRA_KEY, ''), (new Extend\Routes('api')) ->get('/backup/backups', 'backup.list', ListBackupsController::class) diff --git a/js/dist/admin.js b/js/dist/admin.js index cedfc1b..c2741a7 100644 --- a/js/dist/admin.js +++ b/js/dist/admin.js @@ -1,2 +1,2 @@ -(()=>{const t={n:e=>{const s=e&&e.__esModule?()=>e.default:()=>e;return t.d(s,{a:s}),s},d:(e,s)=>{if(Array.isArray(s))for(var a=0;aObject.prototype.hasOwnProperty.call(t,e)};(()=>{"use strict";const e=flarum.reg.get("core","admin/app");var s=t.n(e);const a=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","admin/components/ExtensionPage");var r=t.n(n);function o(t){return o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},o(t)}function i(t,e,s){return(e=function(t){var e=function(t){if("object"!=o(t)||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var s=e.call(t,"string");if("object"!=o(s))return s;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t);return"symbol"==o(e)?e:e+""}(e))in t?Object.defineProperty(t,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[e]=s,t}const l=flarum.reg.get("core","common/Component");var c=t.n(l);const u=flarum.reg.get("core","common/components/Button");var p=t.n(u);const d=flarum.reg.get("core","common/components/LoadingIndicator");var h=t.n(d);const g=flarum.reg.get("core","common/components/Modal");var b=t.n(g);const f=flarum.reg.get("core","common/utils/extractText");var y=t.n(f);function k(){return(s().forum.attribute("apiUrl")||"/api").replace(/\/+$/,"")}function _(t){if(!Number.isFinite(t)||t<=0)return"0 B";const e=["B","KB","MB","GB","TB"];let s=0,a=t;for(;a>=1024&&s=100||0===s?0:1)+" "+e[s]}function v(t,e){var a,n,r;const o=null!=(a=null!=(n=null==t||null==(r=t.response)||null==(r=r.errors)||null==(r=r[0])?void 0:r.detail)?n:null==t?void 0:t.detail)?a:"string"==typeof(null==t?void 0:t.message)?t.message:void 0;return o?String(o):e||String(s().translator.trans("ramon-backup.admin.errors.generic"))}async function x(t){try{return await s().request(t)}catch(e){const a=v(e,t.fallbackMessage);if(console.error("[backup] api error",t.method,t.url,e),!1!==t.surface&&s().alerts.show({type:"error"},a),e&&"object"==typeof e&&!e.detail)try{e.detail=a}catch(t){}throw e}}flarum.reg.add("ramon-backup","admin/utils/api",{apiUrl:k,fmtBytes:_,errorDetail:v});const B=(t,e)=>s().translator.trans("ramon-backup.admin.encryption.".concat(t),null!=e?e:{});class N extends(b()){constructor(){super(...arguments),i(this,"copied",!1)}className(){return"BackupRevealModal Modal--medium"}title(){return B("reveal_modal.title")}content(){const t=this.attrs,e=t.privateKey,s=t.configKey,a="'".concat(s,"' => '").concat(e,"',");return m("div",{className:"Modal-body"},m("p",null,B("reveal_modal.intro")),m("div",{className:"Alert Alert--error"},m("strong",null,B("reveal_modal.warning_title")),m("p",null,B("reveal_modal.warning_body"))),m("label",{className:"BackupReveal-label"},B("reveal_modal.snippet_label")),m("pre",{className:"BackupReveal-snippet"},m("code",null,a)),m("div",{className:"Form-group BackupReveal-actions"},m(p(),{className:"Button",icon:"fas fa-copy",onclick:()=>this.copy(a)},this.copied?B("reveal_modal.copied"):B("reveal_modal.copy_button")),m(p(),{className:"Button Button--primary",onclick:()=>this.hide()},B("reveal_modal.close"))))}copy(t){navigator.clipboard?navigator.clipboard.writeText(t).then(()=>{this.copied=!0,m.redraw(),setTimeout(()=>{this.copied=!1,m.redraw()},2e3)}).catch(t=>{console.error("[backup] clipboard writeText failed",t),s().alerts.show({type:"error"},B("clipboard_failed"))}):s().alerts.show({type:"error"},B("clipboard_unavailable"))}}class w extends(b()){constructor(){super(...arguments),i(this,"acknowledged",!1),i(this,"submitting",!1)}className(){return"BackupRegenerateModal Modal--medium"}title(){return B("regenerate_modal.title")}content(){return m("div",{className:"Modal-body"},m("div",{className:"Alert Alert--error"},m("p",null,B("regenerate_modal.warning"))),m("label",{className:"BackupRegenerate-confirm"},m("input",{type:"checkbox",checked:this.acknowledged,onchange:t=>{this.acknowledged=t.target.checked}})," ",B("regenerate_modal.acknowledge")),m("div",{className:"Form-group"},m(p(),{className:"Button Button--primary",loading:this.submitting,disabled:!this.acknowledged||this.submitting,onclick:()=>this.submit()},B("regenerate_modal.submit"))))}async submit(){this.submitting=!0,m.redraw();try{await this.attrs.onConfirm(),this.hide()}catch(t){this.submitting=!1,m.redraw()}}}class E extends(c()){constructor(){super(...arguments),i(this,"status",null),i(this,"loadState","loading"),i(this,"loadError",null),i(this,"publicCopied",!1)}oninit(t){super.oninit(t),this.refresh()}view(){return m("section",{className:"BackupEncryptionCard"},m("header",null,m("h3",null,B("section_title")),m("p",{className:"helpText"},B("section_help"))),"loading"===this.loadState&&m(h(),null),"error"===this.loadState&&m("div",{className:"Alert Alert--error BackupEncryption-loadError"},m("p",null,B("status.load_failed")),this.loadError&&m("p",{className:"helpText"},m("code",null,this.loadError)),m(p(),{className:"Button",icon:"fas fa-rotate",onclick:()=>this.refresh()},B("status.retry"))),"ok"===this.loadState&&this.body())}body(){if(!this.status)return m("p",{className:"helpText"},B("status.unknown"));const t=this.status;return t.available?m("[",null,m("div",{className:"BackupEncryption-statusRow"},this.statusBadge("public",t.has_public_key),this.statusBadge("private",t.private_key_present)),t.healthy&&m("div",{className:"Alert Alert--success"},B("status.healthy")),!t.has_public_key&&!t.private_key_present&&m("div",null,m("p",{className:"helpText"},B("status.not_setup")),m(p(),{className:"Button Button--primary",icon:"fas fa-key",onclick:()=>this.generate(!1)},B("actions.generate"))),t.has_public_key&&t.private_key_present&&!1===t.keys_match&&m("div",{className:"Alert Alert--error"},m("strong",null,B("status.mismatch_title")),m("p",null,B("status.mismatch_body")),m("p",null,m("code",null,"'",t.config_key,"'"))),t.has_public_key&&!t.private_key_present&&m("div",{className:"Alert Alert--error"},m("strong",null,B("status.private_missing_title")),m("p",null,B("status.private_missing_body")),m("p",null,m("code",null,"'",t.config_key,"'"))),t.has_public_key&&this.publicKeyPanel(t.public_key||"",t.healthy)):m("div",{className:"Alert Alert--error"},B("status.libsodium_missing"))}publicKeyPanel(t,e){return m("div",{className:"BackupEncryption-publicKey"},m("label",null,B("public_key.label")),m("div",{className:"BackupEncryption-publicKeyRow"},m("pre",null,m("code",null,t)),m(p(),{className:"Button Button--icon",icon:"fas fa-copy",title:y()(B("public_key.copy_title")),onclick:()=>this.copyPublic(t)},this.publicCopied?y()(B("public_key.copied")):"")),m("p",{className:"helpText"},B(e?"public_key.help_healthy":"public_key.help_broken")),m(p(),{className:"Button Button--danger",icon:"fas fa-rotate",onclick:()=>this.openRegenerate()},B("public_key.remove_button")))}statusBadge(t,e){return m("div",{className:"BackupEncryption-badge BackupEncryption-badge--".concat(e?"ok":"missing")},m("i",{className:"icon fas fa-".concat(e?"check":"times")}),m("span",null,B("status.".concat(t,"_key_label"))),m("span",{className:"BackupEncryption-badgeState"},B("status.".concat(e?"present":"absent"))))}copyPublic(t){t&&(navigator.clipboard?navigator.clipboard.writeText(t).then(()=>{this.publicCopied=!0,m.redraw(),setTimeout(()=>{this.publicCopied=!1,m.redraw()},2e3)}).catch(t=>{console.error("[backup] clipboard writeText failed",t),s().alerts.show({type:"error"},B("clipboard_failed"))}):s().alerts.show({type:"error"},B("clipboard_unavailable")))}refresh(){return this.loadState="loading",this.loadError=null,x({method:"GET",url:"".concat(k(),"/backup/encryption/status"),surface:!1}).then(t=>{this.status=t,this.loadState="ok"}).catch(t=>{this.status=null,this.loadState="error",this.loadError=v(t)}).then(()=>{m.redraw()})}async generate(t){try{const e=await x({method:"POST",url:"".concat(k(),"/backup/encryption/generate-keypair"),body:{acknowledge_loss:t},surface:!1});await this.refresh(),s().modal.show(N,{privateKey:e.private_key,configKey:e.config_key})}catch(t){throw s().alerts.show({type:"error"},v(t,String(B("actions.generate_failed")))),t}}openRegenerate(){s().modal.show(w,{onConfirm:()=>this.generate(!0)})}}flarum.reg.add("ramon-backup","admin/components/EncryptionCard",E);const S=flarum.reg.get("core","common/helpers/humanTime");var I=t.n(S);const O={mysql:"MySQL",mariadb:"MariaDB",postgres:"PostgreSQL",sqlite:"SQLite"},P=(t,e)=>s().translator.trans("ramon-backup.admin.list.".concat(t),null!=e?e:{});class T extends(c()){view(t){const e=t.attrs,s=e.backups,a=e.onDelete;return s.length?m("table",{className:"BackupList Table"},m("thead",null,m("tr",null,m("th",null,P("col_when")),m("th",null,P("col_size")),m("th",null,P("col_contents")),m("th",null,P("col_status")),m("th",null))),m("tbody",null,s.map(t=>m("tr",{key:t.id,className:"BackupList-row"},m("td",null,m("div",{className:"BackupList-when"},t.created_at?I()(new Date(t.created_at)):"—"),m("div",{className:"BackupList-filename"},t.filename),t.target_dialect&&m("div",{className:"BackupList-target BackupList-target--".concat(t.target_dialect),title:String(P("target_tooltip",{engine:O[t.target_dialect]||t.target_dialect}))},m("i",{className:"icon fas fa-arrow-right-arrow-left"})," ",P("target_for",{engine:O[t.target_dialect]||t.target_dialect}))),m("td",null,_(t.size_bytes)),m("td",null,t.contents.map(t=>m("span",{className:"BackupList-tag BackupList-tag--".concat(t)},P("content_"+t)))),m("td",null,t.encrypted?m("span",{className:"BackupList-encryption BackupList-encryption--on"},m("i",{className:"icon fas fa-lock"})," ",P("encrypted")):m("span",{className:"BackupList-encryption BackupList-encryption--off"},m("i",{className:"icon fas fa-lock-open"})," ",P("plain"))),m("td",{className:"BackupList-actions"},m("a",{className:"Button Button--icon",href:"".concat(k(),"/backup/backups/").concat(t.id,"/download"),target:"_blank",title:String(P("download_title"))},m("i",{className:"icon fas fa-download"})),m(p(),{className:"Button Button--icon Button--danger",icon:"fas fa-trash",title:P("delete_title"),onclick:()=>a(t.id)})))))):m("p",{className:"BackupList-empty helpText"},P("empty"))}}function j(t,e){(null==e||e>t.length)&&(e=t.length);for(var s=0,a=Array(e);ss().translator.trans("ramon-backup.admin.export_modal.".concat(t),null!=e?e:{});class D extends(b()){constructor(){super(...arguments),i(this,"stage","form"),i(this,"includeDb",!0),i(this,"includeAssets",!0),i(this,"includeStorage",!1),i(this,"includeExtensions",!1),i(this,"extensionsLoading",!1),i(this,"extensionsLoaded",!1),i(this,"extensions",[]),i(this,"extensionSelected",{}),i(this,"encryptionEnabled",!1),i(this,"encryptionUseExternal",!1),i(this,"externalPublicKey",""),i(this,"targetDialect",""),i(this,"starting",!1),i(this,"jobId",null),i(this,"status",null),i(this,"polling",!1)}className(){return"BackupExportModal Modal--medium"}title(){return M("title")}content(){return"form"===this.stage?this.formContent():this.progressContent()}formContent(){return m("div",{className:"Modal-body"},m("p",{className:"helpText"},M("intro")),m("fieldset",{className:"BackupExport-fieldset"},m("legend",null,M("contents_title")),this.checkbox("db",()=>this.includeDb,t=>this.includeDb=t),this.checkbox("assets",()=>this.includeAssets,t=>this.includeAssets=t),this.checkbox("storage",()=>this.includeStorage,t=>this.includeStorage=t),this.checkbox("extensions",()=>this.includeExtensions,t=>{this.includeExtensions=t,t&&!this.extensionsLoaded&&this.loadExtensions()}),this.includeExtensions&&this.extensionList()),this.includeDb&&m("fieldset",{className:"BackupExport-fieldset"},m("legend",null,M("target_title")),m("p",{className:"helpText"},M("target_help")),m("select",{className:"FormControl BackupExport-targetSelect",value:this.targetDialect,onchange:t=>{this.targetDialect=t.target.value}},m("option",{value:""},M("target_same")),m("option",{value:"mysql"},M("target_mysql")),m("option",{value:"mariadb"},M("target_mariadb")),m("option",{value:"postgres"},M("target_postgres")),m("option",{value:"sqlite"},M("target_sqlite")))),m("fieldset",{className:"BackupExport-fieldset"},m("legend",null,M("encryption_title")),m("label",{className:"BackupExport-checkbox"},m("input",{type:"checkbox",checked:this.encryptionEnabled,onchange:t=>{this.encryptionEnabled=t.target.checked}})," ",m("span",null,M("encryption_enable"))),m("p",{className:"helpText"},M("encryption_help")),this.encryptionEnabled&&m("[",null,m("label",{className:"BackupExport-checkbox"},m("input",{type:"checkbox",checked:this.encryptionUseExternal,onchange:t=>{this.encryptionUseExternal=t.target.checked}})," ",m("span",null,M("encryption_external"))),this.encryptionUseExternal&&m("[",null,m("p",{className:"helpText"},M("encryption_external_help")),m("textarea",{className:"FormControl BackupExport-keyInput",rows:3,placeholder:"base64 public key",value:this.externalPublicKey,oninput:t=>{this.externalPublicKey=t.target.value}})))),m("div",{className:"Form-group"},m(p(),{className:"Button Button--primary",loading:this.starting,disabled:this.starting||!this.canStart(),onclick:()=>this.start()},M("start_button"))))}extensionList(){if(this.extensionsLoading)return m("div",{className:"BackupExport-extLoading"},m(h(),null));if(!this.extensions.length)return m("p",{className:"helpText BackupExport-extEmpty"},M("extensions_none"));const t={workbench:[],vendor:[],unknown:[]};for(const s of this.extensions){var e;null==(e=t[s.location])||e.push(s)}return m("div",{className:"BackupExport-extList"},m("div",{className:"BackupExport-extActions"},m("button",{type:"button",className:"BackupExport-extLink",onclick:()=>this.toggleAllExtensions(!0)},M("extensions_select_all")),m("span",null," · "),m("button",{type:"button",className:"BackupExport-extLink",onclick:()=>this.toggleAllExtensions(!1)},M("extensions_select_none"))),["workbench","vendor","unknown"].filter(e=>t[e].length>0).map(e=>m("div",{className:"BackupExport-extGroup",key:e},m("div",{className:"BackupExport-extGroupHeader"},M("extensions_group_"+e)," ",m("span",{className:"helpText"},"(",t[e].length,")")),t[e].map(t=>m("label",{className:"BackupExport-extRow",key:t.id},m("input",{type:"checkbox",checked:!!this.extensionSelected[t.id],onchange:e=>{this.extensionSelected[t.id]=e.target.checked}})," ",m("span",{className:"BackupExport-extTitle"},t.title)," ",m("code",{className:"BackupExport-extName"},t.name||t.id),m("span",{className:"BackupExport-extTag BackupExport-extTag--".concat(t.location)},M("extensions_tag_"+t.location)))))))}toggleAllExtensions(t){for(const e of this.extensions)this.extensionSelected[e.id]=t}async loadExtensions(){this.extensionsLoading=!0;try{const t=await x({method:"GET",url:"".concat(k(),"/backup/extensions"),surface:!1});this.extensions=t.extensions||[],this.extensionsLoaded=!0;for(const t of this.extensions)this.extensionSelected[t.id]=!0}catch(t){s().alerts.show({type:"error"},v(t,String(M("extensions_load_failed"))))}finally{this.extensionsLoading=!1,m.redraw()}}checkbox(t,e,s){return m("label",{className:"BackupExport-checkbox"},m("input",{type:"checkbox",checked:e(),onchange:t=>{s(t.target.checked)}})," ",m("span",{className:"BackupExport-checkbox-label"},M("content_"+t)),m("span",{className:"BackupExport-checkbox-help helpText"},M("content_"+t+"_help")))}canStart(){return!(!(this.includeDb||this.includeAssets||this.includeStorage||this.includeExtensions)||this.encryptionEnabled&&this.encryptionUseExternal&&!this.externalPublicKey.trim())}progressContent(){var t,e,s;const a=this.status;if(!a)return m(h(),null);const n="done"===a.phase,r="error"===a.phase,o=Math.max(0,Math.min(100,(null==(t=a.progress)?void 0:t.percent)||0));return m("div",{className:"Modal-body BackupExport-progress"},m("div",{className:"BackupExport-status BackupExport-status--".concat(a.phase)},m("strong",null,M("phase_"+a.phase)),m("p",null,a.message)),!r&&m("[",null,m("div",{className:"BackupExport-bar"},m("div",{className:"BackupExport-bar-fill",style:{width:"".concat(n?100:o,"%")},role:"progressbar","aria-valuenow":o,"aria-valuemin":0,"aria-valuemax":100})),m("div",{className:"BackupExport-stats"},m("span",null,_(a.progress.processed_bytes)," /"," ",_(a.progress.total_bytes||a.progress.processed_bytes)),a.progress.total_files>0&&m("span",null,M("files_count",{done:a.progress.processed_files,total:a.progress.total_files})))),(null!=(e=null==(s=a.warnings)?void 0:s.length)?e:0)>0&&m("div",{className:"BackupExport-warnings",role:"alert"},m("div",{className:"BackupExport-warnings-title"},m("i",{className:"icon fas fa-triangle-exclamation"})," ",M("warnings_title",{count:a.warnings.length})),m("p",{className:"helpText"},M("warnings_help")),m("ul",{className:"BackupExport-warnings-list"},a.warnings.map((t,e)=>m("li",{key:e},t)))),m("div",{className:"Form-group BackupExport-progress-actions"},!n&&!r&&m(p(),{className:"Button",onclick:()=>this.cancel()},M("cancel_button")),(n||r)&&m(p(),{className:"Button Button--primary",onclick:()=>this.close()},M("close_button"))))}async start(){this.starting=!0;try{let t=!1;this.includeExtensions&&(t=!this.extensionsLoaded||Object.entries(this.extensionSelected).filter(t=>A(t,2)[1]).map(t=>A(t,1)[0]));const e=await x({method:"POST",url:"".concat(k(),"/backup/exports"),body:{contents:{db:this.includeDb,assets:this.includeAssets,storage:this.includeStorage,extensions:t},encryption:{enabled:this.encryptionEnabled,public_key:this.encryptionUseExternal?this.externalPublicKey.trim():null},target_dialect:this.targetDialect||null},surface:!1});this.jobId=e.job_id,this.stage="progress",this.status={phase:e.phase,message:e.message,progress:{total_bytes:0,processed_bytes:0,total_files:0,processed_files:0,percent:0}},this.starting=!1,m.redraw(),this.pump()}catch(t){this.starting=!1,s().alerts.show({type:"error"},v(t,String(M("start_failed")))),m.redraw()}}async pump(){if(!this.polling&&this.jobId){this.polling=!0;try{for(var t;this.jobId&&this.status&&"done"!==this.status.phase&&"error"!==this.status.phase;)try{const t=await x({method:"POST",url:"".concat(k(),"/backup/exports/").concat(this.jobId,"/tick"),surface:!1});this.status=t,m.redraw()}catch(t){const e=v(t,String(M("phase_error_network")));this.status=C(C({},this.status),{},{phase:"error",message:e}),m.redraw();break}"done"===(null==(t=this.status)?void 0:t.phase)&&(s().alerts.show({type:"success"},M("completed")),this.attrs.onComplete())}finally{this.polling=!1}}}async cancel(){if(this.jobId){try{await x({method:"DELETE",url:"".concat(k(),"/backup/exports/").concat(this.jobId),surface:!1})}catch(t){console.warn("[backup] export cancel failed",t),s().alerts.show({type:"warning"},M("cancel_failed_warn"))}this.close()}}close(){this.jobId=null,this.hide()}}function R(t,e){var s=Object.keys(t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(t);e&&(a=a.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),s.push.apply(s,a)}return s}function K(t){for(var e=1;es().translator.trans("ramon-backup.admin.import_modal.".concat(t),null!=e?e:{});class z extends(b()){constructor(){super(...arguments),i(this,"stage","upload"),i(this,"file",null),i(this,"uploading",!1),i(this,"uploadProgress",0),i(this,"uploadIndeterminate",!1),i(this,"uploadError",null),i(this,"inspect",null),i(this,"privateKey",""),i(this,"confirmReplace",!1),i(this,"starting",!1),i(this,"sectionDb",!1),i(this,"sectionAssets",!1),i(this,"sectionStorage",!1),i(this,"sectionExtensions",!1),i(this,"extensionsByName",{}),i(this,"status",null),i(this,"polling",!1)}className(){return"BackupImportModal Modal--medium"}title(){var t;return"progress"===this.stage&&"done"===(null==(t=this.status)?void 0:t.phase)?this.sectionDb?F("logout_title"):F("done_title"):F("title")}content(){return"upload"===this.stage?this.uploadContent():"configure"===this.stage?this.configureContent():this.progressContent()}uploadContent(){return m("div",{className:"Modal-body"},m("div",{className:"Alert Alert--warning"},m("strong",null,F("warning_title")),m("p",null,F("warning_body"))),m("label",{className:"BackupImport-fileLabel"},m("input",{type:"file",accept:".flarum",onchange:t=>{var e;const s=(null==(e=t.target.files)?void 0:e[0])||null;this.file=s}}),this.file?m("span",null,this.file.name," ",m("span",{className:"helpText"},"(",_(this.file.size),")")):m("span",{className:"helpText"},F("choose_file"))),this.uploadError&&m("div",{className:"Alert Alert--error"},this.uploadError),this.uploading&&m("div",{className:"BackupImport-uploadProgress"},m("div",{className:"BackupImport-bar"},m("div",{className:"BackupImport-bar-fill"+(this.uploadIndeterminate?" BackupImport-bar-fill--indeterminate":""),style:this.uploadIndeterminate?void 0:{width:"".concat(Math.max(2,this.uploadProgress),"%")}})),m("div",{className:"BackupImport-uploadStatus helpText"},this.uploadIndeterminate?F("inspecting_archive"):F("uploading_pct",{pct:this.uploadProgress}))),m("div",{className:"Form-group"},m(p(),{className:"Button Button--primary",loading:this.uploading,disabled:this.uploading||!this.file,onclick:()=>this.upload()},F("upload_button"))))}async upload(){if(this.file){this.uploading=!0,this.uploadProgress=0,this.uploadIndeterminate=!1,this.uploadError=null;try{var t;const e=await this.uploadWithProgress(this.file,t=>{this.uploadProgress=t,t>=100&&(this.uploadIndeterminate=!0),m.redraw()});this.inspect=e;const s=e.meta.contents||[];this.sectionDb=s.includes("db"),this.sectionAssets=s.includes("assets"),this.sectionStorage=s.includes("storage"),this.sectionExtensions=s.includes("extensions");const a=(null==(t=e.meta.manifest)?void 0:t.extensions)||[];this.extensionsByName={};for(const t of a){const e="string"==typeof t?t:t.id;e&&(this.extensionsByName[e]=!0)}this.stage="configure"}catch(t){console.error("[backup] archive upload failed",t),this.uploadError=v(t,String(F("upload_failed")))}finally{this.uploading=!1,m.redraw()}}}async uploadWithProgress(t,e){const s=await x({method:"POST",url:"".concat(k(),"/backup/imports"),body:{filename:t.name,size:t.size},surface:!1}),a=s.job_id,n=s.chunk_size>0?s.chunk_size:4194304;let r=0;for(;r2)throw t;await new Promise(t=>setTimeout(t,750*i))}r=s,e(Math.min(99,Math.round(r/t.size*100)))}return e(100),x({method:"POST",url:"".concat(k(),"/backup/imports/").concat(a,"/inspect"),surface:!1})}sendChunk(t,e,a){return new Promise((n,r)=>{var o;const i=new XMLHttpRequest;let l=Date.now();const c=setInterval(()=>{Date.now()-l>6e4&&(clearInterval(c),i.abort())},5e3),m=()=>clearInterval(c);i.upload.addEventListener("progress",()=>{l=Date.now()}),i.addEventListener("load",()=>{if(m(),i.status>=200&&i.status<300)n();else{let e;try{var t;e=null==(t=JSON.parse(i.responseText))||null==(t=t.errors)||null==(t=t[0])?void 0:t.detail}catch(t){}r({detail:e||"".concat(i.status," ").concat(i.statusText)})}}),i.addEventListener("error",()=>{m(),r({detail:String(F("upload_failed"))})}),i.addEventListener("abort",()=>{m(),r({detail:String(F("upload_idle_timeout"))})}),i.open("POST","".concat(k(),"/backup/imports/").concat(t,"/chunk"),!0),i.withCredentials=!0,i.setRequestHeader("Content-Type","application/octet-stream"),i.setRequestHeader("X-Chunk-Offset",String(e));const u=null==(o=s().session)?void 0:o.csrfToken;u&&i.setRequestHeader("X-CSRF-Token",u),i.send(a)})}configureContent(){const t=this.inspect;return m("div",{className:"Modal-body"},m("h4",null,F("inspect_title")),m("dl",{className:"BackupImport-meta"},t.meta.created_at&&m("[",null,m("dt",null,F("meta_when")),m("dd",null,t.meta.created_at)),t.meta.flarum_version&&m("[",null,m("dt",null,F("meta_flarum")),m("dd",null,t.meta.flarum_version)),t.meta.contents&&m("[",null,m("dt",null,F("meta_contents")),m("dd",null,t.meta.contents.join(", "))),t.meta.source_url&&m("[",null,m("dt",null,F("meta_source_url")),m("dd",null,m("code",null,t.meta.source_url))),m("dt",null,F("meta_size")),m("dd",null,_(t.size))),m("div",{className:"Alert Alert--info BackupImport-urlNote"},m("i",{className:"icon fas fa-info-circle"})," ",F("url_rewrite_note")),this.selectionFieldset(t),t.is_encrypted&&m("fieldset",{className:"BackupImport-fieldset"},m("legend",null,F("key_title")),m("p",{className:"helpText"},F("key_help")),m("textarea",{className:"FormControl BackupImport-keyInput",rows:3,placeholder:"base64 private key",value:this.privateKey,oninput:t=>{this.privateKey=t.target.value}}),m("p",{className:"helpText BackupImport-keyHint"},F("key_hint_local"))),m("div",{className:"Alert Alert--error BackupImport-confirmAlert"},m("strong",null,F("confirm_title")),m("p",null,F("confirm_body")),m("label",{className:"BackupImport-confirm"},m("input",{type:"checkbox",checked:this.confirmReplace,onchange:t=>{this.confirmReplace=t.target.checked}})," ",m("span",null,F("confirm_check")))),m("div",{className:"Form-group"},m(p(),{className:"Button Button--primary",loading:this.starting,disabled:this.starting||!this.confirmReplace,onclick:()=>this.startRestore()},F("start_button"))))}selectionFieldset(t){const e=t.meta.contents||[],s=t.meta.manifest||{},a=e.includes("db"),n=e.includes("assets"),r=e.includes("storage"),o=e.includes("extensions"),i=(s.extensions||[]).map(t=>"string"==typeof t?{id:t,location:"workbench"}:t);return m("fieldset",{className:"BackupImport-fieldset"},m("legend",null,F("selection_title")),m("p",{className:"helpText"},F("selection_help")),a&&this.sectionRow("db",this.sectionDb,t=>this.sectionDb=t),n&&this.sectionRow("assets",this.sectionAssets,t=>this.sectionAssets=t,s.asset_count),r&&this.sectionRow("storage",this.sectionStorage,t=>this.sectionStorage=t,s.storage_count),o&&m("[",null,this.sectionRow("extensions",this.sectionExtensions,t=>{this.sectionExtensions=t;for(const e of i)this.extensionsByName[e.id]=t},s.extension_count),this.sectionExtensions&&s.has_composer&&m("div",{className:"BackupImport-composerNote helpText"},m("i",{className:"icon fas fa-cube"})," ",F("extensions_composer_note")),this.sectionExtensions&&i.length>0&&m("div",{className:"BackupImport-extList"},i.map(t=>m("label",{className:"BackupImport-extRow",key:t.id},m("input",{type:"checkbox",checked:!!this.extensionsByName[t.id],onchange:e=>{this.extensionsByName[t.id]=e.target.checked}})," ",m("span",{className:"BackupImport-extTitle"},t.title||t.id)," ",t.name&&t.name!==t.id&&m("code",{className:"BackupImport-extName"},t.name),t.location&&m("span",{className:"BackupImport-extTag BackupImport-extTag--".concat(t.location)},F("extensions_tag_"+t.location)))))))}sectionRow(t,e,s,a){return m("label",{className:"BackupImport-sectionRow"},m("input",{type:"checkbox",checked:e,onchange:t=>s(t.target.checked)})," ",m("span",{className:"BackupImport-sectionLabel"},F("section_"+t)),void 0!==a&&a>0&&m("span",{className:"BackupImport-sectionCount helpText"}," ","(",F("section_count",{count:a}),")"))}buildSelection(){const t=Object.entries(this.extensionsByName),e=t.length>0&&t.every(t=>A(t,2)[1]),s=!!this.sectionExtensions&&(!!e||t.filter(t=>A(t,2)[1]).map(t=>A(t,1)[0]));return{db:this.sectionDb,assets:this.sectionAssets,storage:this.sectionStorage,extensions:s}}async startRestore(){if(this.inspect){this.starting=!0;try{const t=await x({method:"POST",url:"".concat(k(),"/backup/imports/").concat(this.inspect.job_id,"/start"),surface:!1,body:{private_key:this.privateKey.trim()||null,confirm_replace:this.confirmReplace,selection:this.buildSelection()}});this.stage="progress",this.status={phase:t.phase,message:t.message,progress:{total_bytes:this.inspect.size,processed_bytes:0,extracted_entries:0,restored_statements:0,percent:0}},m.redraw(),this.pump()}catch(t){s().alerts.show({type:"error"},v(t,String(F("start_failed"))))}finally{this.starting=!1}}}progressContent(){var t;const e=this.status;if(!e)return m(h(),null);if("done"===e.phase)return this.completedContent();const s="error"===e.phase,a=Math.max(0,Math.min(100,(null==(t=e.progress)?void 0:t.percent)||0));return m("div",{className:"Modal-body BackupImport-progress"},m("div",{className:"BackupImport-status BackupImport-status--".concat(e.phase)},m("strong",null,F("phase_"+e.phase)),m("p",null,e.message)),!s&&m("div",{className:"BackupImport-bar"},m("div",{className:"BackupImport-bar-fill",style:{width:"".concat(a,"%")}})),m("div",{className:"Form-group BackupImport-progress-actions"},!s&&m(p(),{className:"Button",onclick:()=>this.cancel()},F("cancel_button")),s&&m(p(),{className:"Button Button--primary",onclick:()=>this.close()},F("close_button"))))}completedWarnings(){var t,e;const s=null!=(t=null==(e=this.status)?void 0:e.warnings)?t:[];return s.length?m("div",{className:"Alert Alert--error BackupImport-completedWarnings"},m("strong",null,F("incomplete_title")),m("ul",null,s.map(t=>m("li",null,t)))):null}completedContent(){var t;if(this.sectionDb)return m("div",{className:"Modal-body BackupImport-completed BackupImport-completed--logout"},this.completedWarnings(),m("div",{className:"BackupImport-completedIcon"},m("i",{className:"fas fa-right-from-bracket"})),m("h3",{className:"BackupImport-completedTitle"},F("logout_title")),m("p",{className:"BackupImport-completedBody"},F("logout_body")),m("ol",{className:"BackupImport-completedSteps"},m("li",null,F("logout_step_reload")),m("li",null,F("logout_step_login"))),m(p(),{className:"Button Button--primary BackupImport-completedAction",icon:"fas fa-rotate",onclick:()=>window.location.reload()},F("logout_button")));const e=!(null==(t=this.status)||!t.incomplete);return m("div",{className:"Modal-body BackupImport-completed"},this.completedWarnings(),m("div",{className:"BackupImport-completedIcon BackupImport-completedIcon--".concat(e?"warning":"success")},m("i",{className:e?"fas fa-triangle-exclamation":"fas fa-circle-check"})),m("h3",{className:"BackupImport-completedTitle"},F(e?"incomplete_title":"done_title")),m("p",{className:"BackupImport-completedBody"},F(e?"incomplete_body":"done_body")),m(p(),{className:"Button Button--primary",onclick:()=>this.close()},F("close_button")))}async pump(){if(!this.polling&&this.inspect){this.polling=!0;try{for(var t;this.inspect&&this.status&&"done"!==this.status.phase&&"error"!==this.status.phase;)try{const t=await x({method:"POST",url:"".concat(k(),"/backup/imports/").concat(this.inspect.job_id,"/tick"),surface:!1,body:{private_key:this.privateKey.trim()||null}});this.status=t,m.redraw()}catch(t){console.error("[backup] import tick failed",t);const e=v(t,String(F("phase_error_network")));this.status=K(K({},this.status),{},{phase:"error",message:e}),m.redraw();break}"done"===(null==(t=this.status)?void 0:t.phase)&&(s().alerts.show({type:"success"},F("completed")),this.sectionDb||this.attrs.onComplete())}finally{this.polling=!1}}}async cancel(){if(this.inspect){try{await x({method:"DELETE",url:"".concat(k(),"/backup/imports/").concat(this.inspect.job_id),surface:!1})}catch(t){console.error("[backup] import cancel failed",t),s().alerts.show({type:"warning"},F("cancel_failed_warn"))}this.close()}}close(){this.hide()}}function q(t,e){var s=Object.keys(t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(t);e&&(a=a.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),s.push.apply(s,a)}return s}function U(t){for(var e=1;ethis.decide(!0)},a),m(p(),{className:"Button",onclick:()=>this.decide(!1)},n)))}decide(t){var e;this.resolved||(this.resolved=!0,null==(e=t?this.attrs.onConfirm:this.attrs.onCancel)||e(),this.hide())}onbeforeremove(t){var e,s;return this.resolved||(this.resolved=!0,null==(e=(s=this.attrs).onCancel)||e.call(s)),super.onbeforeremove(t)}}flarum.reg.add("ramon-backup","admin/components/ConfirmModal",G);const H=t=>s().translator.trans("ramon-backup.admin.errors.".concat(t));class W extends(c()){constructor(){super(...arguments),i(this,"failed",!1),i(this,"lastError",null)}view(t){if(this.failed){const e=()=>{this.failed=!1,this.lastError=null,m.redraw()};return t.attrs.fallback?t.attrs.fallback(this.lastError,e):m("div",{className:"Alert Alert--error BackupErrorBoundary"},m("strong",null,H("boundary_title")),m("p",null,H("boundary_body")),m("button",{type:"button",className:"Button",onclick:e},H("boundary_retry")))}try{return t.children}catch(a){var e,s;return this.failed=!0,this.lastError=a,null==(e=(s=t.attrs).onError)||e.call(s,a),console.error("[backup] render boundary caught",a),null}}}flarum.reg.add("ramon-backup","admin/utils/errorBoundary",{ErrorBoundary:W});const Q=(t,e)=>s().translator.trans("ramon-backup.admin.".concat(t),null!=e?e:{});class X extends(c()){constructor(){super(...arguments),i(this,"listState","loading"),i(this,"listError",null),i(this,"backups",[])}oninit(t){super.oninit(t),this.refresh()}view(){return m("div",{className:"BackupPanel"},m(W,{onError:t=>console.error("[backup] panel render",t)},m("section",{className:"BackupPanel-actions"},m("h3",null,Q("panel.actions_title")),m("p",{className:"helpText"},Q("panel.actions_help")),m("div",{className:"BackupPanel-actionButtons"},m(p(),{className:"Button Button--primary",icon:"fas fa-download",onclick:()=>this.openExport()},Q("panel.create_button")),m(p(),{className:"Button",icon:"fas fa-upload",onclick:()=>this.openImport()},Q("panel.import_button")))),m(E,null),m("section",{className:"BackupPanel-list"},m("h3",null,Q("panel.list_title")),this.renderList())))}renderList(){return"loading"===this.listState?m(h(),null):"error"===this.listState?m("div",{className:"Alert Alert--error BackupPanel-listError"},m("p",null,Q("list.load_failed")),this.listError&&m("p",{className:"helpText"},m("code",null,this.listError)),m(p(),{className:"Button",icon:"fas fa-rotate",onclick:()=>this.refresh()},Q("list.retry"))):m(T,{backups:this.backups,onDelete:t=>this.delete(t),onRefresh:()=>this.refresh()})}refresh(){return this.listState="loading",this.listError=null,x({method:"GET",url:"".concat(k(),"/backup/backups"),surface:!1}).then(t=>{this.backups=t.backups||[],this.listState="ok"}).catch(t=>{this.backups=[],this.listState="error",this.listError=v(t)}).then(()=>{m.redraw()})}openExport(){s().modal.show(D,{onComplete:()=>this.refresh()})}openImport(){s().modal.show(z,{onComplete:()=>this.refresh()})}async delete(t){var e;if(await(e={title:Q("list.confirm_delete_title"),body:Q("list.confirm_delete"),confirmLabel:Q("list.delete_title"),danger:!0},new Promise(t=>{let a=!1;const n=e=>{a||(a=!0,t(e))};s().modal.show(G,U(U({},e),{},{onConfirm:()=>n(!0),onCancel:()=>n(!1)}))})))try{await x({method:"DELETE",url:"".concat(k(),"/backup/backups/").concat(t),surface:!1,fallbackMessage:String(Q("list.delete_failed"))}),s().alerts.show({type:"success"},Q("list.deleted")),this.refresh()}catch(t){s().alerts.show({type:"error"},v(t,String(Q("list.delete_failed"))))}}}flarum.reg.add("ramon-backup","admin/components/BackupPanel",X);const $="ramon-backup";(0,a.override)(r().prototype,"submitButton",function(t){return this.extension&&this.extension.id===$?null:t()}),s().initializers.add($,()=>{s().registry.for($).registerSetting(()=>m(X,null),100,"ramon-backup.panel").registerPermission({icon:"fas fa-file-archive",label:s().translator.trans("ramon-backup.admin.permissions.manage_label"),permission:"backup.manage"},"moderate")})})(),module.exports={}})(); +(()=>{const t={n:e=>{const s=e&&e.__esModule?()=>e.default:()=>e;return t.d(s,{a:s}),s},d:(e,s)=>{if(Array.isArray(s))for(var a=0;aObject.prototype.hasOwnProperty.call(t,e)};(()=>{"use strict";const e=flarum.reg.get("core","admin/app");var s=t.n(e);const a=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","admin/components/ExtensionPage");var r=t.n(n);function o(t){return o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},o(t)}function i(t,e,s){return(e=function(t){var e=function(t){if("object"!=o(t)||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var s=e.call(t,"string");if("object"!=o(s))return s;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t);return"symbol"==o(e)?e:e+""}(e))in t?Object.defineProperty(t,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[e]=s,t}const l=flarum.reg.get("core","common/Component");var c=t.n(l);const u=flarum.reg.get("core","common/components/Button");var p=t.n(u);const d=flarum.reg.get("core","common/components/LoadingIndicator");var h=t.n(d);const g=flarum.reg.get("core","common/components/Modal");var b=t.n(g);const f=flarum.reg.get("core","common/utils/extractText");var y=t.n(f);function k(){return(s().forum.attribute("apiUrl")||"/api").replace(/\/+$/,"")}function _(t){if(!Number.isFinite(t)||t<=0)return"0 B";const e=["B","KB","MB","GB","TB"];let s=0,a=t;for(;a>=1024&&s=100||0===s?0:1)+" "+e[s]}function v(t,e){var a,n,r;const o=null!=(a=null!=(n=null==t||null==(r=t.response)||null==(r=r.errors)||null==(r=r[0])?void 0:r.detail)?n:null==t?void 0:t.detail)?a:"string"==typeof(null==t?void 0:t.message)?t.message:void 0;return o?String(o):e||String(s().translator.trans("ramon-backup.admin.errors.generic"))}async function x(t){try{return await s().request(t)}catch(e){const a=v(e,t.fallbackMessage);if(console.error("[backup] api error",t.method,t.url,e),!1!==t.surface&&s().alerts.show({type:"error"},a),e&&"object"==typeof e&&!e.detail)try{e.detail=a}catch(t){}throw e}}flarum.reg.add("ramon-backup","admin/utils/api",{apiUrl:k,fmtBytes:_,errorDetail:v});const N=(t,e)=>s().translator.trans("ramon-backup.admin.encryption.".concat(t),null!=e?e:{});class B extends(b()){constructor(){super(...arguments),i(this,"copied",!1)}className(){return"BackupRevealModal Modal--medium"}title(){return N("reveal_modal.title")}content(){const t=this.attrs,e=t.privateKey,s=t.configKey,a="'".concat(s,"' => '").concat(e,"',");return m("div",{className:"Modal-body"},m("p",null,N("reveal_modal.intro")),m("div",{className:"Alert Alert--error"},m("strong",null,N("reveal_modal.warning_title")),m("p",null,N("reveal_modal.warning_body"))),m("label",{className:"BackupReveal-label"},N("reveal_modal.snippet_label")),m("pre",{className:"BackupReveal-snippet"},m("code",null,a)),m("div",{className:"Form-group BackupReveal-actions"},m(p(),{className:"Button",icon:"fas fa-copy",onclick:()=>this.copy(a)},this.copied?N("reveal_modal.copied"):N("reveal_modal.copy_button")),m(p(),{className:"Button Button--primary",onclick:()=>this.hide()},N("reveal_modal.close"))))}copy(t){navigator.clipboard?navigator.clipboard.writeText(t).then(()=>{this.copied=!0,m.redraw(),setTimeout(()=>{this.copied=!1,m.redraw()},2e3)}).catch(t=>{console.error("[backup] clipboard writeText failed",t),s().alerts.show({type:"error"},N("clipboard_failed"))}):s().alerts.show({type:"error"},N("clipboard_unavailable"))}}class w extends(b()){constructor(){super(...arguments),i(this,"acknowledged",!1),i(this,"submitting",!1)}className(){return"BackupRegenerateModal Modal--medium"}title(){return N("regenerate_modal.title")}content(){return m("div",{className:"Modal-body"},m("div",{className:"Alert Alert--error"},m("p",null,N("regenerate_modal.warning"))),m("label",{className:"BackupRegenerate-confirm"},m("input",{type:"checkbox",checked:this.acknowledged,onchange:t=>{this.acknowledged=t.target.checked}})," ",N("regenerate_modal.acknowledge")),m("div",{className:"Form-group"},m(p(),{className:"Button Button--primary",loading:this.submitting,disabled:!this.acknowledged||this.submitting,onclick:()=>this.submit()},N("regenerate_modal.submit"))))}async submit(){this.submitting=!0,m.redraw();try{await this.attrs.onConfirm(),this.hide()}catch(t){this.submitting=!1,m.redraw()}}}class E extends(c()){constructor(){super(...arguments),i(this,"status",null),i(this,"loadState","loading"),i(this,"loadError",null),i(this,"publicCopied",!1)}oninit(t){super.oninit(t),this.refresh()}view(){return m("section",{className:"BackupEncryptionCard"},m("header",null,m("h3",null,N("section_title")),m("p",{className:"helpText"},N("section_help"))),"loading"===this.loadState&&m(h(),null),"error"===this.loadState&&m("div",{className:"Alert Alert--error BackupEncryption-loadError"},m("p",null,N("status.load_failed")),this.loadError&&m("p",{className:"helpText"},m("code",null,this.loadError)),m(p(),{className:"Button",icon:"fas fa-rotate",onclick:()=>this.refresh()},N("status.retry"))),"ok"===this.loadState&&this.body())}body(){if(!this.status)return m("p",{className:"helpText"},N("status.unknown"));const t=this.status;return t.available?m("[",null,m("div",{className:"BackupEncryption-statusRow"},this.statusBadge("public",t.has_public_key),this.statusBadge("private",t.private_key_present)),t.healthy&&m("div",{className:"Alert Alert--success"},N("status.healthy")),!t.has_public_key&&!t.private_key_present&&m("div",null,m("p",{className:"helpText"},N("status.not_setup")),m(p(),{className:"Button Button--primary",icon:"fas fa-key",onclick:()=>this.generate(!1)},N("actions.generate"))),t.has_public_key&&t.private_key_present&&!1===t.keys_match&&m("div",{className:"Alert Alert--error"},m("strong",null,N("status.mismatch_title")),m("p",null,N("status.mismatch_body")),m("p",null,m("code",null,"'",t.config_key,"'"))),t.has_public_key&&!t.private_key_present&&m("div",{className:"Alert Alert--error"},m("strong",null,N("status.private_missing_title")),m("p",null,N("status.private_missing_body")),m("p",null,m("code",null,"'",t.config_key,"'"))),t.has_public_key&&this.publicKeyPanel(t.public_key||"",t.healthy)):m("div",{className:"Alert Alert--error"},N("status.libsodium_missing"))}publicKeyPanel(t,e){return m("div",{className:"BackupEncryption-publicKey"},m("label",null,N("public_key.label")),m("div",{className:"BackupEncryption-publicKeyRow"},m("pre",null,m("code",null,t)),m(p(),{className:"Button Button--icon",icon:"fas fa-copy",title:y()(N("public_key.copy_title")),onclick:()=>this.copyPublic(t)},this.publicCopied?y()(N("public_key.copied")):"")),m("p",{className:"helpText"},N(e?"public_key.help_healthy":"public_key.help_broken")),m(p(),{className:"Button Button--danger",icon:"fas fa-rotate",onclick:()=>this.openRegenerate()},N("public_key.remove_button")))}statusBadge(t,e){return m("div",{className:"BackupEncryption-badge BackupEncryption-badge--".concat(e?"ok":"missing")},m("i",{className:"icon fas fa-".concat(e?"check":"times")}),m("span",null,N("status.".concat(t,"_key_label"))),m("span",{className:"BackupEncryption-badgeState"},N("status.".concat(e?"present":"absent"))))}copyPublic(t){t&&(navigator.clipboard?navigator.clipboard.writeText(t).then(()=>{this.publicCopied=!0,m.redraw(),setTimeout(()=>{this.publicCopied=!1,m.redraw()},2e3)}).catch(t=>{console.error("[backup] clipboard writeText failed",t),s().alerts.show({type:"error"},N("clipboard_failed"))}):s().alerts.show({type:"error"},N("clipboard_unavailable")))}refresh(){return this.loadState="loading",this.loadError=null,x({method:"GET",url:"".concat(k(),"/backup/encryption/status"),surface:!1}).then(t=>{this.status=t,this.loadState="ok"}).catch(t=>{this.status=null,this.loadState="error",this.loadError=v(t)}).then(()=>{m.redraw()})}async generate(t){try{const e=await x({method:"POST",url:"".concat(k(),"/backup/encryption/generate-keypair"),body:{acknowledge_loss:t},surface:!1});await this.refresh(),s().modal.show(B,{privateKey:e.private_key,configKey:e.config_key})}catch(t){throw s().alerts.show({type:"error"},v(t,String(N("actions.generate_failed")))),t}}openRegenerate(){s().modal.show(w,{onConfirm:()=>this.generate(!0)})}}flarum.reg.add("ramon-backup","admin/components/EncryptionCard",E);const S=flarum.reg.get("core","common/helpers/humanTime");var I=t.n(S);const O={mysql:"MySQL",mariadb:"MariaDB",postgres:"PostgreSQL",sqlite:"SQLite"},P=(t,e)=>s().translator.trans("ramon-backup.admin.list.".concat(t),null!=e?e:{});class T extends(c()){view(t){const e=t.attrs,s=e.backups,a=e.onDelete;return s.length?m("table",{className:"BackupList Table"},m("thead",null,m("tr",null,m("th",null,P("col_when")),m("th",null,P("col_size")),m("th",null,P("col_contents")),m("th",null,P("col_status")),m("th",null))),m("tbody",null,s.map(t=>m("tr",{key:t.id,className:"BackupList-row"},m("td",null,m("div",{className:"BackupList-when"},t.created_at?I()(new Date(t.created_at)):"—"),m("div",{className:"BackupList-filename"},t.filename),t.target_dialect&&m("div",{className:"BackupList-target BackupList-target--".concat(t.target_dialect),title:String(P("target_tooltip",{engine:O[t.target_dialect]||t.target_dialect}))},m("i",{className:"icon fas fa-arrow-right-arrow-left"})," ",P("target_for",{engine:O[t.target_dialect]||t.target_dialect}))),m("td",null,_(t.size_bytes)),m("td",null,t.contents.map(t=>m("span",{className:"BackupList-tag BackupList-tag--".concat(t)},P("content_"+t)))),m("td",null,t.encrypted?m("span",{className:"BackupList-encryption BackupList-encryption--on"},m("i",{className:"icon fas fa-lock"})," ",P("encrypted")):m("span",{className:"BackupList-encryption BackupList-encryption--off"},m("i",{className:"icon fas fa-lock-open"})," ",P("plain"))),m("td",{className:"BackupList-actions"},m("a",{className:"Button Button--icon",href:"".concat(k(),"/backup/backups/").concat(t.id,"/download"),target:"_blank",title:String(P("download_title"))},m("i",{className:"icon fas fa-download"})),m(p(),{className:"Button Button--icon Button--danger",icon:"fas fa-trash",title:P("delete_title"),onclick:()=>a(t.id)})))))):m("p",{className:"BackupList-empty helpText"},P("empty"))}}function j(t,e){(null==e||e>t.length)&&(e=t.length);for(var s=0,a=Array(e);ss().translator.trans("ramon-backup.admin.export_modal.".concat(t),null!=e?e:{});class D extends(b()){constructor(){super(...arguments),i(this,"stage","form"),i(this,"includeDb",!0),i(this,"includeAssets",!0),i(this,"includeStorage",!1),i(this,"includeExtensions",!1),i(this,"extensionsLoading",!1),i(this,"extensionsLoaded",!1),i(this,"extensions",[]),i(this,"extensionSelected",{}),i(this,"encryptionEnabled",!1),i(this,"encryptionUseExternal",!1),i(this,"externalPublicKey",""),i(this,"targetDialect",""),i(this,"starting",!1),i(this,"jobId",null),i(this,"status",null),i(this,"polling",!1)}className(){return"BackupExportModal Modal--medium"}title(){return M("title")}content(){return"form"===this.stage?this.formContent():this.progressContent()}formContent(){return m("div",{className:"Modal-body"},m("p",{className:"helpText"},M("intro")),m("fieldset",{className:"BackupExport-fieldset"},m("legend",null,M("contents_title")),this.checkbox("db",()=>this.includeDb,t=>this.includeDb=t),this.checkbox("assets",()=>this.includeAssets,t=>this.includeAssets=t),this.checkbox("storage",()=>this.includeStorage,t=>this.includeStorage=t),this.checkbox("extensions",()=>this.includeExtensions,t=>{this.includeExtensions=t,t&&!this.extensionsLoaded&&this.loadExtensions()}),this.includeExtensions&&this.extensionList()),this.includeDb&&m("fieldset",{className:"BackupExport-fieldset"},m("legend",null,M("target_title")),m("p",{className:"helpText"},M("target_help")),m("select",{className:"FormControl BackupExport-targetSelect",value:this.targetDialect,onchange:t=>{this.targetDialect=t.target.value}},m("option",{value:""},M("target_same")),m("option",{value:"mysql"},M("target_mysql")),m("option",{value:"mariadb"},M("target_mariadb")),m("option",{value:"postgres"},M("target_postgres")),m("option",{value:"sqlite"},M("target_sqlite")))),m("fieldset",{className:"BackupExport-fieldset"},m("legend",null,M("encryption_title")),m("label",{className:"BackupExport-checkbox"},m("input",{type:"checkbox",checked:this.encryptionEnabled,onchange:t=>{this.encryptionEnabled=t.target.checked}})," ",m("span",null,M("encryption_enable"))),m("p",{className:"helpText"},M("encryption_help")),this.encryptionEnabled&&m("[",null,m("label",{className:"BackupExport-checkbox"},m("input",{type:"checkbox",checked:this.encryptionUseExternal,onchange:t=>{this.encryptionUseExternal=t.target.checked}})," ",m("span",null,M("encryption_external"))),this.encryptionUseExternal&&m("[",null,m("p",{className:"helpText"},M("encryption_external_help")),m("textarea",{className:"FormControl BackupExport-keyInput",rows:3,placeholder:"base64 public key",value:this.externalPublicKey,oninput:t=>{this.externalPublicKey=t.target.value}})))),m("div",{className:"Form-group"},m(p(),{className:"Button Button--primary",loading:this.starting,disabled:this.starting||!this.canStart(),onclick:()=>this.start()},M("start_button"))))}extensionList(){if(this.extensionsLoading)return m("div",{className:"BackupExport-extLoading"},m(h(),null));if(!this.extensions.length)return m("p",{className:"helpText BackupExport-extEmpty"},M("extensions_none"));const t={workbench:[],vendor:[],unknown:[]};for(const s of this.extensions){var e;null==(e=t[s.location])||e.push(s)}return m("div",{className:"BackupExport-extList"},m("div",{className:"BackupExport-extActions"},m("button",{type:"button",className:"BackupExport-extLink",onclick:()=>this.toggleAllExtensions(!0)},M("extensions_select_all")),m("span",null," · "),m("button",{type:"button",className:"BackupExport-extLink",onclick:()=>this.toggleAllExtensions(!1)},M("extensions_select_none"))),["workbench","vendor","unknown"].filter(e=>t[e].length>0).map(e=>m("div",{className:"BackupExport-extGroup",key:e},m("div",{className:"BackupExport-extGroupHeader"},M("extensions_group_"+e)," ",m("span",{className:"helpText"},"(",t[e].length,")")),t[e].map(t=>m("label",{className:"BackupExport-extRow",key:t.id},m("input",{type:"checkbox",checked:!!this.extensionSelected[t.id],onchange:e=>{this.extensionSelected[t.id]=e.target.checked}})," ",m("span",{className:"BackupExport-extTitle"},t.title)," ",m("code",{className:"BackupExport-extName"},t.name||t.id),m("span",{className:"BackupExport-extTag BackupExport-extTag--".concat(t.location)},M("extensions_tag_"+t.location)))))))}toggleAllExtensions(t){for(const e of this.extensions)this.extensionSelected[e.id]=t}async loadExtensions(){this.extensionsLoading=!0;try{const t=await x({method:"GET",url:"".concat(k(),"/backup/extensions"),surface:!1});this.extensions=t.extensions||[],this.extensionsLoaded=!0;for(const t of this.extensions)this.extensionSelected[t.id]=!0}catch(t){s().alerts.show({type:"error"},v(t,String(M("extensions_load_failed"))))}finally{this.extensionsLoading=!1,m.redraw()}}checkbox(t,e,s){return m("label",{className:"BackupExport-checkbox"},m("input",{type:"checkbox",checked:e(),onchange:t=>{s(t.target.checked)}})," ",m("span",{className:"BackupExport-checkbox-label"},M("content_"+t)),m("span",{className:"BackupExport-checkbox-help helpText"},M("content_"+t+"_help")))}canStart(){return!(!(this.includeDb||this.includeAssets||this.includeStorage||this.includeExtensions)||this.encryptionEnabled&&this.encryptionUseExternal&&!this.externalPublicKey.trim())}progressContent(){var t,e,s;const a=this.status;if(!a)return m(h(),null);const n="done"===a.phase,r="error"===a.phase,o=Math.max(0,Math.min(100,(null==(t=a.progress)?void 0:t.percent)||0));return m("div",{className:"Modal-body BackupExport-progress"},m("div",{className:"BackupExport-status BackupExport-status--".concat(a.phase)},m("strong",null,M("phase_"+a.phase)),m("p",null,a.message)),!r&&m("[",null,m("div",{className:"BackupExport-bar"},m("div",{className:"BackupExport-bar-fill",style:{width:"".concat(n?100:o,"%")},role:"progressbar","aria-valuenow":o,"aria-valuemin":0,"aria-valuemax":100})),m("div",{className:"BackupExport-stats"},m("span",null,_(a.progress.processed_bytes)," /"," ",_(a.progress.total_bytes||a.progress.processed_bytes)),a.progress.total_files>0&&m("span",null,M("files_count",{done:a.progress.processed_files,total:a.progress.total_files})))),(null!=(e=null==(s=a.warnings)?void 0:s.length)?e:0)>0&&m("div",{className:"BackupExport-warnings",role:"alert"},m("div",{className:"BackupExport-warnings-title"},m("i",{className:"icon fas fa-triangle-exclamation"})," ",M("warnings_title",{count:a.warnings.length})),m("p",{className:"helpText"},M("warnings_help")),m("ul",{className:"BackupExport-warnings-list"},a.warnings.map((t,e)=>m("li",{key:e},t)))),m("div",{className:"Form-group BackupExport-progress-actions"},!n&&!r&&m(p(),{className:"Button",onclick:()=>this.cancel()},M("cancel_button")),(n||r)&&m(p(),{className:"Button Button--primary",onclick:()=>this.close()},M("close_button"))))}async start(){this.starting=!0;try{let t=!1;this.includeExtensions&&(t=!this.extensionsLoaded||Object.entries(this.extensionSelected).filter(t=>A(t,2)[1]).map(t=>A(t,1)[0]));const e=await x({method:"POST",url:"".concat(k(),"/backup/exports"),body:{contents:{db:this.includeDb,assets:this.includeAssets,storage:this.includeStorage,extensions:t},encryption:{enabled:this.encryptionEnabled,public_key:this.encryptionUseExternal?this.externalPublicKey.trim():null},target_dialect:this.targetDialect||null},surface:!1});this.jobId=e.job_id,this.stage="progress",this.status={phase:e.phase,message:e.message,progress:{total_bytes:0,processed_bytes:0,total_files:0,processed_files:0,percent:0}},this.starting=!1,m.redraw(),this.pump()}catch(t){this.starting=!1,s().alerts.show({type:"error"},v(t,String(M("start_failed")))),m.redraw()}}async pump(){if(!this.polling&&this.jobId){this.polling=!0;try{for(var t;this.jobId&&this.status&&"done"!==this.status.phase&&"error"!==this.status.phase;)try{const t=await x({method:"POST",url:"".concat(k(),"/backup/exports/").concat(this.jobId,"/tick"),surface:!1});this.status=t,m.redraw()}catch(t){const e=v(t,String(M("phase_error_network")));this.status=C(C({},this.status),{},{phase:"error",message:e}),m.redraw();break}"done"===(null==(t=this.status)?void 0:t.phase)&&(s().alerts.show({type:"success"},M("completed")),this.attrs.onComplete())}finally{this.polling=!1}}}async cancel(){if(this.jobId){try{await x({method:"DELETE",url:"".concat(k(),"/backup/exports/").concat(this.jobId),surface:!1})}catch(t){console.warn("[backup] export cancel failed",t),s().alerts.show({type:"warning"},M("cancel_failed_warn"))}this.close()}}close(){this.jobId=null,this.hide()}}function R(t,e){var s=Object.keys(t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(t);e&&(a=a.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),s.push.apply(s,a)}return s}function K(t){for(var e=1;es().translator.trans("ramon-backup.admin.import_modal.".concat(t),null!=e?e:{});class z extends(b()){constructor(){super(...arguments),i(this,"stage","upload"),i(this,"file",null),i(this,"uploading",!1),i(this,"uploadProgress",0),i(this,"uploadIndeterminate",!1),i(this,"uploadError",null),i(this,"inspect",null),i(this,"privateKey",""),i(this,"confirmReplace",!1),i(this,"starting",!1),i(this,"sectionDb",!1),i(this,"sectionAssets",!1),i(this,"sectionStorage",!1),i(this,"sectionExtensions",!1),i(this,"extensionsByName",{}),i(this,"sectionRootExtend",!1),i(this,"preserveSettings",!0),i(this,"status",null),i(this,"polling",!1)}className(){return"BackupImportModal Modal--medium"}title(){var t;return"progress"===this.stage&&"done"===(null==(t=this.status)?void 0:t.phase)?this.sectionDb?F("logout_title"):F("done_title"):F("title")}content(){return"upload"===this.stage?this.uploadContent():"configure"===this.stage?this.configureContent():this.progressContent()}uploadContent(){return m("div",{className:"Modal-body"},m("div",{className:"Alert Alert--warning"},m("strong",null,F("warning_title")),m("p",null,F("warning_body"))),m("label",{className:"BackupImport-fileLabel"},m("input",{type:"file",accept:".flarum",onchange:t=>{var e;const s=(null==(e=t.target.files)?void 0:e[0])||null;this.file=s}}),this.file?m("span",null,this.file.name," ",m("span",{className:"helpText"},"(",_(this.file.size),")")):m("span",{className:"helpText"},F("choose_file"))),this.uploadError&&m("div",{className:"Alert Alert--error"},this.uploadError),this.uploading&&m("div",{className:"BackupImport-uploadProgress"},m("div",{className:"BackupImport-bar"},m("div",{className:"BackupImport-bar-fill"+(this.uploadIndeterminate?" BackupImport-bar-fill--indeterminate":""),style:this.uploadIndeterminate?void 0:{width:"".concat(Math.max(2,this.uploadProgress),"%")}})),m("div",{className:"BackupImport-uploadStatus helpText"},this.uploadIndeterminate?F("inspecting_archive"):F("uploading_pct",{pct:this.uploadProgress}))),m("div",{className:"Form-group"},m(p(),{className:"Button Button--primary",loading:this.uploading,disabled:this.uploading||!this.file,onclick:()=>this.upload()},F("upload_button"))))}async upload(){if(this.file){this.uploading=!0,this.uploadProgress=0,this.uploadIndeterminate=!1,this.uploadError=null;try{var t;const e=await this.uploadWithProgress(this.file,t=>{this.uploadProgress=t,t>=100&&(this.uploadIndeterminate=!0),m.redraw()});this.inspect=e;const s=e.meta.contents||[];this.sectionDb=s.includes("db"),this.sectionAssets=s.includes("assets"),this.sectionStorage=s.includes("storage"),this.sectionExtensions=s.includes("extensions");const a=(null==(t=e.meta.manifest)?void 0:t.extensions)||[];this.extensionsByName={};for(const t of a){const e="string"==typeof t?t:t.id;e&&(this.extensionsByName[e]=!0)}this.stage="configure"}catch(t){console.error("[backup] archive upload failed",t),this.uploadError=v(t,String(F("upload_failed")))}finally{this.uploading=!1,m.redraw()}}}async uploadWithProgress(t,e){const s=await x({method:"POST",url:"".concat(k(),"/backup/imports"),body:{filename:t.name,size:t.size},surface:!1}),a=s.job_id,n=s.chunk_size>0?s.chunk_size:4194304;let r=0;for(;r2)throw t;await new Promise(t=>setTimeout(t,750*i))}r=s,e(Math.min(99,Math.round(r/t.size*100)))}return e(100),x({method:"POST",url:"".concat(k(),"/backup/imports/").concat(a,"/inspect"),surface:!1})}sendChunk(t,e,a){return new Promise((n,r)=>{var o;const i=new XMLHttpRequest;let l=Date.now();const c=setInterval(()=>{Date.now()-l>6e4&&(clearInterval(c),i.abort())},5e3),m=()=>clearInterval(c);i.upload.addEventListener("progress",()=>{l=Date.now()}),i.addEventListener("load",()=>{if(m(),i.status>=200&&i.status<300)n();else{let e;try{var t;e=null==(t=JSON.parse(i.responseText))||null==(t=t.errors)||null==(t=t[0])?void 0:t.detail}catch(t){}r({detail:e||"".concat(i.status," ").concat(i.statusText)})}}),i.addEventListener("error",()=>{m(),r({detail:String(F("upload_failed"))})}),i.addEventListener("abort",()=>{m(),r({detail:String(F("upload_idle_timeout"))})}),i.open("POST","".concat(k(),"/backup/imports/").concat(t,"/chunk"),!0),i.withCredentials=!0,i.setRequestHeader("Content-Type","application/octet-stream"),i.setRequestHeader("X-Chunk-Offset",String(e));const u=null==(o=s().session)?void 0:o.csrfToken;u&&i.setRequestHeader("X-CSRF-Token",u),i.send(a)})}configureContent(){const t=this.inspect;return m("div",{className:"Modal-body"},m("h4",null,F("inspect_title")),m("dl",{className:"BackupImport-meta"},t.meta.created_at&&m("[",null,m("dt",null,F("meta_when")),m("dd",null,t.meta.created_at)),t.meta.flarum_version&&m("[",null,m("dt",null,F("meta_flarum")),m("dd",null,t.meta.flarum_version)),t.meta.contents&&m("[",null,m("dt",null,F("meta_contents")),m("dd",null,t.meta.contents.join(", "))),t.meta.source_url&&m("[",null,m("dt",null,F("meta_source_url")),m("dd",null,m("code",null,t.meta.source_url))),m("dt",null,F("meta_size")),m("dd",null,_(t.size))),m("div",{className:"Alert Alert--info BackupImport-urlNote"},m("i",{className:"icon fas fa-info-circle"})," ",F("url_rewrite_note")),this.selectionFieldset(t),t.is_encrypted&&m("fieldset",{className:"BackupImport-fieldset"},m("legend",null,F("key_title")),m("p",{className:"helpText"},F("key_help")),m("textarea",{className:"FormControl BackupImport-keyInput",rows:3,placeholder:"base64 private key",value:this.privateKey,oninput:t=>{this.privateKey=t.target.value}}),m("p",{className:"helpText BackupImport-keyHint"},F("key_hint_local"))),m("div",{className:"Alert Alert--error BackupImport-confirmAlert"},m("strong",null,F("confirm_title")),m("p",null,F("confirm_body")),m("label",{className:"BackupImport-confirm"},m("input",{type:"checkbox",checked:this.confirmReplace,onchange:t=>{this.confirmReplace=t.target.checked}})," ",m("span",null,F("confirm_check")))),m("div",{className:"Form-group"},m(p(),{className:"Button Button--primary",loading:this.starting,disabled:this.starting||!this.confirmReplace,onclick:()=>this.startRestore()},F("start_button"))))}selectionFieldset(t){const e=t.meta.contents||[],s=t.meta.manifest||{},a=e.includes("db"),n=e.includes("assets"),r=e.includes("storage"),o=e.includes("extensions"),i=(s.extensions||[]).map(t=>"string"==typeof t?{id:t,location:"workbench"}:t);return m("fieldset",{className:"BackupImport-fieldset"},m("legend",null,F("selection_title")),m("p",{className:"helpText"},F("selection_help")),a&&this.sectionRow("db",this.sectionDb,t=>this.sectionDb=t),n&&this.sectionRow("assets",this.sectionAssets,t=>this.sectionAssets=t,s.asset_count),r&&this.sectionRow("storage",this.sectionStorage,t=>this.sectionStorage=t,s.storage_count),o&&m("[",null,this.sectionRow("extensions",this.sectionExtensions,t=>{this.sectionExtensions=t;for(const e of i)this.extensionsByName[e.id]=t},s.extension_count),this.sectionExtensions&&s.has_composer&&m("div",{className:"BackupImport-composerNote helpText"},m("i",{className:"icon fas fa-cube"})," ",F("extensions_composer_note")),this.sectionExtensions&&i.length>0&&m("div",{className:"BackupImport-extList"},i.map(t=>m("label",{className:"BackupImport-extRow",key:t.id},m("input",{type:"checkbox",checked:!!this.extensionsByName[t.id],onchange:e=>{this.extensionsByName[t.id]=e.target.checked}})," ",m("span",{className:"BackupImport-extTitle"},t.title||t.id)," ",t.name&&t.name!==t.id&&m("code",{className:"BackupImport-extName"},t.name),t.location&&m("span",{className:"BackupImport-extTag BackupImport-extTag--".concat(t.location)},F("extensions_tag_"+t.location)))))),(a||s.has_root_extend)&&m("div",{className:"BackupImport-serverState"},m("h4",null,F("server_state_title")),m("p",{className:"helpText"},F("server_state_help")),a&&this.sectionRow("preserve_settings",this.preserveSettings,t=>{this.preserveSettings=t}),s.has_root_extend&&this.sectionRow("root_extend",this.sectionRootExtend,t=>{this.sectionRootExtend=t}),this.sectionRootExtend&&m("div",{className:"BackupImport-composerNote helpText"},m("i",{className:"icon fas fa-triangle-exclamation"})," ",F("root_extend_warning"))))}sectionRow(t,e,s,a){return m("label",{className:"BackupImport-sectionRow"},m("input",{type:"checkbox",checked:e,onchange:t=>s(t.target.checked)})," ",m("span",{className:"BackupImport-sectionLabel"},F("section_"+t)),void 0!==a&&a>0&&m("span",{className:"BackupImport-sectionCount helpText"}," ","(",F("section_count",{count:a}),")"))}buildSelection(){const t=Object.entries(this.extensionsByName),e=t.length>0&&t.every(t=>A(t,2)[1]),s=!!this.sectionExtensions&&(!!e||t.filter(t=>A(t,2)[1]).map(t=>A(t,1)[0]));return{db:this.sectionDb,assets:this.sectionAssets,storage:this.sectionStorage,extensions:s,root_extend:this.sectionRootExtend,preserve_settings:this.preserveSettings}}async startRestore(){if(this.inspect){this.starting=!0;try{const t=await x({method:"POST",url:"".concat(k(),"/backup/imports/").concat(this.inspect.job_id,"/start"),surface:!1,body:{private_key:this.privateKey.trim()||null,confirm_replace:this.confirmReplace,selection:this.buildSelection()}});this.stage="progress",this.status={phase:t.phase,message:t.message,progress:{total_bytes:this.inspect.size,processed_bytes:0,extracted_entries:0,restored_statements:0,percent:0}},m.redraw(),this.pump()}catch(t){s().alerts.show({type:"error"},v(t,String(F("start_failed"))))}finally{this.starting=!1}}}progressContent(){var t;const e=this.status;if(!e)return m(h(),null);if("done"===e.phase)return this.completedContent();const s="error"===e.phase,a=Math.max(0,Math.min(100,(null==(t=e.progress)?void 0:t.percent)||0));return m("div",{className:"Modal-body BackupImport-progress"},m("div",{className:"BackupImport-status BackupImport-status--".concat(e.phase)},m("strong",null,F("phase_"+e.phase)),m("p",null,e.message)),!s&&m("div",{className:"BackupImport-bar"},m("div",{className:"BackupImport-bar-fill",style:{width:"".concat(a,"%")}})),m("div",{className:"Form-group BackupImport-progress-actions"},!s&&m(p(),{className:"Button",onclick:()=>this.cancel()},F("cancel_button")),s&&m(p(),{className:"Button Button--primary",onclick:()=>this.close()},F("close_button"))))}completedWarnings(){var t,e;const s=null!=(t=null==(e=this.status)?void 0:e.warnings)?t:[];return s.length?m("div",{className:"Alert Alert--error BackupImport-completedWarnings"},m("strong",null,F("incomplete_title")),m("ul",null,s.map(t=>m("li",null,t)))):null}completedContent(){var t;if(this.sectionDb)return m("div",{className:"Modal-body BackupImport-completed BackupImport-completed--logout"},this.completedWarnings(),m("div",{className:"BackupImport-completedIcon"},m("i",{className:"fas fa-right-from-bracket"})),m("h3",{className:"BackupImport-completedTitle"},F("logout_title")),m("p",{className:"BackupImport-completedBody"},F("logout_body")),m("ol",{className:"BackupImport-completedSteps"},m("li",null,F("logout_step_reload")),m("li",null,F("logout_step_login"))),m(p(),{className:"Button Button--primary BackupImport-completedAction",icon:"fas fa-rotate",onclick:()=>window.location.reload()},F("logout_button")));const e=!(null==(t=this.status)||!t.incomplete);return m("div",{className:"Modal-body BackupImport-completed"},this.completedWarnings(),m("div",{className:"BackupImport-completedIcon BackupImport-completedIcon--".concat(e?"warning":"success")},m("i",{className:e?"fas fa-triangle-exclamation":"fas fa-circle-check"})),m("h3",{className:"BackupImport-completedTitle"},F(e?"incomplete_title":"done_title")),m("p",{className:"BackupImport-completedBody"},F(e?"incomplete_body":"done_body")),m(p(),{className:"Button Button--primary",onclick:()=>this.close()},F("close_button")))}async pump(){if(!this.polling&&this.inspect){this.polling=!0;try{for(var t;this.inspect&&this.status&&"done"!==this.status.phase&&"error"!==this.status.phase;)try{const t=await x({method:"POST",url:"".concat(k(),"/backup/imports/").concat(this.inspect.job_id,"/tick"),surface:!1,body:{private_key:this.privateKey.trim()||null}});this.status=t,m.redraw()}catch(t){console.error("[backup] import tick failed",t);const e=v(t,String(F("phase_error_network")));this.status=K(K({},this.status),{},{phase:"error",message:e}),m.redraw();break}"done"===(null==(t=this.status)?void 0:t.phase)&&(s().alerts.show({type:"success"},F("completed")),this.sectionDb||this.attrs.onComplete())}finally{this.polling=!1}}}async cancel(){if(this.inspect){try{await x({method:"DELETE",url:"".concat(k(),"/backup/imports/").concat(this.inspect.job_id),surface:!1})}catch(t){console.error("[backup] import cancel failed",t),s().alerts.show({type:"warning"},F("cancel_failed_warn"))}this.close()}}close(){this.hide()}}function q(t,e){var s=Object.keys(t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(t);e&&(a=a.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),s.push.apply(s,a)}return s}function U(t){for(var e=1;ethis.decide(!0)},a),m(p(),{className:"Button",onclick:()=>this.decide(!1)},n)))}decide(t){var e;this.resolved||(this.resolved=!0,null==(e=t?this.attrs.onConfirm:this.attrs.onCancel)||e(),this.hide())}onbeforeremove(t){var e,s;return this.resolved||(this.resolved=!0,null==(e=(s=this.attrs).onCancel)||e.call(s)),super.onbeforeremove(t)}}flarum.reg.add("ramon-backup","admin/components/ConfirmModal",G);const H=t=>s().translator.trans("ramon-backup.admin.errors.".concat(t));class W extends(c()){constructor(){super(...arguments),i(this,"failed",!1),i(this,"lastError",null)}view(t){if(this.failed){const e=()=>{this.failed=!1,this.lastError=null,m.redraw()};return t.attrs.fallback?t.attrs.fallback(this.lastError,e):m("div",{className:"Alert Alert--error BackupErrorBoundary"},m("strong",null,H("boundary_title")),m("p",null,H("boundary_body")),m("button",{type:"button",className:"Button",onclick:e},H("boundary_retry")))}try{return t.children}catch(a){var e,s;return this.failed=!0,this.lastError=a,null==(e=(s=t.attrs).onError)||e.call(s,a),console.error("[backup] render boundary caught",a),null}}}flarum.reg.add("ramon-backup","admin/utils/errorBoundary",{ErrorBoundary:W});const Q=(t,e)=>s().translator.trans("ramon-backup.admin.".concat(t),null!=e?e:{});class X extends(c()){constructor(){super(...arguments),i(this,"listState","loading"),i(this,"listError",null),i(this,"backups",[])}oninit(t){super.oninit(t),this.refresh()}view(){return m("div",{className:"BackupPanel"},m(W,{onError:t=>console.error("[backup] panel render",t)},m("section",{className:"BackupPanel-actions"},m("h3",null,Q("panel.actions_title")),m("p",{className:"helpText"},Q("panel.actions_help")),m("div",{className:"BackupPanel-actionButtons"},m(p(),{className:"Button Button--primary",icon:"fas fa-download",onclick:()=>this.openExport()},Q("panel.create_button")),m(p(),{className:"Button",icon:"fas fa-upload",onclick:()=>this.openImport()},Q("panel.import_button")))),m(E,null),m("section",{className:"BackupPanel-list"},m("h3",null,Q("panel.list_title")),this.renderList())))}renderList(){return"loading"===this.listState?m(h(),null):"error"===this.listState?m("div",{className:"Alert Alert--error BackupPanel-listError"},m("p",null,Q("list.load_failed")),this.listError&&m("p",{className:"helpText"},m("code",null,this.listError)),m(p(),{className:"Button",icon:"fas fa-rotate",onclick:()=>this.refresh()},Q("list.retry"))):m(T,{backups:this.backups,onDelete:t=>this.delete(t),onRefresh:()=>this.refresh()})}refresh(){return this.listState="loading",this.listError=null,x({method:"GET",url:"".concat(k(),"/backup/backups"),surface:!1}).then(t=>{this.backups=t.backups||[],this.listState="ok"}).catch(t=>{this.backups=[],this.listState="error",this.listError=v(t)}).then(()=>{m.redraw()})}openExport(){s().modal.show(D,{onComplete:()=>this.refresh()})}openImport(){s().modal.show(z,{onComplete:()=>this.refresh()})}async delete(t){var e;if(await(e={title:Q("list.confirm_delete_title"),body:Q("list.confirm_delete"),confirmLabel:Q("list.delete_title"),danger:!0},new Promise(t=>{let a=!1;const n=e=>{a||(a=!0,t(e))};s().modal.show(G,U(U({},e),{},{onConfirm:()=>n(!0),onCancel:()=>n(!1)}))})))try{await x({method:"DELETE",url:"".concat(k(),"/backup/backups/").concat(t),surface:!1,fallbackMessage:String(Q("list.delete_failed"))}),s().alerts.show({type:"success"},Q("list.deleted")),this.refresh()}catch(t){s().alerts.show({type:"error"},v(t,String(Q("list.delete_failed"))))}}}flarum.reg.add("ramon-backup","admin/components/BackupPanel",X);const $="ramon-backup";(0,a.override)(r().prototype,"submitButton",function(t){return this.extension&&this.extension.id===$?null:t()}),s().initializers.add($,()=>{s().registry.for($).registerSetting(()=>m(X,null),100,"ramon-backup.panel").registerPermission({icon:"fas fa-file-archive",label:s().translator.trans("ramon-backup.admin.permissions.manage_label"),permission:"backup.manage"},"moderate")})})(),module.exports={}})(); //# sourceMappingURL=admin.js.map \ No newline at end of file diff --git a/js/dist/admin.js.map b/js/dist/admin.js.map index 69d996e..492993b 100644 --- a/js/dist/admin.js.map +++ b/js/dist/admin.js.map @@ -1 +1 @@ -{"version":3,"file":"admin.js","mappings":"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","sources":["webpack://@ramon/backup/webpack/bootstrap","webpack://@ramon/backup/webpack/runtime/compat get default export","webpack://@ramon/backup/webpack/runtime/define property getters","webpack://@ramon/backup/webpack/runtime/hasOwnProperty shorthand","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'admin/app')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/extend')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'admin/components/ExtensionPage')\"","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/typeof.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/defineProperty.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/toPropertyKey.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/toPrimitive.js","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/Component')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/components/Button')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/components/LoadingIndicator')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/components/Modal')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/utils/extractText')\"","webpack://@ramon/backup/./src/admin/utils/api.ts","webpack://@ramon/backup/./src/admin/components/EncryptionCard.tsx","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/helpers/humanTime')\"","webpack://@ramon/backup/./src/admin/components/BackupList.tsx","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/arrayLikeToArray.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/slicedToArray.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/arrayWithHoles.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/iterableToArrayLimit.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/unsupportedIterableToArray.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/nonIterableRest.js","webpack://@ramon/backup/./src/admin/components/ExportModal.tsx","webpack://@ramon/backup/./src/admin/components/ImportModal.tsx","webpack://@ramon/backup/./src/admin/components/ConfirmModal.tsx","webpack://@ramon/backup/./src/admin/utils/errorBoundary.tsx","webpack://@ramon/backup/./src/admin/components/BackupPanel.tsx","webpack://@ramon/backup/./src/admin/index.tsx"],"sourcesContent":["// The require scope\nconst __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tconst getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter/value functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tif(Array.isArray(definition)) {\n\t\tvar i = 0;\n\t\twhile(i < definition.length) {\n\t\t\tvar key = definition[i++];\n\t\t\tvar binding = definition[i++];\n\t\t\tif(!__webpack_require__.o(exports, key)) {\n\t\t\t\tif(binding === 0) {\n\t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, value: definition[i++] });\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: binding });\n\t\t\t\t}\n\t\t\t} else if(binding === 0) { i++; }\n\t\t}\n\t} else {\n\t\tfor(var key in definition) {\n\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t\t}\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/app');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extend');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/components/ExtensionPage');","function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, _typeof(o);\n}\nexport { _typeof as default };","import toPropertyKey from \"./toPropertyKey.js\";\nfunction _defineProperty(e, r, t) {\n return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {\n value: t,\n enumerable: !0,\n configurable: !0,\n writable: !0\n }) : e[r] = t, e;\n}\nexport { _defineProperty as default };","import _typeof from \"./typeof.js\";\nimport toPrimitive from \"./toPrimitive.js\";\nfunction toPropertyKey(t) {\n var i = toPrimitive(t, \"string\");\n return \"symbol\" == _typeof(i) ? i : i + \"\";\n}\nexport { toPropertyKey as default };","import _typeof from \"./typeof.js\";\nfunction toPrimitive(t, r) {\n if (\"object\" != _typeof(t) || !t) return t;\n var e = t[Symbol.toPrimitive];\n if (void 0 !== e) {\n var i = e.call(t, r || \"default\");\n if (\"object\" != _typeof(i)) return i;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (\"string\" === r ? String : Number)(t);\n}\nexport { toPrimitive as default };","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/Component');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Button');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/LoadingIndicator');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Modal');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/utils/extractText');","import app from \"flarum/admin/app\";\n/** Trim trailing slashes off the configured API URL. */\nexport function apiUrl() {\n return (app.forum.attribute(\"apiUrl\") || \"/api\").replace(/\\/+$/, \"\");\n}\nexport function fmtBytes(bytes) {\n if (!Number.isFinite(bytes) || bytes <= 0) return \"0 B\";\n const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n let i = 0;\n let n = bytes;\n while (n >= 1024 && i < units.length - 1) {\n n /= 1024;\n i++;\n }\n return n.toFixed(n >= 100 || i === 0 ? 0 : 1) + \" \" + units[i];\n}\n\n/** Best-effort extraction of a human-readable detail from a RequestError or thrown object. */\nexport function errorDetail(raw, fallback) {\n var _ref, _raw$response$errors$, _raw$response;\n const detail = (_ref = (_raw$response$errors$ = raw == null || (_raw$response = raw.response) == null || (_raw$response = _raw$response.errors) == null || (_raw$response = _raw$response[0]) == null ? void 0 : _raw$response.detail) != null ? _raw$response$errors$ : raw == null ? void 0 : raw.detail) != null ? _ref : typeof (raw == null ? void 0 : raw.message) === \"string\" ? raw.message : undefined;\n if (detail) return String(detail);\n if (fallback) return fallback;\n return String(app.translator.trans(\"ramon-backup.admin.errors.generic\"));\n}\n/**\n * Wrapper around `app.request` that:\n * - logs every failure to console.error with action + URL;\n * - extracts the JSON:API error detail when present;\n * - shows a Flarum alert (unless `surface: false`);\n * - rethrows so the caller can still branch on the failure.\n *\n * Centralised so we don't reinvent error extraction at every call-site.\n */\nexport async function apiRequest(opts) {\n try {\n return await app.request(opts);\n } catch (raw) {\n const detail = errorDetail(raw, opts.fallbackMessage);\n console.error(\"[backup] api error\", opts.method, opts.url, raw);\n if (opts.surface !== false) {\n app.alerts.show({\n type: \"error\"\n }, detail);\n }\n if (raw && typeof raw === \"object\" && !raw.detail) {\n try {\n raw.detail = detail;\n } catch (_unused) {\n /* read-only, ignore */\n }\n }\n throw raw;\n }\n}\nflarum.reg.add('ramon-backup', 'admin/utils/api', { apiUrl: apiUrl,fmtBytes: fmtBytes,errorDetail: errorDetail, });","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from \"flarum/admin/app\";\nimport Component from \"flarum/common/Component\";\nimport Button from \"flarum/common/components/Button\";\nimport Modal from \"flarum/common/components/Modal\";\nimport LoadingIndicator from \"flarum/common/components/LoadingIndicator\";\nimport extractText from \"flarum/common/utils/extractText\";\nimport { apiRequest, apiUrl, errorDetail } from \"../utils/api\";\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.encryption.\".concat(key), params != null ? params : {});\nclass KeypairRevealModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"copied\", false);\n }\n className() {\n return \"BackupRevealModal Modal--medium\";\n }\n title() {\n return trans(\"reveal_modal.title\");\n }\n content() {\n const _this$attrs = this.attrs,\n privateKey = _this$attrs.privateKey,\n configKey = _this$attrs.configKey;\n const snippet = \"'\".concat(configKey, \"' => '\").concat(privateKey, \"',\");\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"p\", null, trans(\"reveal_modal.intro\")), m(\"div\", {\n className: \"Alert Alert--error\"\n }, m(\"strong\", null, trans(\"reveal_modal.warning_title\")), m(\"p\", null, trans(\"reveal_modal.warning_body\"))), m(\"label\", {\n className: \"BackupReveal-label\"\n }, trans(\"reveal_modal.snippet_label\")), m(\"pre\", {\n className: \"BackupReveal-snippet\"\n }, m(\"code\", null, snippet)), m(\"div\", {\n className: \"Form-group BackupReveal-actions\"\n }, m(Button, {\n className: \"Button\",\n icon: \"fas fa-copy\",\n onclick: () => this.copy(snippet)\n }, this.copied ? trans(\"reveal_modal.copied\") : trans(\"reveal_modal.copy_button\")), m(Button, {\n className: \"Button Button--primary\",\n onclick: () => this.hide()\n }, trans(\"reveal_modal.close\"))));\n }\n copy(snippet) {\n if (!navigator.clipboard) {\n app.alerts.show({\n type: \"error\"\n }, trans(\"clipboard_unavailable\"));\n return;\n }\n navigator.clipboard.writeText(snippet).then(() => {\n this.copied = true;\n m.redraw();\n setTimeout(() => {\n this.copied = false;\n m.redraw();\n }, 2000);\n }).catch(err => {\n console.error(\"[backup] clipboard writeText failed\", err);\n app.alerts.show({\n type: \"error\"\n }, trans(\"clipboard_failed\"));\n });\n }\n}\nclass RegenerateConfirmModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"acknowledged\", false);\n _defineProperty(this, \"submitting\", false);\n }\n className() {\n return \"BackupRegenerateModal Modal--medium\";\n }\n title() {\n return trans(\"regenerate_modal.title\");\n }\n content() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"div\", {\n className: \"Alert Alert--error\"\n }, m(\"p\", null, trans(\"regenerate_modal.warning\"))), m(\"label\", {\n className: \"BackupRegenerate-confirm\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: this.acknowledged,\n onchange: e => {\n this.acknowledged = e.target.checked;\n }\n }), \" \", trans(\"regenerate_modal.acknowledge\")), m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary\",\n loading: this.submitting,\n disabled: !this.acknowledged || this.submitting,\n onclick: () => this.submit()\n }, trans(\"regenerate_modal.submit\"))));\n }\n async submit() {\n this.submitting = true;\n m.redraw();\n try {\n await this.attrs.onConfirm();\n this.hide();\n } catch (_unused) {\n // Parent already showed an error toast — keep the modal open so\n // the user can retry without re-acknowledging the warning.\n this.submitting = false;\n m.redraw();\n }\n }\n}\nexport default class EncryptionCard extends Component {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"status\", null);\n _defineProperty(this, \"loadState\", \"loading\");\n _defineProperty(this, \"loadError\", null);\n _defineProperty(this, \"publicCopied\", false);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.refresh();\n }\n view() {\n return m(\"section\", {\n className: \"BackupEncryptionCard\"\n }, m(\"header\", null, m(\"h3\", null, trans(\"section_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"section_help\"))), this.loadState === \"loading\" && m(LoadingIndicator, null), this.loadState === \"error\" && m(\"div\", {\n className: \"Alert Alert--error BackupEncryption-loadError\"\n }, m(\"p\", null, trans(\"status.load_failed\")), this.loadError && m(\"p\", {\n className: \"helpText\"\n }, m(\"code\", null, this.loadError)), m(Button, {\n className: \"Button\",\n icon: \"fas fa-rotate\",\n onclick: () => this.refresh()\n }, trans(\"status.retry\"))), this.loadState === \"ok\" && this.body());\n }\n body() {\n if (!this.status) return m(\"p\", {\n className: \"helpText\"\n }, trans(\"status.unknown\"));\n const s = this.status;\n if (!s.available) {\n return m(\"div\", {\n className: \"Alert Alert--error\"\n }, trans(\"status.libsodium_missing\"));\n }\n return m('[', null, m(\"div\", {\n className: \"BackupEncryption-statusRow\"\n }, this.statusBadge(\"public\", s.has_public_key), this.statusBadge(\"private\", s.private_key_present)), s.healthy && m(\"div\", {\n className: \"Alert Alert--success\"\n }, trans(\"status.healthy\")), !s.has_public_key && !s.private_key_present && m(\"div\", null, m(\"p\", {\n className: \"helpText\"\n }, trans(\"status.not_setup\")), m(Button, {\n className: \"Button Button--primary\",\n icon: \"fas fa-key\",\n onclick: () => this.generate(false)\n }, trans(\"actions.generate\"))), s.has_public_key && s.private_key_present && s.keys_match === false && m(\"div\", {\n className: \"Alert Alert--error\"\n }, m(\"strong\", null, trans(\"status.mismatch_title\")), m(\"p\", null, trans(\"status.mismatch_body\")), m(\"p\", null, m(\"code\", null, \"'\", s.config_key, \"'\"))), s.has_public_key && !s.private_key_present && m(\"div\", {\n className: \"Alert Alert--error\"\n }, m(\"strong\", null, trans(\"status.private_missing_title\")), m(\"p\", null, trans(\"status.private_missing_body\")), m(\"p\", null, m(\"code\", null, \"'\", s.config_key, \"'\"))), s.has_public_key && this.publicKeyPanel(s.public_key || \"\", s.healthy));\n }\n publicKeyPanel(publicKey, healthy) {\n return m(\"div\", {\n className: \"BackupEncryption-publicKey\"\n }, m(\"label\", null, trans(\"public_key.label\")), m(\"div\", {\n className: \"BackupEncryption-publicKeyRow\"\n }, m(\"pre\", null, m(\"code\", null, publicKey)), m(Button, {\n className: \"Button Button--icon\",\n icon: \"fas fa-copy\",\n title: extractText(trans(\"public_key.copy_title\")),\n onclick: () => this.copyPublic(publicKey)\n }, this.publicCopied ? extractText(trans(\"public_key.copied\")) : \"\")), m(\"p\", {\n className: \"helpText\"\n }, healthy ? trans(\"public_key.help_healthy\") : trans(\"public_key.help_broken\")), m(Button, {\n className: \"Button Button--danger\",\n icon: \"fas fa-rotate\",\n onclick: () => this.openRegenerate()\n }, trans(\"public_key.remove_button\")));\n }\n statusBadge(kind, present) {\n return m(\"div\", {\n className: \"BackupEncryption-badge BackupEncryption-badge--\".concat(present ? \"ok\" : \"missing\")\n }, m(\"i\", {\n className: \"icon fas fa-\".concat(present ? \"check\" : \"times\")\n }), m(\"span\", null, trans(\"status.\".concat(kind, \"_key_label\"))), m(\"span\", {\n className: \"BackupEncryption-badgeState\"\n }, trans(\"status.\".concat(present ? \"present\" : \"absent\"))));\n }\n copyPublic(publicKey) {\n if (!publicKey) return;\n if (!navigator.clipboard) {\n app.alerts.show({\n type: \"error\"\n }, trans(\"clipboard_unavailable\"));\n return;\n }\n navigator.clipboard.writeText(publicKey).then(() => {\n this.publicCopied = true;\n m.redraw();\n setTimeout(() => {\n this.publicCopied = false;\n m.redraw();\n }, 2000);\n }).catch(err => {\n console.error(\"[backup] clipboard writeText failed\", err);\n app.alerts.show({\n type: \"error\"\n }, trans(\"clipboard_failed\"));\n });\n }\n refresh() {\n this.loadState = \"loading\";\n this.loadError = null;\n return apiRequest({\n method: \"GET\",\n url: \"\".concat(apiUrl(), \"/backup/encryption/status\"),\n surface: false\n }).then(res => {\n this.status = res;\n this.loadState = \"ok\";\n }).catch(e => {\n this.status = null;\n this.loadState = \"error\";\n this.loadError = errorDetail(e);\n }).then(() => {\n m.redraw();\n });\n }\n async generate(acknowledgeLoss) {\n try {\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/encryption/generate-keypair\"),\n body: {\n acknowledge_loss: acknowledgeLoss\n },\n surface: false\n });\n await this.refresh();\n app.modal.show(KeypairRevealModal, {\n privateKey: res.private_key,\n configKey: res.config_key\n });\n } catch (e) {\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"actions.generate_failed\"))));\n throw e;\n }\n }\n openRegenerate() {\n app.modal.show(RegenerateConfirmModal, {\n onConfirm: () => this.generate(true)\n });\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/EncryptionCard', EncryptionCard);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/helpers/humanTime');","import app from \"flarum/admin/app\";\nimport Component from \"flarum/common/Component\";\nimport Button from \"flarum/common/components/Button\";\nimport humanTime from \"flarum/common/helpers/humanTime\";\nimport { apiUrl, fmtBytes } from \"../utils/api\";\nconst DIALECT_LABEL = {\n mysql: \"MySQL\",\n mariadb: \"MariaDB\",\n postgres: \"PostgreSQL\",\n sqlite: \"SQLite\"\n};\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.list.\".concat(key), params != null ? params : {});\nexport default class BackupList extends Component {\n view(vnode) {\n const _vnode$attrs = vnode.attrs,\n backups = _vnode$attrs.backups,\n onDelete = _vnode$attrs.onDelete;\n if (!backups.length) {\n return m(\"p\", {\n className: \"BackupList-empty helpText\"\n }, trans(\"empty\"));\n }\n return m(\"table\", {\n className: \"BackupList Table\"\n }, m(\"thead\", null, m(\"tr\", null, m(\"th\", null, trans(\"col_when\")), m(\"th\", null, trans(\"col_size\")), m(\"th\", null, trans(\"col_contents\")), m(\"th\", null, trans(\"col_status\")), m(\"th\", null))), m(\"tbody\", null, backups.map(b => m(\"tr\", {\n key: b.id,\n className: \"BackupList-row\"\n }, m(\"td\", null, m(\"div\", {\n className: \"BackupList-when\"\n }, b.created_at ? humanTime(new Date(b.created_at)) : \"—\"), m(\"div\", {\n className: \"BackupList-filename\"\n }, b.filename), b.target_dialect &&\n // Only shown when the admin retargeted the dump at\n // export time — same-engine backups have a NULL\n // target_dialect and don't need the visual noise.\n m(\"div\", {\n className: \"BackupList-target BackupList-target--\".concat(b.target_dialect),\n title: String(trans(\"target_tooltip\", {\n engine: DIALECT_LABEL[b.target_dialect] || b.target_dialect\n }))\n }, m(\"i\", {\n className: \"icon fas fa-arrow-right-arrow-left\"\n }), \" \", trans(\"target_for\", {\n engine: DIALECT_LABEL[b.target_dialect] || b.target_dialect\n }))), m(\"td\", null, fmtBytes(b.size_bytes)), m(\"td\", null, b.contents.map(c => m(\"span\", {\n className: \"BackupList-tag BackupList-tag--\".concat(c)\n }, trans(\"content_\" + c)))), m(\"td\", null, b.encrypted ? m(\"span\", {\n className: \"BackupList-encryption BackupList-encryption--on\"\n }, m(\"i\", {\n className: \"icon fas fa-lock\"\n }), \" \", trans(\"encrypted\")) : m(\"span\", {\n className: \"BackupList-encryption BackupList-encryption--off\"\n }, m(\"i\", {\n className: \"icon fas fa-lock-open\"\n }), \" \", trans(\"plain\"))), m(\"td\", {\n className: \"BackupList-actions\"\n }, m(\"a\", {\n className: \"Button Button--icon\",\n href: \"\".concat(apiUrl(), \"/backup/backups/\").concat(b.id, \"/download\"),\n target: \"_blank\",\n title: String(trans(\"download_title\"))\n }, m(\"i\", {\n className: \"icon fas fa-download\"\n })), m(Button, {\n className: \"Button Button--icon Button--danger\",\n icon: \"fas fa-trash\",\n title: trans(\"delete_title\"),\n onclick: () => onDelete(b.id)\n }))))));\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/BackupList', BackupList);","function _arrayLikeToArray(r, a) {\n (null == a || a > r.length) && (a = r.length);\n for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];\n return n;\n}\nexport { _arrayLikeToArray as default };","import arrayWithHoles from \"./arrayWithHoles.js\";\nimport iterableToArrayLimit from \"./iterableToArrayLimit.js\";\nimport unsupportedIterableToArray from \"./unsupportedIterableToArray.js\";\nimport nonIterableRest from \"./nonIterableRest.js\";\nfunction _slicedToArray(r, e) {\n return arrayWithHoles(r) || iterableToArrayLimit(r, e) || unsupportedIterableToArray(r, e) || nonIterableRest();\n}\nexport { _slicedToArray as default };","function _arrayWithHoles(r) {\n if (Array.isArray(r)) return r;\n}\nexport { _arrayWithHoles as default };","function _iterableToArrayLimit(r, l) {\n var t = null == r ? null : \"undefined\" != typeof Symbol && r[Symbol.iterator] || r[\"@@iterator\"];\n if (null != t) {\n var e,\n n,\n i,\n u,\n a = [],\n f = !0,\n o = !1;\n try {\n if (i = (t = t.call(r)).next, 0 === l) {\n if (Object(t) !== t) return;\n f = !1;\n } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);\n } catch (r) {\n o = !0, n = r;\n } finally {\n try {\n if (!f && null != t[\"return\"] && (u = t[\"return\"](), Object(u) !== u)) return;\n } finally {\n if (o) throw n;\n }\n }\n return a;\n }\n}\nexport { _iterableToArrayLimit as default };","import arrayLikeToArray from \"./arrayLikeToArray.js\";\nfunction _unsupportedIterableToArray(r, a) {\n if (r) {\n if (\"string\" == typeof r) return arrayLikeToArray(r, a);\n var t = {}.toString.call(r).slice(8, -1);\n return \"Object\" === t && r.constructor && (t = r.constructor.name), \"Map\" === t || \"Set\" === t ? Array.from(r) : \"Arguments\" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? arrayLikeToArray(r, a) : void 0;\n }\n}\nexport { _unsupportedIterableToArray as default };","function _nonIterableRest() {\n throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\");\n}\nexport { _nonIterableRest as default };","import _slicedToArray from \"@babel/runtime/helpers/esm/slicedToArray\";\nimport _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nfunction ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }\nfunction _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }\nimport app from \"flarum/admin/app\";\nimport Modal from \"flarum/common/components/Modal\";\nimport Button from \"flarum/common/components/Button\";\nimport LoadingIndicator from \"flarum/common/components/LoadingIndicator\";\nimport { apiRequest, apiUrl, errorDetail, fmtBytes } from \"../utils/api\";\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.export_modal.\".concat(key), params != null ? params : {});\n\n/**\n * Two-stage modal:\n *\n * 1. Form — admin picks what to include and whether to encrypt.\n * 2. Progress — chunked-tick polling drives a progress bar until the\n * server reports phase=done (or phase=error).\n *\n * The \"encryption to a foreign key\" path lets the operator paste a\n * public key from another Flarum install — useful when preparing an\n * archive for transfer to a different server whose keypair is not the\n * one in *this* config.php.\n */\nexport default class ExportModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"stage\", \"form\");\n _defineProperty(this, \"includeDb\", true);\n _defineProperty(this, \"includeAssets\", true);\n _defineProperty(this, \"includeStorage\", false);\n _defineProperty(this, \"includeExtensions\", false);\n // Per-extension selection. Loaded lazily when the user ticks\n // \"Extensions\" for the first time so we don't fire an extra request\n // for admins who never use the feature.\n _defineProperty(this, \"extensionsLoading\", false);\n _defineProperty(this, \"extensionsLoaded\", false);\n _defineProperty(this, \"extensions\", []);\n _defineProperty(this, \"extensionSelected\", {});\n _defineProperty(this, \"encryptionEnabled\", false);\n _defineProperty(this, \"encryptionUseExternal\", false);\n _defineProperty(this, \"externalPublicKey\", \"\");\n // Target engine the dump should be generated for. Empty string =\n // \"same as source\" (the most common case — backing up to restore\n // onto the same install / a clone of it). The non-empty values\n // make this a cross-engine migration: e.g. dump from MySQL,\n // restore onto Postgres.\n _defineProperty(this, \"targetDialect\", \"\");\n _defineProperty(this, \"starting\", false);\n _defineProperty(this, \"jobId\", null);\n _defineProperty(this, \"status\", null);\n _defineProperty(this, \"polling\", false);\n }\n className() {\n return \"BackupExportModal Modal--medium\";\n }\n title() {\n return trans(\"title\");\n }\n content() {\n if (this.stage === \"form\") return this.formContent();\n return this.progressContent();\n }\n\n // ────────────────────────────────────────── form\n\n formContent() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"p\", {\n className: \"helpText\"\n }, trans(\"intro\")), m(\"fieldset\", {\n className: \"BackupExport-fieldset\"\n }, m(\"legend\", null, trans(\"contents_title\")), this.checkbox(\"db\", () => this.includeDb, v => this.includeDb = v), this.checkbox(\"assets\", () => this.includeAssets, v => this.includeAssets = v), this.checkbox(\"storage\", () => this.includeStorage, v => this.includeStorage = v), this.checkbox(\"extensions\", () => this.includeExtensions, v => {\n this.includeExtensions = v;\n // Lazy-load the extension inventory the first time\n // someone ticks the box. The list comes back fast (no\n // disk walking — just metadata from the ExtensionManager).\n if (v && !this.extensionsLoaded) this.loadExtensions();\n }), this.includeExtensions && this.extensionList()), this.includeDb && m(\"fieldset\", {\n className: \"BackupExport-fieldset\"\n }, m(\"legend\", null, trans(\"target_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"target_help\")), m(\"select\", {\n className: \"FormControl BackupExport-targetSelect\",\n value: this.targetDialect,\n onchange: e => {\n this.targetDialect = e.target.value;\n }\n }, m(\"option\", {\n value: \"\"\n }, trans(\"target_same\")), m(\"option\", {\n value: \"mysql\"\n }, trans(\"target_mysql\")), m(\"option\", {\n value: \"mariadb\"\n }, trans(\"target_mariadb\")), m(\"option\", {\n value: \"postgres\"\n }, trans(\"target_postgres\")), m(\"option\", {\n value: \"sqlite\"\n }, trans(\"target_sqlite\")))), m(\"fieldset\", {\n className: \"BackupExport-fieldset\"\n }, m(\"legend\", null, trans(\"encryption_title\")), m(\"label\", {\n className: \"BackupExport-checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: this.encryptionEnabled,\n onchange: e => {\n this.encryptionEnabled = e.target.checked;\n }\n }), \" \", m(\"span\", null, trans(\"encryption_enable\"))), m(\"p\", {\n className: \"helpText\"\n }, trans(\"encryption_help\")), this.encryptionEnabled && m('[', null, m(\"label\", {\n className: \"BackupExport-checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: this.encryptionUseExternal,\n onchange: e => {\n this.encryptionUseExternal = e.target.checked;\n }\n }), \" \", m(\"span\", null, trans(\"encryption_external\"))), this.encryptionUseExternal && m('[', null, m(\"p\", {\n className: \"helpText\"\n }, trans(\"encryption_external_help\")), m(\"textarea\", {\n className: \"FormControl BackupExport-keyInput\",\n rows: 3,\n placeholder: \"base64 public key\",\n value: this.externalPublicKey,\n oninput: e => {\n this.externalPublicKey = e.target.value;\n }\n })))), m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary\",\n loading: this.starting,\n disabled: this.starting || !this.canStart(),\n onclick: () => this.start()\n }, trans(\"start_button\"))));\n }\n extensionList() {\n if (this.extensionsLoading) {\n return m(\"div\", {\n className: \"BackupExport-extLoading\"\n }, m(LoadingIndicator, null));\n }\n if (!this.extensions.length) {\n return m(\"p\", {\n className: \"helpText BackupExport-extEmpty\"\n }, trans(\"extensions_none\"));\n }\n const groups = {\n workbench: [],\n vendor: [],\n unknown: []\n };\n for (const ext of this.extensions) {\n var _groups$ext$location;\n (_groups$ext$location = groups[ext.location]) == null || _groups$ext$location.push(ext);\n }\n return m(\"div\", {\n className: \"BackupExport-extList\"\n }, m(\"div\", {\n className: \"BackupExport-extActions\"\n }, m(\"button\", {\n type: \"button\",\n className: \"BackupExport-extLink\",\n onclick: () => this.toggleAllExtensions(true)\n }, trans(\"extensions_select_all\")), m(\"span\", null, \" \\xB7 \"), m(\"button\", {\n type: \"button\",\n className: \"BackupExport-extLink\",\n onclick: () => this.toggleAllExtensions(false)\n }, trans(\"extensions_select_none\"))), [\"workbench\", \"vendor\", \"unknown\"].filter(loc => groups[loc].length > 0).map(loc => m(\"div\", {\n className: \"BackupExport-extGroup\",\n key: loc\n }, m(\"div\", {\n className: \"BackupExport-extGroupHeader\"\n }, trans(\"extensions_group_\" + loc), \" \", m(\"span\", {\n className: \"helpText\"\n }, \"(\", groups[loc].length, \")\")), groups[loc].map(ext => m(\"label\", {\n className: \"BackupExport-extRow\",\n key: ext.id\n }, m(\"input\", {\n type: \"checkbox\",\n checked: !!this.extensionSelected[ext.id],\n onchange: e => {\n this.extensionSelected[ext.id] = e.target.checked;\n }\n }), \" \", m(\"span\", {\n className: \"BackupExport-extTitle\"\n }, ext.title), \" \", m(\"code\", {\n className: \"BackupExport-extName\"\n }, ext.name || ext.id), m(\"span\", {\n className: \"BackupExport-extTag BackupExport-extTag--\".concat(ext.location)\n }, trans(\"extensions_tag_\" + ext.location)))))));\n }\n toggleAllExtensions(value) {\n for (const ext of this.extensions) this.extensionSelected[ext.id] = value;\n }\n async loadExtensions() {\n this.extensionsLoading = true;\n try {\n const res = await apiRequest({\n method: \"GET\",\n url: \"\".concat(apiUrl(), \"/backup/extensions\"),\n surface: false\n });\n this.extensions = res.extensions || [];\n this.extensionsLoaded = true;\n // Default: every extension ticked. The admin un-ticks the\n // ones they don't want.\n for (const ext of this.extensions) this.extensionSelected[ext.id] = true;\n } catch (e) {\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"extensions_load_failed\"))));\n } finally {\n this.extensionsLoading = false;\n m.redraw();\n }\n }\n checkbox(key, get, set) {\n return m(\"label\", {\n className: \"BackupExport-checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: get(),\n onchange: e => {\n set(e.target.checked);\n }\n }), \" \", m(\"span\", {\n className: \"BackupExport-checkbox-label\"\n }, trans(\"content_\" + key)), m(\"span\", {\n className: \"BackupExport-checkbox-help helpText\"\n }, trans(\"content_\" + key + \"_help\")));\n }\n canStart() {\n if (!this.includeDb && !this.includeAssets && !this.includeStorage && !this.includeExtensions) {\n return false;\n }\n if (this.encryptionEnabled && this.encryptionUseExternal && !this.externalPublicKey.trim()) {\n return false;\n }\n return true;\n }\n\n // ────────────────────────────────────────── progress\n\n progressContent() {\n var _s$progress, _s$warnings$length, _s$warnings;\n const s = this.status;\n if (!s) return m(LoadingIndicator, null);\n const isDone = s.phase === \"done\";\n const isError = s.phase === \"error\";\n const pct = Math.max(0, Math.min(100, ((_s$progress = s.progress) == null ? void 0 : _s$progress.percent) || 0));\n return m(\"div\", {\n className: \"Modal-body BackupExport-progress\"\n }, m(\"div\", {\n className: \"BackupExport-status BackupExport-status--\".concat(s.phase)\n }, m(\"strong\", null, trans(\"phase_\" + s.phase)), m(\"p\", null, s.message)), !isError && m('[', null, m(\"div\", {\n className: \"BackupExport-bar\"\n }, m(\"div\", {\n className: \"BackupExport-bar-fill\",\n style: {\n width: \"\".concat(isDone ? 100 : pct, \"%\")\n },\n role: \"progressbar\",\n \"aria-valuenow\": pct,\n \"aria-valuemin\": 0,\n \"aria-valuemax\": 100\n })), m(\"div\", {\n className: \"BackupExport-stats\"\n }, m(\"span\", null, fmtBytes(s.progress.processed_bytes), \" /\", \" \", fmtBytes(s.progress.total_bytes || s.progress.processed_bytes)), s.progress.total_files > 0 && m(\"span\", null, trans(\"files_count\", {\n done: s.progress.processed_files,\n total: s.progress.total_files\n })))), ((_s$warnings$length = (_s$warnings = s.warnings) == null ? void 0 : _s$warnings.length) != null ? _s$warnings$length : 0) > 0 && m(\"div\", {\n className: \"BackupExport-warnings\",\n role: \"alert\"\n }, m(\"div\", {\n className: \"BackupExport-warnings-title\"\n }, m(\"i\", {\n className: \"icon fas fa-triangle-exclamation\"\n }), \" \", trans(\"warnings_title\", {\n count: s.warnings.length\n })), m(\"p\", {\n className: \"helpText\"\n }, trans(\"warnings_help\")), m(\"ul\", {\n className: \"BackupExport-warnings-list\"\n }, s.warnings.map((w, idx) => m(\"li\", {\n key: idx\n }, w)))), m(\"div\", {\n className: \"Form-group BackupExport-progress-actions\"\n }, !isDone && !isError && m(Button, {\n className: \"Button\",\n onclick: () => this.cancel()\n }, trans(\"cancel_button\")), (isDone || isError) && m(Button, {\n className: \"Button Button--primary\",\n onclick: () => this.close()\n }, trans(\"close_button\"))));\n }\n\n // ────────────────────────────────────────── api calls\n\n async start() {\n this.starting = true;\n try {\n // The backend accepts `extensions` as bool OR string[]. When\n // every box is checked we still send the array (explicit),\n // unless the inventory hasn't even loaded yet — which means\n // the admin ticked the section but never opened the list and\n // implicitly wants \"all\".\n let extensionsField = false;\n if (this.includeExtensions) {\n if (!this.extensionsLoaded) {\n extensionsField = true;\n } else {\n const ids = Object.entries(this.extensionSelected).filter(_ref => {\n let _ref2 = _slicedToArray(_ref, 2),\n v = _ref2[1];\n return v;\n }).map(_ref3 => {\n let _ref4 = _slicedToArray(_ref3, 1),\n k = _ref4[0];\n return k;\n });\n extensionsField = ids;\n }\n }\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/exports\"),\n body: {\n contents: {\n db: this.includeDb,\n assets: this.includeAssets,\n storage: this.includeStorage,\n extensions: extensionsField\n },\n encryption: {\n enabled: this.encryptionEnabled,\n public_key: this.encryptionUseExternal ? this.externalPublicKey.trim() : null\n },\n // Empty string = \"same as source\"; the backend treats null\n // and \"\" identically so this carries the user's choice\n // through unambiguously.\n target_dialect: this.targetDialect || null\n },\n surface: false\n });\n this.jobId = res.job_id;\n this.stage = \"progress\";\n this.status = {\n phase: res.phase,\n message: res.message,\n progress: {\n total_bytes: 0,\n processed_bytes: 0,\n total_files: 0,\n processed_files: 0,\n percent: 0\n }\n };\n this.starting = false;\n m.redraw();\n this.pump();\n } catch (e) {\n this.starting = false;\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"start_failed\"))));\n m.redraw();\n }\n }\n async pump() {\n if (this.polling || !this.jobId) return;\n this.polling = true;\n try {\n var _this$status;\n // Sequential ticks — each /tick call performs ~4MB of work.\n while (this.jobId && this.status && this.status.phase !== \"done\" && this.status.phase !== \"error\") {\n try {\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/exports/\").concat(this.jobId, \"/tick\"),\n surface: false\n });\n this.status = res;\n m.redraw();\n } catch (e) {\n // Convert tick failures into a synthetic error phase so the\n // existing UI shows the close button and a meaningful\n // message instead of freezing on the last %.\n const detail = errorDetail(e, String(trans(\"phase_error_network\")));\n this.status = _objectSpread(_objectSpread({}, this.status), {}, {\n phase: \"error\",\n message: detail\n });\n m.redraw();\n break;\n }\n }\n if (((_this$status = this.status) == null ? void 0 : _this$status.phase) === \"done\") {\n app.alerts.show({\n type: \"success\"\n }, trans(\"completed\"));\n this.attrs.onComplete();\n }\n } finally {\n this.polling = false;\n }\n }\n async cancel() {\n if (!this.jobId) return;\n try {\n await apiRequest({\n method: \"DELETE\",\n url: \"\".concat(apiUrl(), \"/backup/exports/\").concat(this.jobId),\n surface: false\n });\n } catch (e) {\n // The job may still be holding a server-side lock — let the user\n // know so they understand if the next export complains.\n console.warn(\"[backup] export cancel failed\", e);\n app.alerts.show({\n type: \"warning\"\n }, trans(\"cancel_failed_warn\"));\n }\n this.close();\n }\n close() {\n this.jobId = null;\n this.hide();\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/ExportModal', ExportModal);","import _slicedToArray from \"@babel/runtime/helpers/esm/slicedToArray\";\nimport _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nfunction ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }\nfunction _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }\nimport app from \"flarum/admin/app\";\nimport Modal from \"flarum/common/components/Modal\";\nimport Button from \"flarum/common/components/Button\";\nimport LoadingIndicator from \"flarum/common/components/LoadingIndicator\";\nimport { apiRequest, apiUrl, errorDetail, fmtBytes } from \"../utils/api\";\n\n/** Abort the upload XHR if no progress event fires for this long. */\nconst UPLOAD_IDLE_TIMEOUT_MS = 60000;\n\n/**\n * Fallback chunk size if /backup/imports init doesn't return one.\n * Server-recommended is 4 MB (see UploadImportController::RECOMMENDED_CHUNK_BYTES).\n */\nconst FALLBACK_CHUNK_BYTES = 4 * 1024 * 1024;\n\n/**\n * How many times to retry a single failed chunk before giving up on\n * the whole upload. Each retry uses the same offset so it overwrites\n * (idempotent). Two retries cover a transient hiccup without\n * spinning forever on a real outage.\n */\nconst CHUNK_RETRY_LIMIT = 2;\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.import_modal.\".concat(key), params != null ? params : {});\n\n/**\n * Three-stage modal:\n * 1. upload — operator picks a `.flarum` file. We POST to /imports\n * and the server validates the header without\n * decrypting.\n * 2. configure — depending on the inspect result we ask for the\n * private key (only when the archive is encrypted)\n * AND a confirm-replace checkbox in all cases. The\n * user agreed in setup that we'd ask at this point.\n * 3. progress — chunked-tick polling drives a progress bar.\n */\nexport default class ImportModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"stage\", \"upload\");\n _defineProperty(this, \"file\", null);\n _defineProperty(this, \"uploading\", false);\n _defineProperty(this, \"uploadProgress\", 0);\n _defineProperty(this, \"uploadIndeterminate\", false);\n _defineProperty(this, \"uploadError\", null);\n _defineProperty(this, \"inspect\", null);\n _defineProperty(this, \"privateKey\", \"\");\n _defineProperty(this, \"confirmReplace\", false);\n _defineProperty(this, \"starting\", false);\n // Section toggles. Default to \"everything that's actually inside the\n // archive\" — the user explicitly opts OUT of pieces they don't want\n // to overwrite. Initialised when the inspect result arrives.\n _defineProperty(this, \"sectionDb\", false);\n _defineProperty(this, \"sectionAssets\", false);\n _defineProperty(this, \"sectionStorage\", false);\n _defineProperty(this, \"sectionExtensions\", false);\n // Per-extension toggles, keyed by directory name from the manifest.\n _defineProperty(this, \"extensionsByName\", {});\n _defineProperty(this, \"status\", null);\n _defineProperty(this, \"polling\", false);\n }\n className() {\n return \"BackupImportModal Modal--medium\";\n }\n title() {\n var _this$status;\n if (this.stage === \"progress\" && ((_this$status = this.status) == null ? void 0 : _this$status.phase) === \"done\") {\n return this.sectionDb ? trans(\"logout_title\") : trans(\"done_title\");\n }\n return trans(\"title\");\n }\n content() {\n if (this.stage === \"upload\") return this.uploadContent();\n if (this.stage === \"configure\") return this.configureContent();\n return this.progressContent();\n }\n\n // ────────────────────────────────────────── upload stage\n\n uploadContent() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"div\", {\n className: \"Alert Alert--warning\"\n }, m(\"strong\", null, trans(\"warning_title\")), m(\"p\", null, trans(\"warning_body\"))), m(\"label\", {\n className: \"BackupImport-fileLabel\"\n }, m(\"input\", {\n type: \"file\",\n accept: \".flarum\",\n onchange: e => {\n var _files;\n const f = ((_files = e.target.files) == null ? void 0 : _files[0]) || null;\n this.file = f;\n }\n }), this.file ? m(\"span\", null, this.file.name, \" \", m(\"span\", {\n className: \"helpText\"\n }, \"(\", fmtBytes(this.file.size), \")\")) : m(\"span\", {\n className: \"helpText\"\n }, trans(\"choose_file\"))), this.uploadError && m(\"div\", {\n className: \"Alert Alert--error\"\n }, this.uploadError), this.uploading && m(\"div\", {\n className: \"BackupImport-uploadProgress\"\n }, m(\"div\", {\n className: \"BackupImport-bar\"\n }, m(\"div\", {\n className: \"BackupImport-bar-fill\" + (this.uploadIndeterminate ? \" BackupImport-bar-fill--indeterminate\" : \"\"),\n style: this.uploadIndeterminate ? undefined : {\n width: \"\".concat(Math.max(2, this.uploadProgress), \"%\")\n }\n })), m(\"div\", {\n className: \"BackupImport-uploadStatus helpText\"\n }, this.uploadIndeterminate ? trans(\"inspecting_archive\") : trans(\"uploading_pct\", {\n pct: this.uploadProgress\n }))), m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary\",\n loading: this.uploading,\n disabled: this.uploading || !this.file,\n onclick: () => this.upload()\n }, trans(\"upload_button\"))));\n }\n async upload() {\n if (!this.file) return;\n this.uploading = true;\n this.uploadProgress = 0;\n this.uploadIndeterminate = false;\n this.uploadError = null;\n try {\n var _res$meta$manifest;\n // Use a raw XHR so we can show upload progress — Flarum's\n // app.request (mithril's m.request) does not expose\n // `xhr.upload.onprogress`. Once 100% has been sent the server\n // still has to read the header + validate the archive, so we\n // flip to an indeterminate \"Inspecting…\" state until the\n // response comes back.\n const res = await this.uploadWithProgress(this.file, pct => {\n this.uploadProgress = pct;\n if (pct >= 100) this.uploadIndeterminate = true;\n m.redraw();\n });\n this.inspect = res;\n\n // Seed section toggles from what the archive actually contains.\n // Anything missing from the archive can't be ticked anyway, so\n // there's no value in defaulting it to true.\n const contents = res.meta.contents || [];\n this.sectionDb = contents.includes(\"db\");\n this.sectionAssets = contents.includes(\"assets\");\n this.sectionStorage = contents.includes(\"storage\");\n this.sectionExtensions = contents.includes(\"extensions\");\n\n // Normalise to {id} regardless of which manifest version the\n // archive was packed with (string[] vs ArchiveExtensionEntry[]).\n const exts = ((_res$meta$manifest = res.meta.manifest) == null ? void 0 : _res$meta$manifest.extensions) || [];\n this.extensionsByName = {};\n for (const e of exts) {\n const id = typeof e === \"string\" ? e : e.id;\n if (id) this.extensionsByName[id] = true;\n }\n this.stage = \"configure\";\n } catch (e) {\n console.error(\"[backup] archive upload failed\", e);\n this.uploadError = errorDetail(e, String(trans(\"upload_failed\")));\n } finally {\n this.uploading = false;\n m.redraw();\n }\n }\n\n /**\n * Chunked upload + inspect.\n *\n * Single multipart POSTs of multi-GB archives reliably hit server\n * caps (`upload_max_filesize`, `post_max_size`, nginx\n * `client_max_body_size`, `memory_limit` during multipart parsing)\n * and surface as 500s. Instead we do three small requests:\n *\n * 1. POST /backup/imports — init, gets job_id + chunk_size\n * 2. POST /backup/imports/{id}/chunk* — append each slice (loop)\n * 3. POST /backup/imports/{id}/inspect — finalise, return meta\n *\n * Progress is computed from `bytesSent / file.size` across all chunk\n * requests so the bar advances smoothly through the entire file\n * even though each individual request only carries a few MB.\n */\n async uploadWithProgress(file, onProgress) {\n // ─── 1. init ──────────────────────────────────────────────────\n const init = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/imports\"),\n body: {\n filename: file.name,\n size: file.size\n },\n surface: false\n });\n const jobId = init.job_id;\n const chunkSize = init.chunk_size > 0 ? init.chunk_size : FALLBACK_CHUNK_BYTES;\n\n // ─── 2. chunk loop ────────────────────────────────────────────\n let offset = 0;\n while (offset < file.size) {\n const end = Math.min(offset + chunkSize, file.size);\n const slice = file.slice(offset, end);\n let attempt = 0;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n await this.sendChunk(jobId, offset, slice);\n break;\n } catch (e) {\n attempt++;\n if (attempt > CHUNK_RETRY_LIMIT) throw e;\n // Back off briefly before retrying — gives a transient\n // hiccup a moment to clear without spamming the server.\n await new Promise(r => setTimeout(r, 750 * attempt));\n }\n }\n offset = end;\n const pct = Math.min(99, Math.round(offset / file.size * 100));\n onProgress(pct);\n }\n\n // ─── 3. inspect ───────────────────────────────────────────────\n onProgress(100);\n return apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/imports/\").concat(jobId, \"/inspect\"),\n surface: false\n });\n }\n\n /**\n * Single-chunk upload as a raw octet-stream POST. We use XHR\n * (rather than fetch) so the per-chunk idle timeout works the\n * same way it did for the old monolithic upload, and so we can\n * pull a detailed error message off any non-2xx response body.\n */\n sendChunk(jobId, offset, slice) {\n return new Promise((resolve, reject) => {\n var _session;\n const xhr = new XMLHttpRequest();\n let lastProgress = Date.now();\n const idleTimer = setInterval(() => {\n if (Date.now() - lastProgress > UPLOAD_IDLE_TIMEOUT_MS) {\n clearInterval(idleTimer);\n xhr.abort();\n }\n }, 5000);\n const stopIdleTimer = () => clearInterval(idleTimer);\n xhr.upload.addEventListener(\"progress\", () => {\n lastProgress = Date.now();\n });\n xhr.addEventListener(\"load\", () => {\n stopIdleTimer();\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n let detail;\n try {\n var _JSON$parse;\n detail = (_JSON$parse = JSON.parse(xhr.responseText)) == null || (_JSON$parse = _JSON$parse.errors) == null || (_JSON$parse = _JSON$parse[0]) == null ? void 0 : _JSON$parse.detail;\n } catch (_unused) {\n /* non-JSON body */\n }\n reject({\n detail: detail || \"\".concat(xhr.status, \" \").concat(xhr.statusText)\n });\n }\n });\n xhr.addEventListener(\"error\", () => {\n stopIdleTimer();\n reject({\n detail: String(trans(\"upload_failed\"))\n });\n });\n xhr.addEventListener(\"abort\", () => {\n stopIdleTimer();\n reject({\n detail: String(trans(\"upload_idle_timeout\"))\n });\n });\n xhr.open(\"POST\", \"\".concat(apiUrl(), \"/backup/imports/\").concat(jobId, \"/chunk\"), true);\n xhr.withCredentials = true;\n xhr.setRequestHeader(\"Content-Type\", \"application/octet-stream\");\n xhr.setRequestHeader(\"X-Chunk-Offset\", String(offset));\n const csrf = (_session = app.session) == null ? void 0 : _session.csrfToken;\n if (csrf) xhr.setRequestHeader(\"X-CSRF-Token\", csrf);\n xhr.send(slice);\n });\n }\n\n // ────────────────────────────────────────── configure stage\n\n configureContent() {\n const i = this.inspect;\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"h4\", null, trans(\"inspect_title\")), m(\"dl\", {\n className: \"BackupImport-meta\"\n }, i.meta.created_at && m('[', null, m(\"dt\", null, trans(\"meta_when\")), m(\"dd\", null, i.meta.created_at)), i.meta.flarum_version && m('[', null, m(\"dt\", null, trans(\"meta_flarum\")), m(\"dd\", null, i.meta.flarum_version)), i.meta.contents && m('[', null, m(\"dt\", null, trans(\"meta_contents\")), m(\"dd\", null, i.meta.contents.join(\", \"))), i.meta.source_url && m('[', null, m(\"dt\", null, trans(\"meta_source_url\")), m(\"dd\", null, m(\"code\", null, i.meta.source_url))), m(\"dt\", null, trans(\"meta_size\")), m(\"dd\", null, fmtBytes(i.size))), m(\"div\", {\n className: \"Alert Alert--info BackupImport-urlNote\"\n }, m(\"i\", {\n className: \"icon fas fa-info-circle\"\n }), \" \", trans(\"url_rewrite_note\")), this.selectionFieldset(i), i.is_encrypted && m(\"fieldset\", {\n className: \"BackupImport-fieldset\"\n }, m(\"legend\", null, trans(\"key_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"key_help\")), m(\"textarea\", {\n className: \"FormControl BackupImport-keyInput\",\n rows: 3,\n placeholder: \"base64 private key\",\n value: this.privateKey,\n oninput: e => {\n this.privateKey = e.target.value;\n }\n }), m(\"p\", {\n className: \"helpText BackupImport-keyHint\"\n }, trans(\"key_hint_local\"))), m(\"div\", {\n className: \"Alert Alert--error BackupImport-confirmAlert\"\n }, m(\"strong\", null, trans(\"confirm_title\")), m(\"p\", null, trans(\"confirm_body\")), m(\"label\", {\n className: \"BackupImport-confirm\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: this.confirmReplace,\n onchange: e => {\n this.confirmReplace = e.target.checked;\n }\n }), \" \", m(\"span\", null, trans(\"confirm_check\")))), m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary\",\n loading: this.starting,\n disabled: this.starting || !this.confirmReplace,\n onclick: () => this.startRestore()\n }, trans(\"start_button\"))));\n }\n selectionFieldset(i) {\n const contents = i.meta.contents || [];\n const manifest = i.meta.manifest || {};\n const hasDb = contents.includes(\"db\");\n const hasAssets = contents.includes(\"assets\");\n const hasStorage = contents.includes(\"storage\");\n const hasExtensions = contents.includes(\"extensions\");\n const rawExtList = manifest.extensions || [];\n // Normalise to a uniform shape so the renderer can stay simple,\n // regardless of which manifest version the archive used.\n const extList = rawExtList.map(e => typeof e === \"string\" ? {\n id: e,\n location: \"workbench\"\n } : e);\n return m(\"fieldset\", {\n className: \"BackupImport-fieldset\"\n }, m(\"legend\", null, trans(\"selection_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"selection_help\")), hasDb && this.sectionRow(\"db\", this.sectionDb, v => this.sectionDb = v), hasAssets && this.sectionRow(\"assets\", this.sectionAssets, v => this.sectionAssets = v, manifest.asset_count), hasStorage && this.sectionRow(\"storage\", this.sectionStorage, v => this.sectionStorage = v, manifest.storage_count), hasExtensions && m('[', null, this.sectionRow(\"extensions\", this.sectionExtensions, v => {\n this.sectionExtensions = v;\n // Cascade: turning the section off / on flips every\n // child to match. The user can then untick individuals.\n for (const ext of extList) this.extensionsByName[ext.id] = v;\n }, manifest.extension_count), this.sectionExtensions && manifest.has_composer && m(\"div\", {\n className: \"BackupImport-composerNote helpText\"\n }, m(\"i\", {\n className: \"icon fas fa-cube\"\n }), \" \", trans(\"extensions_composer_note\")), this.sectionExtensions && extList.length > 0 && m(\"div\", {\n className: \"BackupImport-extList\"\n }, extList.map(ext => m(\"label\", {\n className: \"BackupImport-extRow\",\n key: ext.id\n }, m(\"input\", {\n type: \"checkbox\",\n checked: !!this.extensionsByName[ext.id],\n onchange: e => {\n this.extensionsByName[ext.id] = e.target.checked;\n }\n }), \" \", m(\"span\", {\n className: \"BackupImport-extTitle\"\n }, ext.title || ext.id), \" \", ext.name && ext.name !== ext.id && m(\"code\", {\n className: \"BackupImport-extName\"\n }, ext.name), ext.location && m(\"span\", {\n className: \"BackupImport-extTag BackupImport-extTag--\".concat(ext.location)\n }, trans(\"extensions_tag_\" + ext.location)))))));\n }\n sectionRow(key, checked, set, count) {\n return m(\"label\", {\n className: \"BackupImport-sectionRow\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: checked,\n onchange: e => set(e.target.checked)\n }), \" \", m(\"span\", {\n className: \"BackupImport-sectionLabel\"\n }, trans(\"section_\" + key)), count !== undefined && count > 0 && m(\"span\", {\n className: \"BackupImport-sectionCount helpText\"\n }, \" \", \"(\", trans(\"section_count\", {\n count\n }), \")\"));\n }\n buildSelection() {\n // The backend treats `extensions: true` as \"all\" and an array as\n // a whitelist of directory names. When every box is checked, send\n // `true` so the user's intent isn't lost if a new extension shows\n // up between inspect and apply (it shouldn't, but be defensive).\n const extEntries = Object.entries(this.extensionsByName);\n const allChecked = extEntries.length > 0 && extEntries.every(_ref => {\n let _ref2 = _slicedToArray(_ref, 2),\n v = _ref2[1];\n return v;\n });\n const extensionsField = !this.sectionExtensions ? false : allChecked ? true : extEntries.filter(_ref3 => {\n let _ref4 = _slicedToArray(_ref3, 2),\n v = _ref4[1];\n return v;\n }).map(_ref5 => {\n let _ref6 = _slicedToArray(_ref5, 1),\n k = _ref6[0];\n return k;\n });\n return {\n db: this.sectionDb,\n assets: this.sectionAssets,\n storage: this.sectionStorage,\n extensions: extensionsField\n };\n }\n async startRestore() {\n if (!this.inspect) return;\n this.starting = true;\n try {\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/imports/\").concat(this.inspect.job_id, \"/start\"),\n surface: false,\n body: {\n private_key: this.privateKey.trim() || null,\n confirm_replace: this.confirmReplace,\n selection: this.buildSelection()\n }\n });\n this.stage = \"progress\";\n this.status = {\n phase: res.phase,\n message: res.message,\n progress: {\n total_bytes: this.inspect.size,\n processed_bytes: 0,\n extracted_entries: 0,\n restored_statements: 0,\n percent: 0\n }\n };\n m.redraw();\n this.pump();\n } catch (e) {\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"start_failed\"))));\n } finally {\n this.starting = false;\n }\n }\n\n // ────────────────────────────────────────── progress stage\n\n progressContent() {\n var _s$progress;\n const s = this.status;\n if (!s) return m(LoadingIndicator, null);\n\n // Once the server reports phase=done we hand the screen over to a\n // dedicated completion view: the user has finished waiting and\n // now needs to know what to do next (which differs depending on\n // whether the DB was actually replaced).\n if (s.phase === \"done\") return this.completedContent();\n const isError = s.phase === \"error\";\n const pct = Math.max(0, Math.min(100, ((_s$progress = s.progress) == null ? void 0 : _s$progress.percent) || 0));\n return m(\"div\", {\n className: \"Modal-body BackupImport-progress\"\n }, m(\"div\", {\n className: \"BackupImport-status BackupImport-status--\".concat(s.phase)\n }, m(\"strong\", null, trans(\"phase_\" + s.phase)), m(\"p\", null, s.message)), !isError && m(\"div\", {\n className: \"BackupImport-bar\"\n }, m(\"div\", {\n className: \"BackupImport-bar-fill\",\n style: {\n width: \"\".concat(pct, \"%\")\n }\n })), m(\"div\", {\n className: \"Form-group BackupImport-progress-actions\"\n }, !isError && m(Button, {\n className: \"Button\",\n onclick: () => this.cancel()\n }, trans(\"cancel_button\")), isError && m(Button, {\n className: \"Button Button--primary\",\n onclick: () => this.close()\n }, trans(\"close_button\"))));\n }\n\n /**\n * Replaces the progress UI as soon as `phase === 'done'`. Two\n * shapes:\n *\n * - DB restored — the admin's session was just wiped together\n * with the rest of the `users` / `sessions` tables. We make\n * this very clear and offer a single primary action: reload.\n * Anything else (refreshing the list, dismissing) would race\n * against an invalidated cookie and surface a confusing 401.\n *\n * - Files only — the session is fine, just close.\n */\n /**\n * Warning block for a restore that finished but lost entries, or that\n * carries a stack advisory. Rendered on the completed screen in both\n * shapes — an incomplete restore must never present as clean success,\n * which is the failure mode this whole path exists to remove.\n */\n completedWarnings() {\n var _this$status$warnings, _this$status2;\n const warnings = (_this$status$warnings = (_this$status2 = this.status) == null ? void 0 : _this$status2.warnings) != null ? _this$status$warnings : [];\n if (!warnings.length) return null;\n return m(\"div\", {\n className: \"Alert Alert--error BackupImport-completedWarnings\"\n }, m(\"strong\", null, trans(\"incomplete_title\")), m(\"ul\", null, warnings.map(w => m(\"li\", null, w))));\n }\n completedContent() {\n var _this$status3;\n if (this.sectionDb) {\n return m(\"div\", {\n className: \"Modal-body BackupImport-completed BackupImport-completed--logout\"\n }, this.completedWarnings(), m(\"div\", {\n className: \"BackupImport-completedIcon\"\n }, m(\"i\", {\n className: \"fas fa-right-from-bracket\"\n })), m(\"h3\", {\n className: \"BackupImport-completedTitle\"\n }, trans(\"logout_title\")), m(\"p\", {\n className: \"BackupImport-completedBody\"\n }, trans(\"logout_body\")), m(\"ol\", {\n className: \"BackupImport-completedSteps\"\n }, m(\"li\", null, trans(\"logout_step_reload\")), m(\"li\", null, trans(\"logout_step_login\"))), m(Button, {\n className: \"Button Button--primary BackupImport-completedAction\",\n icon: \"fas fa-rotate\",\n onclick: () => window.location.reload()\n }, trans(\"logout_button\")));\n }\n const incomplete = !!((_this$status3 = this.status) != null && _this$status3.incomplete);\n return m(\"div\", {\n className: \"Modal-body BackupImport-completed\"\n }, this.completedWarnings(), m(\"div\", {\n className: \"BackupImport-completedIcon BackupImport-completedIcon--\".concat(incomplete ? \"warning\" : \"success\")\n }, m(\"i\", {\n className: incomplete ? \"fas fa-triangle-exclamation\" : \"fas fa-circle-check\"\n })), m(\"h3\", {\n className: \"BackupImport-completedTitle\"\n }, trans(incomplete ? \"incomplete_title\" : \"done_title\")), m(\"p\", {\n className: \"BackupImport-completedBody\"\n }, trans(incomplete ? \"incomplete_body\" : \"done_body\")), m(Button, {\n className: \"Button Button--primary\",\n onclick: () => this.close()\n }, trans(\"close_button\")));\n }\n async pump() {\n if (this.polling || !this.inspect) return;\n this.polling = true;\n try {\n var _this$status4;\n while (this.inspect && this.status && this.status.phase !== \"done\" && this.status.phase !== \"error\") {\n try {\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/imports/\").concat(this.inspect.job_id, \"/tick\"),\n surface: false,\n // The server never stores the decryption key on disk; we\n // re-send it each tick so the inspect/extract phases can\n // decrypt. Harmless (ignored) for unencrypted archives and\n // once extraction is done.\n body: {\n private_key: this.privateKey.trim() || null\n }\n });\n this.status = res;\n m.redraw();\n } catch (e) {\n // Restore is more dangerous to leave hanging than export —\n // the server may still be mid-write. Surface a synthetic\n // error phase, and explicitly tell the user to verify\n // server state before retrying.\n console.error(\"[backup] import tick failed\", e);\n const detail = errorDetail(e, String(trans(\"phase_error_network\")));\n this.status = _objectSpread(_objectSpread({}, this.status), {}, {\n phase: \"error\",\n message: detail\n });\n m.redraw();\n break;\n }\n }\n if (((_this$status4 = this.status) == null ? void 0 : _this$status4.phase) === \"done\") {\n // Don't refresh the parent panel — when the backup includes\n // the database, restoring it has just replaced the sessions\n // table this admin is authenticated against. Any further API\n // call from this stale session would fail (401 / CSRF) and\n // surface as a confusing \"Oops!\" toast. The user clicks\n // Reload below and gets a clean session.\n app.alerts.show({\n type: \"success\"\n }, trans(\"completed\"));\n if (!this.sectionDb) this.attrs.onComplete();\n }\n } finally {\n this.polling = false;\n }\n }\n async cancel() {\n if (!this.inspect) return;\n try {\n await apiRequest({\n method: \"DELETE\",\n url: \"\".concat(apiUrl(), \"/backup/imports/\").concat(this.inspect.job_id),\n surface: false\n });\n } catch (e) {\n console.error(\"[backup] import cancel failed\", e);\n app.alerts.show({\n type: \"warning\"\n }, trans(\"cancel_failed_warn\"));\n }\n this.close();\n }\n close() {\n this.hide();\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/ImportModal', ImportModal);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nfunction ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }\nfunction _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }\nimport app from \"flarum/admin/app\";\nimport Modal from \"flarum/common/components/Modal\";\nimport Button from \"flarum/common/components/Button\";\n/**\n * Dropin replacement for `window.confirm()`. Native confirm breaks the\n * Flarum dark-mode chrome and is silently auto-rejected by some\n * locked-down corporate browsers — this modal stays inside the SPA.\n *\n * Usage:\n * const ok = await confirmAsync({ title, body, danger: true });\n * if (!ok) return;\n */\nexport default class ConfirmModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"resolved\", false);\n }\n className() {\n return \"BackupConfirmModal Modal--small\";\n }\n title() {\n return this.attrs.title;\n }\n content() {\n var _this$attrs$confirmLa, _this$attrs$cancelLab;\n const confirmLabel = (_this$attrs$confirmLa = this.attrs.confirmLabel) != null ? _this$attrs$confirmLa : app.translator.trans(\"ramon-backup.admin.errors.confirm_default\");\n const cancelLabel = (_this$attrs$cancelLab = this.attrs.cancelLabel) != null ? _this$attrs$cancelLab : app.translator.trans(\"ramon-backup.admin.errors.cancel_default\");\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"div\", {\n className: \"BackupConfirmModal-body\"\n }, this.attrs.body), m(\"div\", {\n className: \"Form-group BackupConfirmModal-actions\"\n }, m(Button, {\n className: \"Button \" + (this.attrs.danger ? \"Button--danger\" : \"Button--primary\"),\n onclick: () => this.decide(true)\n }, confirmLabel), m(Button, {\n className: \"Button\",\n onclick: () => this.decide(false)\n }, cancelLabel)));\n }\n decide(confirmed) {\n var _ref;\n if (this.resolved) return;\n this.resolved = true;\n (_ref = confirmed ? this.attrs.onConfirm : this.attrs.onCancel) == null || _ref();\n this.hide();\n }\n\n // Esc key / backdrop click / X button all funnel through Mithril's\n // remove lifecycle. If the user dismissed without picking a button,\n // treat that as a cancel so the awaiting Promise actually resolves.\n onbeforeremove(vnode) {\n if (!this.resolved) {\n var _this$attrs$onCancel, _this$attrs;\n this.resolved = true;\n (_this$attrs$onCancel = (_this$attrs = this.attrs).onCancel) == null || _this$attrs$onCancel.call(_this$attrs);\n }\n return super.onbeforeremove(vnode);\n }\n}\n\n/** Promise wrapper so callers can `await confirmAsync(...)`. */\nexport function confirmAsync(attrs) {\n return new Promise(resolve => {\n let settled = false;\n const settle = v => {\n if (settled) return;\n settled = true;\n resolve(v);\n };\n app.modal.show(ConfirmModal, _objectSpread(_objectSpread({}, attrs), {}, {\n onConfirm: () => settle(true),\n onCancel: () => settle(false)\n }));\n });\n}\nflarum.reg.add('ramon-backup', 'admin/components/ConfirmModal', ConfirmModal);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from \"flarum/admin/app\";\nimport Component from \"flarum/common/Component\";\nconst trans = key => app.translator.trans(\"ramon-backup.admin.errors.\".concat(key));\n/**\n * Mithril doesn't have React's Error Boundary — but a tiny vnode\n * wrapper that try/catches `children` rendering covers the same\n * 90% case: a single throw inside a render path won't blow up the\n * whole admin SPA.\n *\n * Limitation: only catches synchronous exceptions during render. Async\n * Promise rejections still need their own try/catch — this is not a\n * substitute for handling failures in event handlers or API calls.\n */\nexport class ErrorBoundary extends Component {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"failed\", false);\n _defineProperty(this, \"lastError\", null);\n }\n view(vnode) {\n if (this.failed) {\n const retry = () => {\n this.failed = false;\n this.lastError = null;\n m.redraw();\n };\n if (vnode.attrs.fallback) return vnode.attrs.fallback(this.lastError, retry);\n return m(\"div\", {\n className: \"Alert Alert--error BackupErrorBoundary\"\n }, m(\"strong\", null, trans(\"boundary_title\")), m(\"p\", null, trans(\"boundary_body\")), m(\"button\", {\n type: \"button\",\n className: \"Button\",\n onclick: retry\n }, trans(\"boundary_retry\")));\n }\n try {\n return vnode.children;\n } catch (err) {\n var _vnode$attrs$onError, _vnode$attrs;\n this.failed = true;\n this.lastError = err;\n (_vnode$attrs$onError = (_vnode$attrs = vnode.attrs).onError) == null || _vnode$attrs$onError.call(_vnode$attrs, err);\n console.error(\"[backup] render boundary caught\", err);\n return null;\n }\n }\n}\nflarum.reg.add('ramon-backup', 'admin/utils/errorBoundary', { ErrorBoundary: ErrorBoundary, });","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from \"flarum/admin/app\";\nimport Component from \"flarum/common/Component\";\nimport Button from \"flarum/common/components/Button\";\nimport LoadingIndicator from \"flarum/common/components/LoadingIndicator\";\nimport EncryptionCard from \"./EncryptionCard\";\nimport BackupList from \"./BackupList\";\nimport ExportModal from \"./ExportModal\";\nimport ImportModal from \"./ImportModal\";\nimport { confirmAsync } from \"./ConfirmModal\";\nimport { apiRequest, apiUrl, errorDetail } from \"../utils/api\";\nimport { ErrorBoundary } from \"../utils/errorBoundary\";\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.\".concat(key), params != null ? params : {});\nexport default class BackupPanel extends Component {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"listState\", \"loading\");\n _defineProperty(this, \"listError\", null);\n _defineProperty(this, \"backups\", []);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.refresh();\n }\n view() {\n return m(\"div\", {\n className: \"BackupPanel\"\n }, m(ErrorBoundary, {\n onError: e => console.error(\"[backup] panel render\", e)\n }, m(\"section\", {\n className: \"BackupPanel-actions\"\n }, m(\"h3\", null, trans(\"panel.actions_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"panel.actions_help\")), m(\"div\", {\n className: \"BackupPanel-actionButtons\"\n }, m(Button, {\n className: \"Button Button--primary\",\n icon: \"fas fa-download\",\n onclick: () => this.openExport()\n }, trans(\"panel.create_button\")), m(Button, {\n className: \"Button\",\n icon: \"fas fa-upload\",\n onclick: () => this.openImport()\n }, trans(\"panel.import_button\")))), m(EncryptionCard, null), m(\"section\", {\n className: \"BackupPanel-list\"\n }, m(\"h3\", null, trans(\"panel.list_title\")), this.renderList())));\n }\n renderList() {\n if (this.listState === \"loading\") return m(LoadingIndicator, null);\n if (this.listState === \"error\") {\n return m(\"div\", {\n className: \"Alert Alert--error BackupPanel-listError\"\n }, m(\"p\", null, trans(\"list.load_failed\")), this.listError && m(\"p\", {\n className: \"helpText\"\n }, m(\"code\", null, this.listError)), m(Button, {\n className: \"Button\",\n icon: \"fas fa-rotate\",\n onclick: () => this.refresh()\n }, trans(\"list.retry\")));\n }\n return m(BackupList, {\n backups: this.backups,\n onDelete: id => this.delete(id),\n onRefresh: () => this.refresh()\n });\n }\n refresh() {\n this.listState = \"loading\";\n this.listError = null;\n return apiRequest({\n method: \"GET\",\n url: \"\".concat(apiUrl(), \"/backup/backups\"),\n surface: false\n }).then(res => {\n this.backups = res.backups || [];\n this.listState = \"ok\";\n }).catch(e => {\n this.backups = [];\n this.listState = \"error\";\n this.listError = errorDetail(e);\n }).then(() => {\n m.redraw();\n });\n }\n openExport() {\n app.modal.show(ExportModal, {\n onComplete: () => this.refresh()\n });\n }\n openImport() {\n app.modal.show(ImportModal, {\n onComplete: () => this.refresh()\n });\n }\n async delete(id) {\n const ok = await confirmAsync({\n title: trans(\"list.confirm_delete_title\"),\n body: trans(\"list.confirm_delete\"),\n confirmLabel: trans(\"list.delete_title\"),\n danger: true\n });\n if (!ok) return;\n try {\n await apiRequest({\n method: \"DELETE\",\n url: \"\".concat(apiUrl(), \"/backup/backups/\").concat(id),\n surface: false,\n fallbackMessage: String(trans(\"list.delete_failed\"))\n });\n app.alerts.show({\n type: \"success\"\n }, trans(\"list.deleted\"));\n this.refresh();\n } catch (e) {\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"list.delete_failed\"))));\n }\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/BackupPanel', BackupPanel);","import app from \"flarum/admin/app\";\nimport { override } from \"flarum/common/extend\";\nimport ExtensionPage from \"flarum/admin/components/ExtensionPage\";\nimport BackupPanel from \"./components/BackupPanel\";\nconst EXT_ID = \"ramon-backup\";\n\n// Hide the default \"Save changes\" submit button on our settings page —\n// every action here happens through dedicated buttons (export now, key\n// rotation, etc.), there's no batch settings save to perform.\noverride(ExtensionPage.prototype, \"submitButton\", function (original) {\n if (this.extension && this.extension.id === EXT_ID) return null;\n return original();\n});\napp.initializers.add(EXT_ID, () => {\n app.registry.for(EXT_ID).registerSetting(() => m(BackupPanel, null), 100, \"ramon-backup.panel\").registerPermission({\n icon: \"fas fa-file-archive\",\n label: app.translator.trans(\"ramon-backup.admin.permissions.manage_label\"),\n permission: \"backup.manage\"\n }, \"moderate\");\n});"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","Array","isArray","i","length","key","binding","o","Object","defineProperty","enumerable","value","get","obj","prop","prototype","hasOwnProperty","call","app_namespaceObject","flarum","reg","extend_namespaceObject","ExtensionPage_namespaceObject","_typeof","Symbol","iterator","constructor","_defineProperty","e","r","t","toPrimitive","TypeError","String","toPropertyKey","configurable","writable","Component_namespaceObject","Button_namespaceObject","LoadingIndicator_namespaceObject","Modal_namespaceObject","extractText_namespaceObject","apiUrl","app_default","forum","attribute","replace","fmtBytes","bytes","Number","isFinite","units","n","toFixed","errorDetail","raw","fallback","_ref","_raw$response$errors$","_raw$response","detail","response","errors","message","undefined","translator","trans","async","apiRequest","opts","request","fallbackMessage","console","error","method","url","surface","alerts","show","type","_unused","add","params","concat","KeypairRevealModal","Modal_default","super","arguments","this","className","title","content","_this$attrs","attrs","privateKey","configKey","snippet","m","Button_default","icon","onclick","copy","copied","hide","navigator","clipboard","writeText","then","redraw","setTimeout","catch","err","RegenerateConfirmModal","checked","acknowledged","onchange","target","loading","submitting","disabled","submit","onConfirm","EncryptionCard","Component_default","oninit","vnode","refresh","view","loadState","LoadingIndicator_default","loadError","body","status","s","available","statusBadge","has_public_key","private_key_present","healthy","generate","keys_match","config_key","publicKeyPanel","public_key","publicKey","extractText_default","copyPublic","publicCopied","openRegenerate","kind","present","res","acknowledgeLoss","acknowledge_loss","modal","private_key","humanTime_namespaceObject","DIALECT_LABEL","mysql","mariadb","postgres","sqlite","BackupList_trans","BackupList","_vnode$attrs","backups","onDelete","map","b","id","created_at","humanTime_default","Date","filename","target_dialect","engine","size_bytes","contents","c","encrypted","href","_arrayLikeToArray","_slicedToArray","arrayWithHoles","l","u","f","next","done","push","iterableToArrayLimit","arrayLikeToArray","toString","slice","name","from","test","unsupportedIterableToArray","nonIterableRest","ownKeys","keys","getOwnPropertySymbols","filter","getOwnPropertyDescriptor","apply","_objectSpread","forEach","getOwnPropertyDescriptors","defineProperties","ExportModal_trans","ExportModal","stage","formContent","progressContent","checkbox","includeDb","v","includeAssets","includeStorage","includeExtensions","extensionsLoaded","loadExtensions","extensionList","targetDialect","encryptionEnabled","encryptionUseExternal","rows","placeholder","externalPublicKey","oninput","starting","canStart","start","extensionsLoading","extensions","groups","workbench","vendor","unknown","ext","_groups$ext$location","location","toggleAllExtensions","loc","extensionSelected","set","trim","_s$progress","_s$warnings$length","_s$warnings","isDone","phase","isError","pct","Math","max","min","progress","percent","style","width","role","processed_bytes","total_bytes","total_files","processed_files","total","warnings","count","w","idx","cancel","close","extensionsField","entries","_ref3","db","assets","storage","encryption","enabled","jobId","job_id","pump","polling","_this$status","onComplete","warn","ImportModal_ownKeys","ImportModal_objectSpread","ImportModal_trans","ImportModal","sectionDb","uploadContent","configureContent","accept","_files","files","file","size","uploadError","uploading","uploadIndeterminate","uploadProgress","upload","_res$meta$manifest","uploadWithProgress","inspect","meta","includes","sectionAssets","sectionStorage","sectionExtensions","exts","manifest","extensionsByName","onProgress","init","chunkSize","chunk_size","offset","end","attempt","sendChunk","Promise","round","resolve","reject","_session","xhr","XMLHttpRequest","lastProgress","now","idleTimer","setInterval","clearInterval","abort","stopIdleTimer","addEventListener","_JSON$parse","JSON","parse","responseText","statusText","open","withCredentials","setRequestHeader","csrf","session","csrfToken","send","flarum_version","join","source_url","selectionFieldset","is_encrypted","confirmReplace","startRestore","hasDb","hasAssets","hasStorage","hasExtensions","extList","sectionRow","asset_count","storage_count","extension_count","has_composer","buildSelection","extEntries","allChecked","every","_ref5","confirm_replace","selection","extracted_entries","restored_statements","completedContent","completedWarnings","_this$status$warnings","_this$status2","_this$status3","window","reload","incomplete","_this$status4","ConfirmModal_ownKeys","ConfirmModal_objectSpread","ConfirmModal","_this$attrs$confirmLa","_this$attrs$cancelLab","confirmLabel","cancelLabel","danger","decide","confirmed","resolved","onCancel","onbeforeremove","_this$attrs$onCancel","errorBoundary_trans","ErrorBoundary","failed","retry","lastError","children","_vnode$attrs$onError","onError","BackupPanel_trans","BackupPanel","openExport","openImport","renderList","listState","listError","delete","onRefresh","settled","settle","EXT_ID","override","ExtensionPage_default","original","extension","initializers","registry","for","registerSetting","registerPermission","label","permission"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"admin.js","mappings":"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","sources":["webpack://@ramon/backup/webpack/bootstrap","webpack://@ramon/backup/webpack/runtime/compat get default export","webpack://@ramon/backup/webpack/runtime/define property getters","webpack://@ramon/backup/webpack/runtime/hasOwnProperty shorthand","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'admin/app')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/extend')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'admin/components/ExtensionPage')\"","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/typeof.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/defineProperty.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/toPropertyKey.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/toPrimitive.js","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/Component')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/components/Button')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/components/LoadingIndicator')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/components/Modal')\"","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/utils/extractText')\"","webpack://@ramon/backup/./src/admin/utils/api.ts","webpack://@ramon/backup/./src/admin/components/EncryptionCard.tsx","webpack://@ramon/backup/external root \"flarum.reg.get('core', 'common/helpers/humanTime')\"","webpack://@ramon/backup/./src/admin/components/BackupList.tsx","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/arrayLikeToArray.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/slicedToArray.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/arrayWithHoles.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/iterableToArrayLimit.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/unsupportedIterableToArray.js","webpack://@ramon/backup/./node_modules/@babel/runtime/helpers/esm/nonIterableRest.js","webpack://@ramon/backup/./src/admin/components/ExportModal.tsx","webpack://@ramon/backup/./src/admin/components/ImportModal.tsx","webpack://@ramon/backup/./src/admin/components/ConfirmModal.tsx","webpack://@ramon/backup/./src/admin/utils/errorBoundary.tsx","webpack://@ramon/backup/./src/admin/components/BackupPanel.tsx","webpack://@ramon/backup/./src/admin/index.tsx"],"sourcesContent":["// The require scope\nconst __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tconst getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter/value functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tif(Array.isArray(definition)) {\n\t\tvar i = 0;\n\t\twhile(i < definition.length) {\n\t\t\tvar key = definition[i++];\n\t\t\tvar binding = definition[i++];\n\t\t\tif(!__webpack_require__.o(exports, key)) {\n\t\t\t\tif(binding === 0) {\n\t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, value: definition[i++] });\n\t\t\t\t} else {\n\t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: binding });\n\t\t\t\t}\n\t\t\t} else if(binding === 0) { i++; }\n\t\t}\n\t} else {\n\t\tfor(var key in definition) {\n\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t\t}\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/app');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extend');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/components/ExtensionPage');","function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, _typeof(o);\n}\nexport { _typeof as default };","import toPropertyKey from \"./toPropertyKey.js\";\nfunction _defineProperty(e, r, t) {\n return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {\n value: t,\n enumerable: !0,\n configurable: !0,\n writable: !0\n }) : e[r] = t, e;\n}\nexport { _defineProperty as default };","import _typeof from \"./typeof.js\";\nimport toPrimitive from \"./toPrimitive.js\";\nfunction toPropertyKey(t) {\n var i = toPrimitive(t, \"string\");\n return \"symbol\" == _typeof(i) ? i : i + \"\";\n}\nexport { toPropertyKey as default };","import _typeof from \"./typeof.js\";\nfunction toPrimitive(t, r) {\n if (\"object\" != _typeof(t) || !t) return t;\n var e = t[Symbol.toPrimitive];\n if (void 0 !== e) {\n var i = e.call(t, r || \"default\");\n if (\"object\" != _typeof(i)) return i;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (\"string\" === r ? String : Number)(t);\n}\nexport { toPrimitive as default };","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/Component');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Button');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/LoadingIndicator');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Modal');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/utils/extractText');","import app from \"flarum/admin/app\";\n/** Trim trailing slashes off the configured API URL. */\nexport function apiUrl() {\n return (app.forum.attribute(\"apiUrl\") || \"/api\").replace(/\\/+$/, \"\");\n}\nexport function fmtBytes(bytes) {\n if (!Number.isFinite(bytes) || bytes <= 0) return \"0 B\";\n const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n let i = 0;\n let n = bytes;\n while (n >= 1024 && i < units.length - 1) {\n n /= 1024;\n i++;\n }\n return n.toFixed(n >= 100 || i === 0 ? 0 : 1) + \" \" + units[i];\n}\n\n/** Best-effort extraction of a human-readable detail from a RequestError or thrown object. */\nexport function errorDetail(raw, fallback) {\n var _ref, _raw$response$errors$, _raw$response;\n const detail = (_ref = (_raw$response$errors$ = raw == null || (_raw$response = raw.response) == null || (_raw$response = _raw$response.errors) == null || (_raw$response = _raw$response[0]) == null ? void 0 : _raw$response.detail) != null ? _raw$response$errors$ : raw == null ? void 0 : raw.detail) != null ? _ref : typeof (raw == null ? void 0 : raw.message) === \"string\" ? raw.message : undefined;\n if (detail) return String(detail);\n if (fallback) return fallback;\n return String(app.translator.trans(\"ramon-backup.admin.errors.generic\"));\n}\n/**\n * Wrapper around `app.request` that:\n * - logs every failure to console.error with action + URL;\n * - extracts the JSON:API error detail when present;\n * - shows a Flarum alert (unless `surface: false`);\n * - rethrows so the caller can still branch on the failure.\n *\n * Centralised so we don't reinvent error extraction at every call-site.\n */\nexport async function apiRequest(opts) {\n try {\n return await app.request(opts);\n } catch (raw) {\n const detail = errorDetail(raw, opts.fallbackMessage);\n console.error(\"[backup] api error\", opts.method, opts.url, raw);\n if (opts.surface !== false) {\n app.alerts.show({\n type: \"error\"\n }, detail);\n }\n if (raw && typeof raw === \"object\" && !raw.detail) {\n try {\n raw.detail = detail;\n } catch (_unused) {\n /* read-only, ignore */\n }\n }\n throw raw;\n }\n}\nflarum.reg.add('ramon-backup', 'admin/utils/api', { apiUrl: apiUrl,fmtBytes: fmtBytes,errorDetail: errorDetail, });","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from \"flarum/admin/app\";\nimport Component from \"flarum/common/Component\";\nimport Button from \"flarum/common/components/Button\";\nimport Modal from \"flarum/common/components/Modal\";\nimport LoadingIndicator from \"flarum/common/components/LoadingIndicator\";\nimport extractText from \"flarum/common/utils/extractText\";\nimport { apiRequest, apiUrl, errorDetail } from \"../utils/api\";\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.encryption.\".concat(key), params != null ? params : {});\nclass KeypairRevealModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"copied\", false);\n }\n className() {\n return \"BackupRevealModal Modal--medium\";\n }\n title() {\n return trans(\"reveal_modal.title\");\n }\n content() {\n const _this$attrs = this.attrs,\n privateKey = _this$attrs.privateKey,\n configKey = _this$attrs.configKey;\n const snippet = \"'\".concat(configKey, \"' => '\").concat(privateKey, \"',\");\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"p\", null, trans(\"reveal_modal.intro\")), m(\"div\", {\n className: \"Alert Alert--error\"\n }, m(\"strong\", null, trans(\"reveal_modal.warning_title\")), m(\"p\", null, trans(\"reveal_modal.warning_body\"))), m(\"label\", {\n className: \"BackupReveal-label\"\n }, trans(\"reveal_modal.snippet_label\")), m(\"pre\", {\n className: \"BackupReveal-snippet\"\n }, m(\"code\", null, snippet)), m(\"div\", {\n className: \"Form-group BackupReveal-actions\"\n }, m(Button, {\n className: \"Button\",\n icon: \"fas fa-copy\",\n onclick: () => this.copy(snippet)\n }, this.copied ? trans(\"reveal_modal.copied\") : trans(\"reveal_modal.copy_button\")), m(Button, {\n className: \"Button Button--primary\",\n onclick: () => this.hide()\n }, trans(\"reveal_modal.close\"))));\n }\n copy(snippet) {\n if (!navigator.clipboard) {\n app.alerts.show({\n type: \"error\"\n }, trans(\"clipboard_unavailable\"));\n return;\n }\n navigator.clipboard.writeText(snippet).then(() => {\n this.copied = true;\n m.redraw();\n setTimeout(() => {\n this.copied = false;\n m.redraw();\n }, 2000);\n }).catch(err => {\n console.error(\"[backup] clipboard writeText failed\", err);\n app.alerts.show({\n type: \"error\"\n }, trans(\"clipboard_failed\"));\n });\n }\n}\nclass RegenerateConfirmModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"acknowledged\", false);\n _defineProperty(this, \"submitting\", false);\n }\n className() {\n return \"BackupRegenerateModal Modal--medium\";\n }\n title() {\n return trans(\"regenerate_modal.title\");\n }\n content() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"div\", {\n className: \"Alert Alert--error\"\n }, m(\"p\", null, trans(\"regenerate_modal.warning\"))), m(\"label\", {\n className: \"BackupRegenerate-confirm\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: this.acknowledged,\n onchange: e => {\n this.acknowledged = e.target.checked;\n }\n }), \" \", trans(\"regenerate_modal.acknowledge\")), m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary\",\n loading: this.submitting,\n disabled: !this.acknowledged || this.submitting,\n onclick: () => this.submit()\n }, trans(\"regenerate_modal.submit\"))));\n }\n async submit() {\n this.submitting = true;\n m.redraw();\n try {\n await this.attrs.onConfirm();\n this.hide();\n } catch (_unused) {\n // Parent already showed an error toast — keep the modal open so\n // the user can retry without re-acknowledging the warning.\n this.submitting = false;\n m.redraw();\n }\n }\n}\nexport default class EncryptionCard extends Component {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"status\", null);\n _defineProperty(this, \"loadState\", \"loading\");\n _defineProperty(this, \"loadError\", null);\n _defineProperty(this, \"publicCopied\", false);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.refresh();\n }\n view() {\n return m(\"section\", {\n className: \"BackupEncryptionCard\"\n }, m(\"header\", null, m(\"h3\", null, trans(\"section_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"section_help\"))), this.loadState === \"loading\" && m(LoadingIndicator, null), this.loadState === \"error\" && m(\"div\", {\n className: \"Alert Alert--error BackupEncryption-loadError\"\n }, m(\"p\", null, trans(\"status.load_failed\")), this.loadError && m(\"p\", {\n className: \"helpText\"\n }, m(\"code\", null, this.loadError)), m(Button, {\n className: \"Button\",\n icon: \"fas fa-rotate\",\n onclick: () => this.refresh()\n }, trans(\"status.retry\"))), this.loadState === \"ok\" && this.body());\n }\n body() {\n if (!this.status) return m(\"p\", {\n className: \"helpText\"\n }, trans(\"status.unknown\"));\n const s = this.status;\n if (!s.available) {\n return m(\"div\", {\n className: \"Alert Alert--error\"\n }, trans(\"status.libsodium_missing\"));\n }\n return m('[', null, m(\"div\", {\n className: \"BackupEncryption-statusRow\"\n }, this.statusBadge(\"public\", s.has_public_key), this.statusBadge(\"private\", s.private_key_present)), s.healthy && m(\"div\", {\n className: \"Alert Alert--success\"\n }, trans(\"status.healthy\")), !s.has_public_key && !s.private_key_present && m(\"div\", null, m(\"p\", {\n className: \"helpText\"\n }, trans(\"status.not_setup\")), m(Button, {\n className: \"Button Button--primary\",\n icon: \"fas fa-key\",\n onclick: () => this.generate(false)\n }, trans(\"actions.generate\"))), s.has_public_key && s.private_key_present && s.keys_match === false && m(\"div\", {\n className: \"Alert Alert--error\"\n }, m(\"strong\", null, trans(\"status.mismatch_title\")), m(\"p\", null, trans(\"status.mismatch_body\")), m(\"p\", null, m(\"code\", null, \"'\", s.config_key, \"'\"))), s.has_public_key && !s.private_key_present && m(\"div\", {\n className: \"Alert Alert--error\"\n }, m(\"strong\", null, trans(\"status.private_missing_title\")), m(\"p\", null, trans(\"status.private_missing_body\")), m(\"p\", null, m(\"code\", null, \"'\", s.config_key, \"'\"))), s.has_public_key && this.publicKeyPanel(s.public_key || \"\", s.healthy));\n }\n publicKeyPanel(publicKey, healthy) {\n return m(\"div\", {\n className: \"BackupEncryption-publicKey\"\n }, m(\"label\", null, trans(\"public_key.label\")), m(\"div\", {\n className: \"BackupEncryption-publicKeyRow\"\n }, m(\"pre\", null, m(\"code\", null, publicKey)), m(Button, {\n className: \"Button Button--icon\",\n icon: \"fas fa-copy\",\n title: extractText(trans(\"public_key.copy_title\")),\n onclick: () => this.copyPublic(publicKey)\n }, this.publicCopied ? extractText(trans(\"public_key.copied\")) : \"\")), m(\"p\", {\n className: \"helpText\"\n }, healthy ? trans(\"public_key.help_healthy\") : trans(\"public_key.help_broken\")), m(Button, {\n className: \"Button Button--danger\",\n icon: \"fas fa-rotate\",\n onclick: () => this.openRegenerate()\n }, trans(\"public_key.remove_button\")));\n }\n statusBadge(kind, present) {\n return m(\"div\", {\n className: \"BackupEncryption-badge BackupEncryption-badge--\".concat(present ? \"ok\" : \"missing\")\n }, m(\"i\", {\n className: \"icon fas fa-\".concat(present ? \"check\" : \"times\")\n }), m(\"span\", null, trans(\"status.\".concat(kind, \"_key_label\"))), m(\"span\", {\n className: \"BackupEncryption-badgeState\"\n }, trans(\"status.\".concat(present ? \"present\" : \"absent\"))));\n }\n copyPublic(publicKey) {\n if (!publicKey) return;\n if (!navigator.clipboard) {\n app.alerts.show({\n type: \"error\"\n }, trans(\"clipboard_unavailable\"));\n return;\n }\n navigator.clipboard.writeText(publicKey).then(() => {\n this.publicCopied = true;\n m.redraw();\n setTimeout(() => {\n this.publicCopied = false;\n m.redraw();\n }, 2000);\n }).catch(err => {\n console.error(\"[backup] clipboard writeText failed\", err);\n app.alerts.show({\n type: \"error\"\n }, trans(\"clipboard_failed\"));\n });\n }\n refresh() {\n this.loadState = \"loading\";\n this.loadError = null;\n return apiRequest({\n method: \"GET\",\n url: \"\".concat(apiUrl(), \"/backup/encryption/status\"),\n surface: false\n }).then(res => {\n this.status = res;\n this.loadState = \"ok\";\n }).catch(e => {\n this.status = null;\n this.loadState = \"error\";\n this.loadError = errorDetail(e);\n }).then(() => {\n m.redraw();\n });\n }\n async generate(acknowledgeLoss) {\n try {\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/encryption/generate-keypair\"),\n body: {\n acknowledge_loss: acknowledgeLoss\n },\n surface: false\n });\n await this.refresh();\n app.modal.show(KeypairRevealModal, {\n privateKey: res.private_key,\n configKey: res.config_key\n });\n } catch (e) {\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"actions.generate_failed\"))));\n throw e;\n }\n }\n openRegenerate() {\n app.modal.show(RegenerateConfirmModal, {\n onConfirm: () => this.generate(true)\n });\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/EncryptionCard', EncryptionCard);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/helpers/humanTime');","import app from \"flarum/admin/app\";\nimport Component from \"flarum/common/Component\";\nimport Button from \"flarum/common/components/Button\";\nimport humanTime from \"flarum/common/helpers/humanTime\";\nimport { apiUrl, fmtBytes } from \"../utils/api\";\nconst DIALECT_LABEL = {\n mysql: \"MySQL\",\n mariadb: \"MariaDB\",\n postgres: \"PostgreSQL\",\n sqlite: \"SQLite\"\n};\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.list.\".concat(key), params != null ? params : {});\nexport default class BackupList extends Component {\n view(vnode) {\n const _vnode$attrs = vnode.attrs,\n backups = _vnode$attrs.backups,\n onDelete = _vnode$attrs.onDelete;\n if (!backups.length) {\n return m(\"p\", {\n className: \"BackupList-empty helpText\"\n }, trans(\"empty\"));\n }\n return m(\"table\", {\n className: \"BackupList Table\"\n }, m(\"thead\", null, m(\"tr\", null, m(\"th\", null, trans(\"col_when\")), m(\"th\", null, trans(\"col_size\")), m(\"th\", null, trans(\"col_contents\")), m(\"th\", null, trans(\"col_status\")), m(\"th\", null))), m(\"tbody\", null, backups.map(b => m(\"tr\", {\n key: b.id,\n className: \"BackupList-row\"\n }, m(\"td\", null, m(\"div\", {\n className: \"BackupList-when\"\n }, b.created_at ? humanTime(new Date(b.created_at)) : \"—\"), m(\"div\", {\n className: \"BackupList-filename\"\n }, b.filename), b.target_dialect &&\n // Only shown when the admin retargeted the dump at\n // export time — same-engine backups have a NULL\n // target_dialect and don't need the visual noise.\n m(\"div\", {\n className: \"BackupList-target BackupList-target--\".concat(b.target_dialect),\n title: String(trans(\"target_tooltip\", {\n engine: DIALECT_LABEL[b.target_dialect] || b.target_dialect\n }))\n }, m(\"i\", {\n className: \"icon fas fa-arrow-right-arrow-left\"\n }), \" \", trans(\"target_for\", {\n engine: DIALECT_LABEL[b.target_dialect] || b.target_dialect\n }))), m(\"td\", null, fmtBytes(b.size_bytes)), m(\"td\", null, b.contents.map(c => m(\"span\", {\n className: \"BackupList-tag BackupList-tag--\".concat(c)\n }, trans(\"content_\" + c)))), m(\"td\", null, b.encrypted ? m(\"span\", {\n className: \"BackupList-encryption BackupList-encryption--on\"\n }, m(\"i\", {\n className: \"icon fas fa-lock\"\n }), \" \", trans(\"encrypted\")) : m(\"span\", {\n className: \"BackupList-encryption BackupList-encryption--off\"\n }, m(\"i\", {\n className: \"icon fas fa-lock-open\"\n }), \" \", trans(\"plain\"))), m(\"td\", {\n className: \"BackupList-actions\"\n }, m(\"a\", {\n className: \"Button Button--icon\",\n href: \"\".concat(apiUrl(), \"/backup/backups/\").concat(b.id, \"/download\"),\n target: \"_blank\",\n title: String(trans(\"download_title\"))\n }, m(\"i\", {\n className: \"icon fas fa-download\"\n })), m(Button, {\n className: \"Button Button--icon Button--danger\",\n icon: \"fas fa-trash\",\n title: trans(\"delete_title\"),\n onclick: () => onDelete(b.id)\n }))))));\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/BackupList', BackupList);","function _arrayLikeToArray(r, a) {\n (null == a || a > r.length) && (a = r.length);\n for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];\n return n;\n}\nexport { _arrayLikeToArray as default };","import arrayWithHoles from \"./arrayWithHoles.js\";\nimport iterableToArrayLimit from \"./iterableToArrayLimit.js\";\nimport unsupportedIterableToArray from \"./unsupportedIterableToArray.js\";\nimport nonIterableRest from \"./nonIterableRest.js\";\nfunction _slicedToArray(r, e) {\n return arrayWithHoles(r) || iterableToArrayLimit(r, e) || unsupportedIterableToArray(r, e) || nonIterableRest();\n}\nexport { _slicedToArray as default };","function _arrayWithHoles(r) {\n if (Array.isArray(r)) return r;\n}\nexport { _arrayWithHoles as default };","function _iterableToArrayLimit(r, l) {\n var t = null == r ? null : \"undefined\" != typeof Symbol && r[Symbol.iterator] || r[\"@@iterator\"];\n if (null != t) {\n var e,\n n,\n i,\n u,\n a = [],\n f = !0,\n o = !1;\n try {\n if (i = (t = t.call(r)).next, 0 === l) {\n if (Object(t) !== t) return;\n f = !1;\n } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);\n } catch (r) {\n o = !0, n = r;\n } finally {\n try {\n if (!f && null != t[\"return\"] && (u = t[\"return\"](), Object(u) !== u)) return;\n } finally {\n if (o) throw n;\n }\n }\n return a;\n }\n}\nexport { _iterableToArrayLimit as default };","import arrayLikeToArray from \"./arrayLikeToArray.js\";\nfunction _unsupportedIterableToArray(r, a) {\n if (r) {\n if (\"string\" == typeof r) return arrayLikeToArray(r, a);\n var t = {}.toString.call(r).slice(8, -1);\n return \"Object\" === t && r.constructor && (t = r.constructor.name), \"Map\" === t || \"Set\" === t ? Array.from(r) : \"Arguments\" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? arrayLikeToArray(r, a) : void 0;\n }\n}\nexport { _unsupportedIterableToArray as default };","function _nonIterableRest() {\n throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\");\n}\nexport { _nonIterableRest as default };","import _slicedToArray from \"@babel/runtime/helpers/esm/slicedToArray\";\nimport _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nfunction ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }\nfunction _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }\nimport app from \"flarum/admin/app\";\nimport Modal from \"flarum/common/components/Modal\";\nimport Button from \"flarum/common/components/Button\";\nimport LoadingIndicator from \"flarum/common/components/LoadingIndicator\";\nimport { apiRequest, apiUrl, errorDetail, fmtBytes } from \"../utils/api\";\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.export_modal.\".concat(key), params != null ? params : {});\n\n/**\n * Two-stage modal:\n *\n * 1. Form — admin picks what to include and whether to encrypt.\n * 2. Progress — chunked-tick polling drives a progress bar until the\n * server reports phase=done (or phase=error).\n *\n * The \"encryption to a foreign key\" path lets the operator paste a\n * public key from another Flarum install — useful when preparing an\n * archive for transfer to a different server whose keypair is not the\n * one in *this* config.php.\n */\nexport default class ExportModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"stage\", \"form\");\n _defineProperty(this, \"includeDb\", true);\n _defineProperty(this, \"includeAssets\", true);\n _defineProperty(this, \"includeStorage\", false);\n _defineProperty(this, \"includeExtensions\", false);\n // Per-extension selection. Loaded lazily when the user ticks\n // \"Extensions\" for the first time so we don't fire an extra request\n // for admins who never use the feature.\n _defineProperty(this, \"extensionsLoading\", false);\n _defineProperty(this, \"extensionsLoaded\", false);\n _defineProperty(this, \"extensions\", []);\n _defineProperty(this, \"extensionSelected\", {});\n _defineProperty(this, \"encryptionEnabled\", false);\n _defineProperty(this, \"encryptionUseExternal\", false);\n _defineProperty(this, \"externalPublicKey\", \"\");\n // Target engine the dump should be generated for. Empty string =\n // \"same as source\" (the most common case — backing up to restore\n // onto the same install / a clone of it). The non-empty values\n // make this a cross-engine migration: e.g. dump from MySQL,\n // restore onto Postgres.\n _defineProperty(this, \"targetDialect\", \"\");\n _defineProperty(this, \"starting\", false);\n _defineProperty(this, \"jobId\", null);\n _defineProperty(this, \"status\", null);\n _defineProperty(this, \"polling\", false);\n }\n className() {\n return \"BackupExportModal Modal--medium\";\n }\n title() {\n return trans(\"title\");\n }\n content() {\n if (this.stage === \"form\") return this.formContent();\n return this.progressContent();\n }\n\n // ────────────────────────────────────────── form\n\n formContent() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"p\", {\n className: \"helpText\"\n }, trans(\"intro\")), m(\"fieldset\", {\n className: \"BackupExport-fieldset\"\n }, m(\"legend\", null, trans(\"contents_title\")), this.checkbox(\"db\", () => this.includeDb, v => this.includeDb = v), this.checkbox(\"assets\", () => this.includeAssets, v => this.includeAssets = v), this.checkbox(\"storage\", () => this.includeStorage, v => this.includeStorage = v), this.checkbox(\"extensions\", () => this.includeExtensions, v => {\n this.includeExtensions = v;\n // Lazy-load the extension inventory the first time\n // someone ticks the box. The list comes back fast (no\n // disk walking — just metadata from the ExtensionManager).\n if (v && !this.extensionsLoaded) this.loadExtensions();\n }), this.includeExtensions && this.extensionList()), this.includeDb && m(\"fieldset\", {\n className: \"BackupExport-fieldset\"\n }, m(\"legend\", null, trans(\"target_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"target_help\")), m(\"select\", {\n className: \"FormControl BackupExport-targetSelect\",\n value: this.targetDialect,\n onchange: e => {\n this.targetDialect = e.target.value;\n }\n }, m(\"option\", {\n value: \"\"\n }, trans(\"target_same\")), m(\"option\", {\n value: \"mysql\"\n }, trans(\"target_mysql\")), m(\"option\", {\n value: \"mariadb\"\n }, trans(\"target_mariadb\")), m(\"option\", {\n value: \"postgres\"\n }, trans(\"target_postgres\")), m(\"option\", {\n value: \"sqlite\"\n }, trans(\"target_sqlite\")))), m(\"fieldset\", {\n className: \"BackupExport-fieldset\"\n }, m(\"legend\", null, trans(\"encryption_title\")), m(\"label\", {\n className: \"BackupExport-checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: this.encryptionEnabled,\n onchange: e => {\n this.encryptionEnabled = e.target.checked;\n }\n }), \" \", m(\"span\", null, trans(\"encryption_enable\"))), m(\"p\", {\n className: \"helpText\"\n }, trans(\"encryption_help\")), this.encryptionEnabled && m('[', null, m(\"label\", {\n className: \"BackupExport-checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: this.encryptionUseExternal,\n onchange: e => {\n this.encryptionUseExternal = e.target.checked;\n }\n }), \" \", m(\"span\", null, trans(\"encryption_external\"))), this.encryptionUseExternal && m('[', null, m(\"p\", {\n className: \"helpText\"\n }, trans(\"encryption_external_help\")), m(\"textarea\", {\n className: \"FormControl BackupExport-keyInput\",\n rows: 3,\n placeholder: \"base64 public key\",\n value: this.externalPublicKey,\n oninput: e => {\n this.externalPublicKey = e.target.value;\n }\n })))), m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary\",\n loading: this.starting,\n disabled: this.starting || !this.canStart(),\n onclick: () => this.start()\n }, trans(\"start_button\"))));\n }\n extensionList() {\n if (this.extensionsLoading) {\n return m(\"div\", {\n className: \"BackupExport-extLoading\"\n }, m(LoadingIndicator, null));\n }\n if (!this.extensions.length) {\n return m(\"p\", {\n className: \"helpText BackupExport-extEmpty\"\n }, trans(\"extensions_none\"));\n }\n const groups = {\n workbench: [],\n vendor: [],\n unknown: []\n };\n for (const ext of this.extensions) {\n var _groups$ext$location;\n (_groups$ext$location = groups[ext.location]) == null || _groups$ext$location.push(ext);\n }\n return m(\"div\", {\n className: \"BackupExport-extList\"\n }, m(\"div\", {\n className: \"BackupExport-extActions\"\n }, m(\"button\", {\n type: \"button\",\n className: \"BackupExport-extLink\",\n onclick: () => this.toggleAllExtensions(true)\n }, trans(\"extensions_select_all\")), m(\"span\", null, \" \\xB7 \"), m(\"button\", {\n type: \"button\",\n className: \"BackupExport-extLink\",\n onclick: () => this.toggleAllExtensions(false)\n }, trans(\"extensions_select_none\"))), [\"workbench\", \"vendor\", \"unknown\"].filter(loc => groups[loc].length > 0).map(loc => m(\"div\", {\n className: \"BackupExport-extGroup\",\n key: loc\n }, m(\"div\", {\n className: \"BackupExport-extGroupHeader\"\n }, trans(\"extensions_group_\" + loc), \" \", m(\"span\", {\n className: \"helpText\"\n }, \"(\", groups[loc].length, \")\")), groups[loc].map(ext => m(\"label\", {\n className: \"BackupExport-extRow\",\n key: ext.id\n }, m(\"input\", {\n type: \"checkbox\",\n checked: !!this.extensionSelected[ext.id],\n onchange: e => {\n this.extensionSelected[ext.id] = e.target.checked;\n }\n }), \" \", m(\"span\", {\n className: \"BackupExport-extTitle\"\n }, ext.title), \" \", m(\"code\", {\n className: \"BackupExport-extName\"\n }, ext.name || ext.id), m(\"span\", {\n className: \"BackupExport-extTag BackupExport-extTag--\".concat(ext.location)\n }, trans(\"extensions_tag_\" + ext.location)))))));\n }\n toggleAllExtensions(value) {\n for (const ext of this.extensions) this.extensionSelected[ext.id] = value;\n }\n async loadExtensions() {\n this.extensionsLoading = true;\n try {\n const res = await apiRequest({\n method: \"GET\",\n url: \"\".concat(apiUrl(), \"/backup/extensions\"),\n surface: false\n });\n this.extensions = res.extensions || [];\n this.extensionsLoaded = true;\n // Default: every extension ticked. The admin un-ticks the\n // ones they don't want.\n for (const ext of this.extensions) this.extensionSelected[ext.id] = true;\n } catch (e) {\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"extensions_load_failed\"))));\n } finally {\n this.extensionsLoading = false;\n m.redraw();\n }\n }\n checkbox(key, get, set) {\n return m(\"label\", {\n className: \"BackupExport-checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: get(),\n onchange: e => {\n set(e.target.checked);\n }\n }), \" \", m(\"span\", {\n className: \"BackupExport-checkbox-label\"\n }, trans(\"content_\" + key)), m(\"span\", {\n className: \"BackupExport-checkbox-help helpText\"\n }, trans(\"content_\" + key + \"_help\")));\n }\n canStart() {\n if (!this.includeDb && !this.includeAssets && !this.includeStorage && !this.includeExtensions) {\n return false;\n }\n if (this.encryptionEnabled && this.encryptionUseExternal && !this.externalPublicKey.trim()) {\n return false;\n }\n return true;\n }\n\n // ────────────────────────────────────────── progress\n\n progressContent() {\n var _s$progress, _s$warnings$length, _s$warnings;\n const s = this.status;\n if (!s) return m(LoadingIndicator, null);\n const isDone = s.phase === \"done\";\n const isError = s.phase === \"error\";\n const pct = Math.max(0, Math.min(100, ((_s$progress = s.progress) == null ? void 0 : _s$progress.percent) || 0));\n return m(\"div\", {\n className: \"Modal-body BackupExport-progress\"\n }, m(\"div\", {\n className: \"BackupExport-status BackupExport-status--\".concat(s.phase)\n }, m(\"strong\", null, trans(\"phase_\" + s.phase)), m(\"p\", null, s.message)), !isError && m('[', null, m(\"div\", {\n className: \"BackupExport-bar\"\n }, m(\"div\", {\n className: \"BackupExport-bar-fill\",\n style: {\n width: \"\".concat(isDone ? 100 : pct, \"%\")\n },\n role: \"progressbar\",\n \"aria-valuenow\": pct,\n \"aria-valuemin\": 0,\n \"aria-valuemax\": 100\n })), m(\"div\", {\n className: \"BackupExport-stats\"\n }, m(\"span\", null, fmtBytes(s.progress.processed_bytes), \" /\", \" \", fmtBytes(s.progress.total_bytes || s.progress.processed_bytes)), s.progress.total_files > 0 && m(\"span\", null, trans(\"files_count\", {\n done: s.progress.processed_files,\n total: s.progress.total_files\n })))), ((_s$warnings$length = (_s$warnings = s.warnings) == null ? void 0 : _s$warnings.length) != null ? _s$warnings$length : 0) > 0 && m(\"div\", {\n className: \"BackupExport-warnings\",\n role: \"alert\"\n }, m(\"div\", {\n className: \"BackupExport-warnings-title\"\n }, m(\"i\", {\n className: \"icon fas fa-triangle-exclamation\"\n }), \" \", trans(\"warnings_title\", {\n count: s.warnings.length\n })), m(\"p\", {\n className: \"helpText\"\n }, trans(\"warnings_help\")), m(\"ul\", {\n className: \"BackupExport-warnings-list\"\n }, s.warnings.map((w, idx) => m(\"li\", {\n key: idx\n }, w)))), m(\"div\", {\n className: \"Form-group BackupExport-progress-actions\"\n }, !isDone && !isError && m(Button, {\n className: \"Button\",\n onclick: () => this.cancel()\n }, trans(\"cancel_button\")), (isDone || isError) && m(Button, {\n className: \"Button Button--primary\",\n onclick: () => this.close()\n }, trans(\"close_button\"))));\n }\n\n // ────────────────────────────────────────── api calls\n\n async start() {\n this.starting = true;\n try {\n // The backend accepts `extensions` as bool OR string[]. When\n // every box is checked we still send the array (explicit),\n // unless the inventory hasn't even loaded yet — which means\n // the admin ticked the section but never opened the list and\n // implicitly wants \"all\".\n let extensionsField = false;\n if (this.includeExtensions) {\n if (!this.extensionsLoaded) {\n extensionsField = true;\n } else {\n const ids = Object.entries(this.extensionSelected).filter(_ref => {\n let _ref2 = _slicedToArray(_ref, 2),\n v = _ref2[1];\n return v;\n }).map(_ref3 => {\n let _ref4 = _slicedToArray(_ref3, 1),\n k = _ref4[0];\n return k;\n });\n extensionsField = ids;\n }\n }\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/exports\"),\n body: {\n contents: {\n db: this.includeDb,\n assets: this.includeAssets,\n storage: this.includeStorage,\n extensions: extensionsField\n },\n encryption: {\n enabled: this.encryptionEnabled,\n public_key: this.encryptionUseExternal ? this.externalPublicKey.trim() : null\n },\n // Empty string = \"same as source\"; the backend treats null\n // and \"\" identically so this carries the user's choice\n // through unambiguously.\n target_dialect: this.targetDialect || null\n },\n surface: false\n });\n this.jobId = res.job_id;\n this.stage = \"progress\";\n this.status = {\n phase: res.phase,\n message: res.message,\n progress: {\n total_bytes: 0,\n processed_bytes: 0,\n total_files: 0,\n processed_files: 0,\n percent: 0\n }\n };\n this.starting = false;\n m.redraw();\n this.pump();\n } catch (e) {\n this.starting = false;\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"start_failed\"))));\n m.redraw();\n }\n }\n async pump() {\n if (this.polling || !this.jobId) return;\n this.polling = true;\n try {\n var _this$status;\n // Sequential ticks — each /tick call performs ~4MB of work.\n while (this.jobId && this.status && this.status.phase !== \"done\" && this.status.phase !== \"error\") {\n try {\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/exports/\").concat(this.jobId, \"/tick\"),\n surface: false\n });\n this.status = res;\n m.redraw();\n } catch (e) {\n // Convert tick failures into a synthetic error phase so the\n // existing UI shows the close button and a meaningful\n // message instead of freezing on the last %.\n const detail = errorDetail(e, String(trans(\"phase_error_network\")));\n this.status = _objectSpread(_objectSpread({}, this.status), {}, {\n phase: \"error\",\n message: detail\n });\n m.redraw();\n break;\n }\n }\n if (((_this$status = this.status) == null ? void 0 : _this$status.phase) === \"done\") {\n app.alerts.show({\n type: \"success\"\n }, trans(\"completed\"));\n this.attrs.onComplete();\n }\n } finally {\n this.polling = false;\n }\n }\n async cancel() {\n if (!this.jobId) return;\n try {\n await apiRequest({\n method: \"DELETE\",\n url: \"\".concat(apiUrl(), \"/backup/exports/\").concat(this.jobId),\n surface: false\n });\n } catch (e) {\n // The job may still be holding a server-side lock — let the user\n // know so they understand if the next export complains.\n console.warn(\"[backup] export cancel failed\", e);\n app.alerts.show({\n type: \"warning\"\n }, trans(\"cancel_failed_warn\"));\n }\n this.close();\n }\n close() {\n this.jobId = null;\n this.hide();\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/ExportModal', ExportModal);","import _slicedToArray from \"@babel/runtime/helpers/esm/slicedToArray\";\nimport _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nfunction ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }\nfunction _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }\nimport app from \"flarum/admin/app\";\nimport Modal from \"flarum/common/components/Modal\";\nimport Button from \"flarum/common/components/Button\";\nimport LoadingIndicator from \"flarum/common/components/LoadingIndicator\";\nimport { apiRequest, apiUrl, errorDetail, fmtBytes } from \"../utils/api\";\n\n/** Abort the upload XHR if no progress event fires for this long. */\nconst UPLOAD_IDLE_TIMEOUT_MS = 60000;\n\n/**\n * Fallback chunk size if /backup/imports init doesn't return one.\n * Server-recommended is 4 MB (see UploadImportController::RECOMMENDED_CHUNK_BYTES).\n */\nconst FALLBACK_CHUNK_BYTES = 4 * 1024 * 1024;\n\n/**\n * How many times to retry a single failed chunk before giving up on\n * the whole upload. Each retry uses the same offset so it overwrites\n * (idempotent). Two retries cover a transient hiccup without\n * spinning forever on a real outage.\n */\nconst CHUNK_RETRY_LIMIT = 2;\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.import_modal.\".concat(key), params != null ? params : {});\n\n/**\n * Three-stage modal:\n * 1. upload — operator picks a `.flarum` file. We POST to /imports\n * and the server validates the header without\n * decrypting.\n * 2. configure — depending on the inspect result we ask for the\n * private key (only when the archive is encrypted)\n * AND a confirm-replace checkbox in all cases. The\n * user agreed in setup that we'd ask at this point.\n * 3. progress — chunked-tick polling drives a progress bar.\n */\nexport default class ImportModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"stage\", \"upload\");\n _defineProperty(this, \"file\", null);\n _defineProperty(this, \"uploading\", false);\n _defineProperty(this, \"uploadProgress\", 0);\n _defineProperty(this, \"uploadIndeterminate\", false);\n _defineProperty(this, \"uploadError\", null);\n _defineProperty(this, \"inspect\", null);\n _defineProperty(this, \"privateKey\", \"\");\n _defineProperty(this, \"confirmReplace\", false);\n _defineProperty(this, \"starting\", false);\n // Section toggles. Default to \"everything that's actually inside the\n // archive\" — the user explicitly opts OUT of pieces they don't want\n // to overwrite. Initialised when the inspect result arrives.\n _defineProperty(this, \"sectionDb\", false);\n _defineProperty(this, \"sectionAssets\", false);\n _defineProperty(this, \"sectionStorage\", false);\n _defineProperty(this, \"sectionExtensions\", false);\n // Per-extension toggles, keyed by directory name from the manifest.\n _defineProperty(this, \"extensionsByName\", {});\n // Opt-in: overwriting the site's root extend.php replaces live config\n // of THIS server, so it never happens unless asked for.\n _defineProperty(this, \"sectionRootExtend\", false);\n // Opt-out: losing this server's SMTP / queue / integration settings to\n // the source forum's is the defect this whole layer exists to stop.\n _defineProperty(this, \"preserveSettings\", true);\n _defineProperty(this, \"status\", null);\n _defineProperty(this, \"polling\", false);\n }\n className() {\n return \"BackupImportModal Modal--medium\";\n }\n title() {\n var _this$status;\n if (this.stage === \"progress\" && ((_this$status = this.status) == null ? void 0 : _this$status.phase) === \"done\") {\n return this.sectionDb ? trans(\"logout_title\") : trans(\"done_title\");\n }\n return trans(\"title\");\n }\n content() {\n if (this.stage === \"upload\") return this.uploadContent();\n if (this.stage === \"configure\") return this.configureContent();\n return this.progressContent();\n }\n\n // ────────────────────────────────────────── upload stage\n\n uploadContent() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"div\", {\n className: \"Alert Alert--warning\"\n }, m(\"strong\", null, trans(\"warning_title\")), m(\"p\", null, trans(\"warning_body\"))), m(\"label\", {\n className: \"BackupImport-fileLabel\"\n }, m(\"input\", {\n type: \"file\",\n accept: \".flarum\",\n onchange: e => {\n var _files;\n const f = ((_files = e.target.files) == null ? void 0 : _files[0]) || null;\n this.file = f;\n }\n }), this.file ? m(\"span\", null, this.file.name, \" \", m(\"span\", {\n className: \"helpText\"\n }, \"(\", fmtBytes(this.file.size), \")\")) : m(\"span\", {\n className: \"helpText\"\n }, trans(\"choose_file\"))), this.uploadError && m(\"div\", {\n className: \"Alert Alert--error\"\n }, this.uploadError), this.uploading && m(\"div\", {\n className: \"BackupImport-uploadProgress\"\n }, m(\"div\", {\n className: \"BackupImport-bar\"\n }, m(\"div\", {\n className: \"BackupImport-bar-fill\" + (this.uploadIndeterminate ? \" BackupImport-bar-fill--indeterminate\" : \"\"),\n style: this.uploadIndeterminate ? undefined : {\n width: \"\".concat(Math.max(2, this.uploadProgress), \"%\")\n }\n })), m(\"div\", {\n className: \"BackupImport-uploadStatus helpText\"\n }, this.uploadIndeterminate ? trans(\"inspecting_archive\") : trans(\"uploading_pct\", {\n pct: this.uploadProgress\n }))), m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary\",\n loading: this.uploading,\n disabled: this.uploading || !this.file,\n onclick: () => this.upload()\n }, trans(\"upload_button\"))));\n }\n async upload() {\n if (!this.file) return;\n this.uploading = true;\n this.uploadProgress = 0;\n this.uploadIndeterminate = false;\n this.uploadError = null;\n try {\n var _res$meta$manifest;\n // Use a raw XHR so we can show upload progress — Flarum's\n // app.request (mithril's m.request) does not expose\n // `xhr.upload.onprogress`. Once 100% has been sent the server\n // still has to read the header + validate the archive, so we\n // flip to an indeterminate \"Inspecting…\" state until the\n // response comes back.\n const res = await this.uploadWithProgress(this.file, pct => {\n this.uploadProgress = pct;\n if (pct >= 100) this.uploadIndeterminate = true;\n m.redraw();\n });\n this.inspect = res;\n\n // Seed section toggles from what the archive actually contains.\n // Anything missing from the archive can't be ticked anyway, so\n // there's no value in defaulting it to true.\n const contents = res.meta.contents || [];\n this.sectionDb = contents.includes(\"db\");\n this.sectionAssets = contents.includes(\"assets\");\n this.sectionStorage = contents.includes(\"storage\");\n this.sectionExtensions = contents.includes(\"extensions\");\n\n // Normalise to {id} regardless of which manifest version the\n // archive was packed with (string[] vs ArchiveExtensionEntry[]).\n const exts = ((_res$meta$manifest = res.meta.manifest) == null ? void 0 : _res$meta$manifest.extensions) || [];\n this.extensionsByName = {};\n for (const e of exts) {\n const id = typeof e === \"string\" ? e : e.id;\n if (id) this.extensionsByName[id] = true;\n }\n this.stage = \"configure\";\n } catch (e) {\n console.error(\"[backup] archive upload failed\", e);\n this.uploadError = errorDetail(e, String(trans(\"upload_failed\")));\n } finally {\n this.uploading = false;\n m.redraw();\n }\n }\n\n /**\n * Chunked upload + inspect.\n *\n * Single multipart POSTs of multi-GB archives reliably hit server\n * caps (`upload_max_filesize`, `post_max_size`, nginx\n * `client_max_body_size`, `memory_limit` during multipart parsing)\n * and surface as 500s. Instead we do three small requests:\n *\n * 1. POST /backup/imports — init, gets job_id + chunk_size\n * 2. POST /backup/imports/{id}/chunk* — append each slice (loop)\n * 3. POST /backup/imports/{id}/inspect — finalise, return meta\n *\n * Progress is computed from `bytesSent / file.size` across all chunk\n * requests so the bar advances smoothly through the entire file\n * even though each individual request only carries a few MB.\n */\n async uploadWithProgress(file, onProgress) {\n // ─── 1. init ──────────────────────────────────────────────────\n const init = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/imports\"),\n body: {\n filename: file.name,\n size: file.size\n },\n surface: false\n });\n const jobId = init.job_id;\n const chunkSize = init.chunk_size > 0 ? init.chunk_size : FALLBACK_CHUNK_BYTES;\n\n // ─── 2. chunk loop ────────────────────────────────────────────\n let offset = 0;\n while (offset < file.size) {\n const end = Math.min(offset + chunkSize, file.size);\n const slice = file.slice(offset, end);\n let attempt = 0;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n await this.sendChunk(jobId, offset, slice);\n break;\n } catch (e) {\n attempt++;\n if (attempt > CHUNK_RETRY_LIMIT) throw e;\n // Back off briefly before retrying — gives a transient\n // hiccup a moment to clear without spamming the server.\n await new Promise(r => setTimeout(r, 750 * attempt));\n }\n }\n offset = end;\n const pct = Math.min(99, Math.round(offset / file.size * 100));\n onProgress(pct);\n }\n\n // ─── 3. inspect ───────────────────────────────────────────────\n onProgress(100);\n return apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/imports/\").concat(jobId, \"/inspect\"),\n surface: false\n });\n }\n\n /**\n * Single-chunk upload as a raw octet-stream POST. We use XHR\n * (rather than fetch) so the per-chunk idle timeout works the\n * same way it did for the old monolithic upload, and so we can\n * pull a detailed error message off any non-2xx response body.\n */\n sendChunk(jobId, offset, slice) {\n return new Promise((resolve, reject) => {\n var _session;\n const xhr = new XMLHttpRequest();\n let lastProgress = Date.now();\n const idleTimer = setInterval(() => {\n if (Date.now() - lastProgress > UPLOAD_IDLE_TIMEOUT_MS) {\n clearInterval(idleTimer);\n xhr.abort();\n }\n }, 5000);\n const stopIdleTimer = () => clearInterval(idleTimer);\n xhr.upload.addEventListener(\"progress\", () => {\n lastProgress = Date.now();\n });\n xhr.addEventListener(\"load\", () => {\n stopIdleTimer();\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n let detail;\n try {\n var _JSON$parse;\n detail = (_JSON$parse = JSON.parse(xhr.responseText)) == null || (_JSON$parse = _JSON$parse.errors) == null || (_JSON$parse = _JSON$parse[0]) == null ? void 0 : _JSON$parse.detail;\n } catch (_unused) {\n /* non-JSON body */\n }\n reject({\n detail: detail || \"\".concat(xhr.status, \" \").concat(xhr.statusText)\n });\n }\n });\n xhr.addEventListener(\"error\", () => {\n stopIdleTimer();\n reject({\n detail: String(trans(\"upload_failed\"))\n });\n });\n xhr.addEventListener(\"abort\", () => {\n stopIdleTimer();\n reject({\n detail: String(trans(\"upload_idle_timeout\"))\n });\n });\n xhr.open(\"POST\", \"\".concat(apiUrl(), \"/backup/imports/\").concat(jobId, \"/chunk\"), true);\n xhr.withCredentials = true;\n xhr.setRequestHeader(\"Content-Type\", \"application/octet-stream\");\n xhr.setRequestHeader(\"X-Chunk-Offset\", String(offset));\n const csrf = (_session = app.session) == null ? void 0 : _session.csrfToken;\n if (csrf) xhr.setRequestHeader(\"X-CSRF-Token\", csrf);\n xhr.send(slice);\n });\n }\n\n // ────────────────────────────────────────── configure stage\n\n configureContent() {\n const i = this.inspect;\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"h4\", null, trans(\"inspect_title\")), m(\"dl\", {\n className: \"BackupImport-meta\"\n }, i.meta.created_at && m('[', null, m(\"dt\", null, trans(\"meta_when\")), m(\"dd\", null, i.meta.created_at)), i.meta.flarum_version && m('[', null, m(\"dt\", null, trans(\"meta_flarum\")), m(\"dd\", null, i.meta.flarum_version)), i.meta.contents && m('[', null, m(\"dt\", null, trans(\"meta_contents\")), m(\"dd\", null, i.meta.contents.join(\", \"))), i.meta.source_url && m('[', null, m(\"dt\", null, trans(\"meta_source_url\")), m(\"dd\", null, m(\"code\", null, i.meta.source_url))), m(\"dt\", null, trans(\"meta_size\")), m(\"dd\", null, fmtBytes(i.size))), m(\"div\", {\n className: \"Alert Alert--info BackupImport-urlNote\"\n }, m(\"i\", {\n className: \"icon fas fa-info-circle\"\n }), \" \", trans(\"url_rewrite_note\")), this.selectionFieldset(i), i.is_encrypted && m(\"fieldset\", {\n className: \"BackupImport-fieldset\"\n }, m(\"legend\", null, trans(\"key_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"key_help\")), m(\"textarea\", {\n className: \"FormControl BackupImport-keyInput\",\n rows: 3,\n placeholder: \"base64 private key\",\n value: this.privateKey,\n oninput: e => {\n this.privateKey = e.target.value;\n }\n }), m(\"p\", {\n className: \"helpText BackupImport-keyHint\"\n }, trans(\"key_hint_local\"))), m(\"div\", {\n className: \"Alert Alert--error BackupImport-confirmAlert\"\n }, m(\"strong\", null, trans(\"confirm_title\")), m(\"p\", null, trans(\"confirm_body\")), m(\"label\", {\n className: \"BackupImport-confirm\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: this.confirmReplace,\n onchange: e => {\n this.confirmReplace = e.target.checked;\n }\n }), \" \", m(\"span\", null, trans(\"confirm_check\")))), m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary\",\n loading: this.starting,\n disabled: this.starting || !this.confirmReplace,\n onclick: () => this.startRestore()\n }, trans(\"start_button\"))));\n }\n selectionFieldset(i) {\n const contents = i.meta.contents || [];\n const manifest = i.meta.manifest || {};\n const hasDb = contents.includes(\"db\");\n const hasAssets = contents.includes(\"assets\");\n const hasStorage = contents.includes(\"storage\");\n const hasExtensions = contents.includes(\"extensions\");\n const rawExtList = manifest.extensions || [];\n // Normalise to a uniform shape so the renderer can stay simple,\n // regardless of which manifest version the archive used.\n const extList = rawExtList.map(e => typeof e === \"string\" ? {\n id: e,\n location: \"workbench\"\n } : e);\n return m(\"fieldset\", {\n className: \"BackupImport-fieldset\"\n }, m(\"legend\", null, trans(\"selection_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"selection_help\")), hasDb && this.sectionRow(\"db\", this.sectionDb, v => this.sectionDb = v), hasAssets && this.sectionRow(\"assets\", this.sectionAssets, v => this.sectionAssets = v, manifest.asset_count), hasStorage && this.sectionRow(\"storage\", this.sectionStorage, v => this.sectionStorage = v, manifest.storage_count), hasExtensions && m('[', null, this.sectionRow(\"extensions\", this.sectionExtensions, v => {\n this.sectionExtensions = v;\n // Cascade: turning the section off / on flips every\n // child to match. The user can then untick individuals.\n for (const ext of extList) this.extensionsByName[ext.id] = v;\n }, manifest.extension_count), this.sectionExtensions && manifest.has_composer && m(\"div\", {\n className: \"BackupImport-composerNote helpText\"\n }, m(\"i\", {\n className: \"icon fas fa-cube\"\n }), \" \", trans(\"extensions_composer_note\")), this.sectionExtensions && extList.length > 0 && m(\"div\", {\n className: \"BackupImport-extList\"\n }, extList.map(ext => m(\"label\", {\n className: \"BackupImport-extRow\",\n key: ext.id\n }, m(\"input\", {\n type: \"checkbox\",\n checked: !!this.extensionsByName[ext.id],\n onchange: e => {\n this.extensionsByName[ext.id] = e.target.checked;\n }\n }), \" \", m(\"span\", {\n className: \"BackupImport-extTitle\"\n }, ext.title || ext.id), \" \", ext.name && ext.name !== ext.id && m(\"code\", {\n className: \"BackupImport-extName\"\n }, ext.name), ext.location && m(\"span\", {\n className: \"BackupImport-extTag BackupImport-extTag--\".concat(ext.location)\n }, trans(\"extensions_tag_\" + ext.location)))))), (hasDb || manifest.has_root_extend) && m(\"div\", {\n className: \"BackupImport-serverState\"\n }, m(\"h4\", null, trans(\"server_state_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"server_state_help\")), hasDb && this.sectionRow(\"preserve_settings\", this.preserveSettings, v => {\n this.preserveSettings = v;\n }), manifest.has_root_extend && this.sectionRow(\"root_extend\", this.sectionRootExtend, v => {\n this.sectionRootExtend = v;\n }), this.sectionRootExtend && m(\"div\", {\n className: \"BackupImport-composerNote helpText\"\n }, m(\"i\", {\n className: \"icon fas fa-triangle-exclamation\"\n }), \" \", trans(\"root_extend_warning\"))));\n }\n sectionRow(key, checked, set, count) {\n return m(\"label\", {\n className: \"BackupImport-sectionRow\"\n }, m(\"input\", {\n type: \"checkbox\",\n checked: checked,\n onchange: e => set(e.target.checked)\n }), \" \", m(\"span\", {\n className: \"BackupImport-sectionLabel\"\n }, trans(\"section_\" + key)), count !== undefined && count > 0 && m(\"span\", {\n className: \"BackupImport-sectionCount helpText\"\n }, \" \", \"(\", trans(\"section_count\", {\n count\n }), \")\"));\n }\n buildSelection() {\n // The backend treats `extensions: true` as \"all\" and an array as\n // a whitelist of directory names. When every box is checked, send\n // `true` so the user's intent isn't lost if a new extension shows\n // up between inspect and apply (it shouldn't, but be defensive).\n const extEntries = Object.entries(this.extensionsByName);\n const allChecked = extEntries.length > 0 && extEntries.every(_ref => {\n let _ref2 = _slicedToArray(_ref, 2),\n v = _ref2[1];\n return v;\n });\n const extensionsField = !this.sectionExtensions ? false : allChecked ? true : extEntries.filter(_ref3 => {\n let _ref4 = _slicedToArray(_ref3, 2),\n v = _ref4[1];\n return v;\n }).map(_ref5 => {\n let _ref6 = _slicedToArray(_ref5, 1),\n k = _ref6[0];\n return k;\n });\n return {\n db: this.sectionDb,\n assets: this.sectionAssets,\n storage: this.sectionStorage,\n extensions: extensionsField,\n root_extend: this.sectionRootExtend,\n preserve_settings: this.preserveSettings\n };\n }\n async startRestore() {\n if (!this.inspect) return;\n this.starting = true;\n try {\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/imports/\").concat(this.inspect.job_id, \"/start\"),\n surface: false,\n body: {\n private_key: this.privateKey.trim() || null,\n confirm_replace: this.confirmReplace,\n selection: this.buildSelection()\n }\n });\n this.stage = \"progress\";\n this.status = {\n phase: res.phase,\n message: res.message,\n progress: {\n total_bytes: this.inspect.size,\n processed_bytes: 0,\n extracted_entries: 0,\n restored_statements: 0,\n percent: 0\n }\n };\n m.redraw();\n this.pump();\n } catch (e) {\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"start_failed\"))));\n } finally {\n this.starting = false;\n }\n }\n\n // ────────────────────────────────────────── progress stage\n\n progressContent() {\n var _s$progress;\n const s = this.status;\n if (!s) return m(LoadingIndicator, null);\n\n // Once the server reports phase=done we hand the screen over to a\n // dedicated completion view: the user has finished waiting and\n // now needs to know what to do next (which differs depending on\n // whether the DB was actually replaced).\n if (s.phase === \"done\") return this.completedContent();\n const isError = s.phase === \"error\";\n const pct = Math.max(0, Math.min(100, ((_s$progress = s.progress) == null ? void 0 : _s$progress.percent) || 0));\n return m(\"div\", {\n className: \"Modal-body BackupImport-progress\"\n }, m(\"div\", {\n className: \"BackupImport-status BackupImport-status--\".concat(s.phase)\n }, m(\"strong\", null, trans(\"phase_\" + s.phase)), m(\"p\", null, s.message)), !isError && m(\"div\", {\n className: \"BackupImport-bar\"\n }, m(\"div\", {\n className: \"BackupImport-bar-fill\",\n style: {\n width: \"\".concat(pct, \"%\")\n }\n })), m(\"div\", {\n className: \"Form-group BackupImport-progress-actions\"\n }, !isError && m(Button, {\n className: \"Button\",\n onclick: () => this.cancel()\n }, trans(\"cancel_button\")), isError && m(Button, {\n className: \"Button Button--primary\",\n onclick: () => this.close()\n }, trans(\"close_button\"))));\n }\n\n /**\n * Replaces the progress UI as soon as `phase === 'done'`. Two\n * shapes:\n *\n * - DB restored — the admin's session was just wiped together\n * with the rest of the `users` / `sessions` tables. We make\n * this very clear and offer a single primary action: reload.\n * Anything else (refreshing the list, dismissing) would race\n * against an invalidated cookie and surface a confusing 401.\n *\n * - Files only — the session is fine, just close.\n */\n /**\n * Warning block for a restore that finished but lost entries, or that\n * carries a stack advisory. Rendered on the completed screen in both\n * shapes — an incomplete restore must never present as clean success,\n * which is the failure mode this whole path exists to remove.\n */\n completedWarnings() {\n var _this$status$warnings, _this$status2;\n const warnings = (_this$status$warnings = (_this$status2 = this.status) == null ? void 0 : _this$status2.warnings) != null ? _this$status$warnings : [];\n if (!warnings.length) return null;\n return m(\"div\", {\n className: \"Alert Alert--error BackupImport-completedWarnings\"\n }, m(\"strong\", null, trans(\"incomplete_title\")), m(\"ul\", null, warnings.map(w => m(\"li\", null, w))));\n }\n completedContent() {\n var _this$status3;\n if (this.sectionDb) {\n return m(\"div\", {\n className: \"Modal-body BackupImport-completed BackupImport-completed--logout\"\n }, this.completedWarnings(), m(\"div\", {\n className: \"BackupImport-completedIcon\"\n }, m(\"i\", {\n className: \"fas fa-right-from-bracket\"\n })), m(\"h3\", {\n className: \"BackupImport-completedTitle\"\n }, trans(\"logout_title\")), m(\"p\", {\n className: \"BackupImport-completedBody\"\n }, trans(\"logout_body\")), m(\"ol\", {\n className: \"BackupImport-completedSteps\"\n }, m(\"li\", null, trans(\"logout_step_reload\")), m(\"li\", null, trans(\"logout_step_login\"))), m(Button, {\n className: \"Button Button--primary BackupImport-completedAction\",\n icon: \"fas fa-rotate\",\n onclick: () => window.location.reload()\n }, trans(\"logout_button\")));\n }\n const incomplete = !!((_this$status3 = this.status) != null && _this$status3.incomplete);\n return m(\"div\", {\n className: \"Modal-body BackupImport-completed\"\n }, this.completedWarnings(), m(\"div\", {\n className: \"BackupImport-completedIcon BackupImport-completedIcon--\".concat(incomplete ? \"warning\" : \"success\")\n }, m(\"i\", {\n className: incomplete ? \"fas fa-triangle-exclamation\" : \"fas fa-circle-check\"\n })), m(\"h3\", {\n className: \"BackupImport-completedTitle\"\n }, trans(incomplete ? \"incomplete_title\" : \"done_title\")), m(\"p\", {\n className: \"BackupImport-completedBody\"\n }, trans(incomplete ? \"incomplete_body\" : \"done_body\")), m(Button, {\n className: \"Button Button--primary\",\n onclick: () => this.close()\n }, trans(\"close_button\")));\n }\n async pump() {\n if (this.polling || !this.inspect) return;\n this.polling = true;\n try {\n var _this$status4;\n while (this.inspect && this.status && this.status.phase !== \"done\" && this.status.phase !== \"error\") {\n try {\n const res = await apiRequest({\n method: \"POST\",\n url: \"\".concat(apiUrl(), \"/backup/imports/\").concat(this.inspect.job_id, \"/tick\"),\n surface: false,\n // The server never stores the decryption key on disk; we\n // re-send it each tick so the inspect/extract phases can\n // decrypt. Harmless (ignored) for unencrypted archives and\n // once extraction is done.\n body: {\n private_key: this.privateKey.trim() || null\n }\n });\n this.status = res;\n m.redraw();\n } catch (e) {\n // Restore is more dangerous to leave hanging than export —\n // the server may still be mid-write. Surface a synthetic\n // error phase, and explicitly tell the user to verify\n // server state before retrying.\n console.error(\"[backup] import tick failed\", e);\n const detail = errorDetail(e, String(trans(\"phase_error_network\")));\n this.status = _objectSpread(_objectSpread({}, this.status), {}, {\n phase: \"error\",\n message: detail\n });\n m.redraw();\n break;\n }\n }\n if (((_this$status4 = this.status) == null ? void 0 : _this$status4.phase) === \"done\") {\n // Don't refresh the parent panel — when the backup includes\n // the database, restoring it has just replaced the sessions\n // table this admin is authenticated against. Any further API\n // call from this stale session would fail (401 / CSRF) and\n // surface as a confusing \"Oops!\" toast. The user clicks\n // Reload below and gets a clean session.\n app.alerts.show({\n type: \"success\"\n }, trans(\"completed\"));\n if (!this.sectionDb) this.attrs.onComplete();\n }\n } finally {\n this.polling = false;\n }\n }\n async cancel() {\n if (!this.inspect) return;\n try {\n await apiRequest({\n method: \"DELETE\",\n url: \"\".concat(apiUrl(), \"/backup/imports/\").concat(this.inspect.job_id),\n surface: false\n });\n } catch (e) {\n console.error(\"[backup] import cancel failed\", e);\n app.alerts.show({\n type: \"warning\"\n }, trans(\"cancel_failed_warn\"));\n }\n this.close();\n }\n close() {\n this.hide();\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/ImportModal', ImportModal);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nfunction ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }\nfunction _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }\nimport app from \"flarum/admin/app\";\nimport Modal from \"flarum/common/components/Modal\";\nimport Button from \"flarum/common/components/Button\";\n/**\n * Dropin replacement for `window.confirm()`. Native confirm breaks the\n * Flarum dark-mode chrome and is silently auto-rejected by some\n * locked-down corporate browsers — this modal stays inside the SPA.\n *\n * Usage:\n * const ok = await confirmAsync({ title, body, danger: true });\n * if (!ok) return;\n */\nexport default class ConfirmModal extends Modal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"resolved\", false);\n }\n className() {\n return \"BackupConfirmModal Modal--small\";\n }\n title() {\n return this.attrs.title;\n }\n content() {\n var _this$attrs$confirmLa, _this$attrs$cancelLab;\n const confirmLabel = (_this$attrs$confirmLa = this.attrs.confirmLabel) != null ? _this$attrs$confirmLa : app.translator.trans(\"ramon-backup.admin.errors.confirm_default\");\n const cancelLabel = (_this$attrs$cancelLab = this.attrs.cancelLabel) != null ? _this$attrs$cancelLab : app.translator.trans(\"ramon-backup.admin.errors.cancel_default\");\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(\"div\", {\n className: \"BackupConfirmModal-body\"\n }, this.attrs.body), m(\"div\", {\n className: \"Form-group BackupConfirmModal-actions\"\n }, m(Button, {\n className: \"Button \" + (this.attrs.danger ? \"Button--danger\" : \"Button--primary\"),\n onclick: () => this.decide(true)\n }, confirmLabel), m(Button, {\n className: \"Button\",\n onclick: () => this.decide(false)\n }, cancelLabel)));\n }\n decide(confirmed) {\n var _ref;\n if (this.resolved) return;\n this.resolved = true;\n (_ref = confirmed ? this.attrs.onConfirm : this.attrs.onCancel) == null || _ref();\n this.hide();\n }\n\n // Esc key / backdrop click / X button all funnel through Mithril's\n // remove lifecycle. If the user dismissed without picking a button,\n // treat that as a cancel so the awaiting Promise actually resolves.\n onbeforeremove(vnode) {\n if (!this.resolved) {\n var _this$attrs$onCancel, _this$attrs;\n this.resolved = true;\n (_this$attrs$onCancel = (_this$attrs = this.attrs).onCancel) == null || _this$attrs$onCancel.call(_this$attrs);\n }\n return super.onbeforeremove(vnode);\n }\n}\n\n/** Promise wrapper so callers can `await confirmAsync(...)`. */\nexport function confirmAsync(attrs) {\n return new Promise(resolve => {\n let settled = false;\n const settle = v => {\n if (settled) return;\n settled = true;\n resolve(v);\n };\n app.modal.show(ConfirmModal, _objectSpread(_objectSpread({}, attrs), {}, {\n onConfirm: () => settle(true),\n onCancel: () => settle(false)\n }));\n });\n}\nflarum.reg.add('ramon-backup', 'admin/components/ConfirmModal', ConfirmModal);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from \"flarum/admin/app\";\nimport Component from \"flarum/common/Component\";\nconst trans = key => app.translator.trans(\"ramon-backup.admin.errors.\".concat(key));\n/**\n * Mithril doesn't have React's Error Boundary — but a tiny vnode\n * wrapper that try/catches `children` rendering covers the same\n * 90% case: a single throw inside a render path won't blow up the\n * whole admin SPA.\n *\n * Limitation: only catches synchronous exceptions during render. Async\n * Promise rejections still need their own try/catch — this is not a\n * substitute for handling failures in event handlers or API calls.\n */\nexport class ErrorBoundary extends Component {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"failed\", false);\n _defineProperty(this, \"lastError\", null);\n }\n view(vnode) {\n if (this.failed) {\n const retry = () => {\n this.failed = false;\n this.lastError = null;\n m.redraw();\n };\n if (vnode.attrs.fallback) return vnode.attrs.fallback(this.lastError, retry);\n return m(\"div\", {\n className: \"Alert Alert--error BackupErrorBoundary\"\n }, m(\"strong\", null, trans(\"boundary_title\")), m(\"p\", null, trans(\"boundary_body\")), m(\"button\", {\n type: \"button\",\n className: \"Button\",\n onclick: retry\n }, trans(\"boundary_retry\")));\n }\n try {\n return vnode.children;\n } catch (err) {\n var _vnode$attrs$onError, _vnode$attrs;\n this.failed = true;\n this.lastError = err;\n (_vnode$attrs$onError = (_vnode$attrs = vnode.attrs).onError) == null || _vnode$attrs$onError.call(_vnode$attrs, err);\n console.error(\"[backup] render boundary caught\", err);\n return null;\n }\n }\n}\nflarum.reg.add('ramon-backup', 'admin/utils/errorBoundary', { ErrorBoundary: ErrorBoundary, });","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from \"flarum/admin/app\";\nimport Component from \"flarum/common/Component\";\nimport Button from \"flarum/common/components/Button\";\nimport LoadingIndicator from \"flarum/common/components/LoadingIndicator\";\nimport EncryptionCard from \"./EncryptionCard\";\nimport BackupList from \"./BackupList\";\nimport ExportModal from \"./ExportModal\";\nimport ImportModal from \"./ImportModal\";\nimport { confirmAsync } from \"./ConfirmModal\";\nimport { apiRequest, apiUrl, errorDetail } from \"../utils/api\";\nimport { ErrorBoundary } from \"../utils/errorBoundary\";\nconst trans = (key, params) => app.translator.trans(\"ramon-backup.admin.\".concat(key), params != null ? params : {});\nexport default class BackupPanel extends Component {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"listState\", \"loading\");\n _defineProperty(this, \"listError\", null);\n _defineProperty(this, \"backups\", []);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.refresh();\n }\n view() {\n return m(\"div\", {\n className: \"BackupPanel\"\n }, m(ErrorBoundary, {\n onError: e => console.error(\"[backup] panel render\", e)\n }, m(\"section\", {\n className: \"BackupPanel-actions\"\n }, m(\"h3\", null, trans(\"panel.actions_title\")), m(\"p\", {\n className: \"helpText\"\n }, trans(\"panel.actions_help\")), m(\"div\", {\n className: \"BackupPanel-actionButtons\"\n }, m(Button, {\n className: \"Button Button--primary\",\n icon: \"fas fa-download\",\n onclick: () => this.openExport()\n }, trans(\"panel.create_button\")), m(Button, {\n className: \"Button\",\n icon: \"fas fa-upload\",\n onclick: () => this.openImport()\n }, trans(\"panel.import_button\")))), m(EncryptionCard, null), m(\"section\", {\n className: \"BackupPanel-list\"\n }, m(\"h3\", null, trans(\"panel.list_title\")), this.renderList())));\n }\n renderList() {\n if (this.listState === \"loading\") return m(LoadingIndicator, null);\n if (this.listState === \"error\") {\n return m(\"div\", {\n className: \"Alert Alert--error BackupPanel-listError\"\n }, m(\"p\", null, trans(\"list.load_failed\")), this.listError && m(\"p\", {\n className: \"helpText\"\n }, m(\"code\", null, this.listError)), m(Button, {\n className: \"Button\",\n icon: \"fas fa-rotate\",\n onclick: () => this.refresh()\n }, trans(\"list.retry\")));\n }\n return m(BackupList, {\n backups: this.backups,\n onDelete: id => this.delete(id),\n onRefresh: () => this.refresh()\n });\n }\n refresh() {\n this.listState = \"loading\";\n this.listError = null;\n return apiRequest({\n method: \"GET\",\n url: \"\".concat(apiUrl(), \"/backup/backups\"),\n surface: false\n }).then(res => {\n this.backups = res.backups || [];\n this.listState = \"ok\";\n }).catch(e => {\n this.backups = [];\n this.listState = \"error\";\n this.listError = errorDetail(e);\n }).then(() => {\n m.redraw();\n });\n }\n openExport() {\n app.modal.show(ExportModal, {\n onComplete: () => this.refresh()\n });\n }\n openImport() {\n app.modal.show(ImportModal, {\n onComplete: () => this.refresh()\n });\n }\n async delete(id) {\n const ok = await confirmAsync({\n title: trans(\"list.confirm_delete_title\"),\n body: trans(\"list.confirm_delete\"),\n confirmLabel: trans(\"list.delete_title\"),\n danger: true\n });\n if (!ok) return;\n try {\n await apiRequest({\n method: \"DELETE\",\n url: \"\".concat(apiUrl(), \"/backup/backups/\").concat(id),\n surface: false,\n fallbackMessage: String(trans(\"list.delete_failed\"))\n });\n app.alerts.show({\n type: \"success\"\n }, trans(\"list.deleted\"));\n this.refresh();\n } catch (e) {\n app.alerts.show({\n type: \"error\"\n }, errorDetail(e, String(trans(\"list.delete_failed\"))));\n }\n }\n}\nflarum.reg.add('ramon-backup', 'admin/components/BackupPanel', BackupPanel);","import app from \"flarum/admin/app\";\nimport { override } from \"flarum/common/extend\";\nimport ExtensionPage from \"flarum/admin/components/ExtensionPage\";\nimport BackupPanel from \"./components/BackupPanel\";\nconst EXT_ID = \"ramon-backup\";\n\n// Hide the default \"Save changes\" submit button on our settings page —\n// every action here happens through dedicated buttons (export now, key\n// rotation, etc.), there's no batch settings save to perform.\noverride(ExtensionPage.prototype, \"submitButton\", function (original) {\n if (this.extension && this.extension.id === EXT_ID) return null;\n return original();\n});\napp.initializers.add(EXT_ID, () => {\n app.registry.for(EXT_ID).registerSetting(() => m(BackupPanel, null), 100, \"ramon-backup.panel\").registerPermission({\n icon: \"fas fa-file-archive\",\n label: app.translator.trans(\"ramon-backup.admin.permissions.manage_label\"),\n permission: \"backup.manage\"\n }, \"moderate\");\n});"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","Array","isArray","i","length","key","binding","o","Object","defineProperty","enumerable","value","get","obj","prop","prototype","hasOwnProperty","call","app_namespaceObject","flarum","reg","extend_namespaceObject","ExtensionPage_namespaceObject","_typeof","Symbol","iterator","constructor","_defineProperty","e","r","t","toPrimitive","TypeError","String","toPropertyKey","configurable","writable","Component_namespaceObject","Button_namespaceObject","LoadingIndicator_namespaceObject","Modal_namespaceObject","extractText_namespaceObject","apiUrl","app_default","forum","attribute","replace","fmtBytes","bytes","Number","isFinite","units","n","toFixed","errorDetail","raw","fallback","_ref","_raw$response$errors$","_raw$response","detail","response","errors","message","undefined","translator","trans","async","apiRequest","opts","request","fallbackMessage","console","error","method","url","surface","alerts","show","type","_unused","add","params","concat","KeypairRevealModal","Modal_default","super","arguments","this","className","title","content","_this$attrs","attrs","privateKey","configKey","snippet","m","Button_default","icon","onclick","copy","copied","hide","navigator","clipboard","writeText","then","redraw","setTimeout","catch","err","RegenerateConfirmModal","checked","acknowledged","onchange","target","loading","submitting","disabled","submit","onConfirm","EncryptionCard","Component_default","oninit","vnode","refresh","view","loadState","LoadingIndicator_default","loadError","body","status","s","available","statusBadge","has_public_key","private_key_present","healthy","generate","keys_match","config_key","publicKeyPanel","public_key","publicKey","extractText_default","copyPublic","publicCopied","openRegenerate","kind","present","res","acknowledgeLoss","acknowledge_loss","modal","private_key","humanTime_namespaceObject","DIALECT_LABEL","mysql","mariadb","postgres","sqlite","BackupList_trans","BackupList","_vnode$attrs","backups","onDelete","map","b","id","created_at","humanTime_default","Date","filename","target_dialect","engine","size_bytes","contents","c","encrypted","href","_arrayLikeToArray","_slicedToArray","arrayWithHoles","l","u","f","next","done","push","iterableToArrayLimit","arrayLikeToArray","toString","slice","name","from","test","unsupportedIterableToArray","nonIterableRest","ownKeys","keys","getOwnPropertySymbols","filter","getOwnPropertyDescriptor","apply","_objectSpread","forEach","getOwnPropertyDescriptors","defineProperties","ExportModal_trans","ExportModal","stage","formContent","progressContent","checkbox","includeDb","v","includeAssets","includeStorage","includeExtensions","extensionsLoaded","loadExtensions","extensionList","targetDialect","encryptionEnabled","encryptionUseExternal","rows","placeholder","externalPublicKey","oninput","starting","canStart","start","extensionsLoading","extensions","groups","workbench","vendor","unknown","ext","_groups$ext$location","location","toggleAllExtensions","loc","extensionSelected","set","trim","_s$progress","_s$warnings$length","_s$warnings","isDone","phase","isError","pct","Math","max","min","progress","percent","style","width","role","processed_bytes","total_bytes","total_files","processed_files","total","warnings","count","w","idx","cancel","close","extensionsField","entries","_ref3","db","assets","storage","encryption","enabled","jobId","job_id","pump","polling","_this$status","onComplete","warn","ImportModal_ownKeys","ImportModal_objectSpread","ImportModal_trans","ImportModal","sectionDb","uploadContent","configureContent","accept","_files","files","file","size","uploadError","uploading","uploadIndeterminate","uploadProgress","upload","_res$meta$manifest","uploadWithProgress","inspect","meta","includes","sectionAssets","sectionStorage","sectionExtensions","exts","manifest","extensionsByName","onProgress","init","chunkSize","chunk_size","offset","end","attempt","sendChunk","Promise","round","resolve","reject","_session","xhr","XMLHttpRequest","lastProgress","now","idleTimer","setInterval","clearInterval","abort","stopIdleTimer","addEventListener","_JSON$parse","JSON","parse","responseText","statusText","open","withCredentials","setRequestHeader","csrf","session","csrfToken","send","flarum_version","join","source_url","selectionFieldset","is_encrypted","confirmReplace","startRestore","hasDb","hasAssets","hasStorage","hasExtensions","extList","sectionRow","asset_count","storage_count","extension_count","has_composer","has_root_extend","preserveSettings","sectionRootExtend","buildSelection","extEntries","allChecked","every","_ref5","root_extend","preserve_settings","confirm_replace","selection","extracted_entries","restored_statements","completedContent","completedWarnings","_this$status$warnings","_this$status2","_this$status3","window","reload","incomplete","_this$status4","ConfirmModal_ownKeys","ConfirmModal_objectSpread","ConfirmModal","_this$attrs$confirmLa","_this$attrs$cancelLab","confirmLabel","cancelLabel","danger","decide","confirmed","resolved","onCancel","onbeforeremove","_this$attrs$onCancel","errorBoundary_trans","ErrorBoundary","failed","retry","lastError","children","_vnode$attrs$onError","onError","BackupPanel_trans","BackupPanel","openExport","openImport","renderList","listState","listError","delete","onRefresh","settled","settle","EXT_ID","override","ExtensionPage_default","original","extension","initializers","registry","for","registerSetting","registerPermission","label","permission"],"sourceRoot":""} \ No newline at end of file diff --git a/js/src/admin/components/ImportModal.tsx b/js/src/admin/components/ImportModal.tsx index b1a1c13..b6a2cf9 100644 --- a/js/src/admin/components/ImportModal.tsx +++ b/js/src/admin/components/ImportModal.tsx @@ -44,6 +44,9 @@ interface ArchiveManifest { // New archives ship rich descriptors. extensions?: string[] | ArchiveExtensionEntry[]; has_composer?: boolean; + // Archives from before the project-reconcile work don't carry the + // site's root extend.php, so the toggle for it stays hidden. + has_root_extend?: boolean; } interface InspectResult { @@ -65,8 +68,10 @@ interface ImportProgress { phase: | "inspect" | "extract" + | "reconcile" | "restore" | "rewrite" + | "verify" | "finalize" | "done" | "error"; @@ -125,6 +130,13 @@ export default class ImportModal extends Modal { // Per-extension toggles, keyed by directory name from the manifest. protected extensionsByName: Record = {}; + // Opt-in: overwriting the site's root extend.php replaces live config + // of THIS server, so it never happens unless asked for. + protected sectionRootExtend = false; + // Opt-out: losing this server's SMTP / queue / integration settings to + // the source forum's is the defect this whole layer exists to stop. + protected preserveSettings = true; + protected status: ImportProgress | null = null; protected polling = false; @@ -571,12 +583,46 @@ export default class ImportModal extends Modal { )} )} + + {(hasDb || manifest.has_root_extend) && ( +
+

{trans("server_state_title")}

+

{trans("server_state_help")}

+ + {hasDb && + this.sectionRow( + "preserve_settings", + this.preserveSettings, + (v) => { + this.preserveSettings = v; + }, + )} + + {manifest.has_root_extend && + this.sectionRow("root_extend", this.sectionRootExtend, (v) => { + this.sectionRootExtend = v; + })} + + {this.sectionRootExtend && ( +
+ {" "} + {trans("root_extend_warning")} +
+ )} +
+ )} ); } sectionRow( - key: "db" | "assets" | "storage" | "extensions", + key: + | "db" + | "assets" + | "storage" + | "extensions" + | "preserve_settings" + | "root_extend", checked: boolean, set: (v: boolean) => void, count?: number, @@ -619,6 +665,8 @@ export default class ImportModal extends Modal { assets: this.sectionAssets, storage: this.sectionStorage, extensions: extensionsField, + root_extend: this.sectionRootExtend, + preserve_settings: this.preserveSettings, }; } diff --git a/less/admin.less b/less/admin.less index a04a7cd..90fcb7f 100644 --- a/less/admin.less +++ b/less/admin.less @@ -617,6 +617,25 @@ font-size: 12px; } +// Bloco das opções que decidem o que do SERVIDOR sobrevive ao restore. +// Separado por uma régua de propósito: são escolhas de natureza +// diferente das caixas "o que do backup aplicar" acima. +.BackupImport-serverState { + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid var(--control-bg, rgba(0, 0, 0, 0.08)); + + h4 { + margin: 0 0 4px; + font-size: 13px; + font-weight: 600; + } + + > .helpText { + margin-bottom: 8px; + } +} + .BackupImport-extList { margin: 4px 0 8px 28px; padding: 8px 12px; diff --git a/locale/en.yml b/locale/en.yml index cc3ffb4..845f018 100644 --- a/locale/en.yml +++ b/locale/en.yml @@ -150,7 +150,7 @@ ramon-backup: extensions_tag_workbench: "workbench" extensions_tag_vendor: "vendor" extensions_tag_unknown: "unknown" - extensions_composer_note: "composer.json and composer.lock are bundled and will overwrite the destination's project root. Run `composer install` after restoring to materialise vendor extensions." + extensions_composer_note: "composer.json is merged into this server's, never overwritten — nothing installed here is removed. composer.lock is NOT applied, because a lock from another server prunes packages that only exist here. Run `composer install` after restoring to materialise vendor extensions." inspect_title: "Archive details" meta_when: "Created" meta_flarum: "Flarum version" @@ -164,6 +164,11 @@ ramon-backup: section_storage: "Storage" section_extensions: "Extensions" section_count: "{count} files" + server_state_title: "This server's own configuration" + server_state_help: "Some state describes THIS server rather than the forum in the backup — SMTP credentials, queue driver, integration tokens, and the root extend.php. These options decide what survives the restore." + section_preserve_settings: "Keep this server's mail, queue and integration settings" + section_root_extend: "Replace the root extend.php with the backup's" + root_extend_warning: "The root extend.php is loaded before Flarum's error handler exists — a class it references that isn't installed here takes the forum down with a blank HTTP 500. The current file is copied to a .bak-* first, and the restore checks for unresolvable classes when it finishes." key_title: "Private key" key_help: "This archive is encrypted. Paste the matching base64 private key — it stays in this browser session and is sent ONCE to the server for the restore." key_hint_local: "Leave blank to use the private key configured in this server's config.php." @@ -189,8 +194,10 @@ ramon-backup: logout_button: "Reload and sign in" phase_inspect: "Inspecting archive…" phase_extract: "Extracting…" + phase_reconcile: "Reconciling project files…" phase_restore: "Restoring database…" phase_rewrite: "Rewriting URLs…" + phase_verify: "Checking the forum still boots…" phase_finalize: "Finalising…" phase_done: "Done" phase_error: "Error" diff --git a/locale/pt-BR.yml b/locale/pt-BR.yml index 5c078b2..706da3e 100644 --- a/locale/pt-BR.yml +++ b/locale/pt-BR.yml @@ -150,7 +150,7 @@ ramon-backup: extensions_tag_workbench: "workbench" extensions_tag_vendor: "vendor" extensions_tag_unknown: "unknown" - extensions_composer_note: "composer.json e composer.lock estão no arquivo e vão sobrescrever os do servidor de destino. Rode `composer install` depois de restaurar para materializar as extensões vendor." + extensions_composer_note: "O composer.json é UNIDO ao deste servidor, nunca sobrescrito — nada que está instalado aqui é removido. O composer.lock NÃO é aplicado: um lock de outro servidor poda pacotes que só existem aqui. Rode `composer install` depois de restaurar para materializar as extensões vendor." inspect_title: "Detalhes do arquivo" meta_when: "Criado em" meta_flarum: "Versão do Flarum" @@ -164,6 +164,11 @@ ramon-backup: section_storage: "Storage" section_extensions: "Extensões" section_count: "{count} arquivos" + server_state_title: "Configuração deste servidor" + server_state_help: "Parte do estado descreve ESTE servidor, não o fórum do backup — credenciais de SMTP, driver de fila, tokens de integração e o extend.php da raiz. Estas opções decidem o que sobrevive à restauração." + section_preserve_settings: "Manter e-mail, fila e integrações deste servidor" + section_root_extend: "Substituir o extend.php da raiz pelo do backup" + root_extend_warning: "O extend.php da raiz é carregado antes de existir handler de erro no Flarum — uma classe referenciada ali que não esteja instalada aqui derruba o fórum com HTTP 500 em branco. O arquivo atual é copiado para um .bak-* antes, e a restauração verifica classes não resolvidas ao terminar." key_title: "Chave privada" key_help: "Este arquivo está criptografado. Cole a chave privada base64 correspondente — ela fica somente nesta sessão do navegador e é enviada UMA VEZ ao servidor para a restauração." key_hint_local: "Deixe em branco para usar a chave privada configurada no config.php deste servidor." @@ -189,8 +194,10 @@ ramon-backup: logout_button: "Recarregar e fazer login" phase_inspect: "Inspecionando arquivo…" phase_extract: "Extraindo…" + phase_reconcile: "Reconciliando arquivos do projeto…" phase_restore: "Restaurando banco…" phase_rewrite: "Reescrevendo URLs…" + phase_verify: "Conferindo se o fórum ainda sobe…" phase_finalize: "Finalizando…" phase_done: "Pronto" phase_error: "Erro" diff --git a/src/Console/ImportCommand.php b/src/Console/ImportCommand.php index 7008d8b..44ab930 100644 --- a/src/Console/ImportCommand.php +++ b/src/Console/ImportCommand.php @@ -51,6 +51,19 @@ protected function configure(): void InputOption::VALUE_OPTIONAL, 'Restore extensions. Omit the value for ALL, or pass a comma-separated list of ids.', false + ) + ->addOption( + 'root-extend', + null, + InputOption::VALUE_NONE, + 'Replace this install\'s root extend.php with the backup\'s (off by default — it is loaded before Flarum\'s error handler).' + ) + ->addOption( + 'preserve-settings', + null, + InputOption::VALUE_NEGATABLE, + 'Keep this server\'s mail / queue / integration settings across the restore.', + true ); } @@ -113,7 +126,30 @@ protected function fire(): int .', posts parsed: '.((int) ($rewrite['posts_parsed'] ?? 0))); } - return 0; + return $this->reportOutcome($state); + } + + /** + * Despeja os avisos e escolhe o exit code. + * + * Um restore que terminou `incomplete` sai com 1 de propósito: são os + * casos em que o operador precisa agir antes de recarregar o fórum — + * entrada não gravada, ou classe que o extend.php da raiz referencia + * e o autoloader não resolve. Sair 0 aqui é o que faz um script de + * deploy seguir adiante rumo a um HTTP 500 sem mensagem. + */ + private function reportOutcome(JobState $state): int + { + foreach ((array) $state->get('warnings', []) as $warning) { + $this->output->writeln('! '.(string) $warning.''); + } + + if (! $state->get('incomplete', false)) { + return 0; + } + + $this->error('Restore finished INCOMPLETE — review the warnings above before reloading the forum.'); + return 1; } /** @@ -130,7 +166,9 @@ private function resolveSelection(): ?array || $this->input->hasParameterOption('--no-assets') || $this->input->hasParameterOption('--storage') || $this->input->hasParameterOption('--no-storage') - || $this->input->hasParameterOption('--extensions'); + || $this->input->hasParameterOption('--extensions') + || $this->input->hasParameterOption('--root-extend') + || $this->input->hasParameterOption('--no-preserve-settings'); if (! $touched) { return null; @@ -141,6 +179,12 @@ private function resolveSelection(): ?array 'assets' => (bool) $this->input->getOption('assets'), 'storage' => (bool) $this->input->getOption('storage'), 'extensions' => $this->resolveExtensionSelection(), + /* + * Assimétricos de propósito: sobrescrever o extend.php da raiz + * exige pedir, preservar a config deste servidor exige recusar. + */ + 'root_extend' => (bool) $this->input->getOption('root-extend'), + 'preserve_settings' => (bool) $this->input->getOption('preserve-settings'), ]; } diff --git a/src/Job/ExportJob.php b/src/Job/ExportJob.php index 254a3d2..1f57b1a 100644 --- a/src/Job/ExportJob.php +++ b/src/Job/ExportJob.php @@ -281,7 +281,7 @@ private function runScan(JobState $state): void * in; the inventory drives WHERE they live (workbench/ for local * dev, vendor/ for composer-managed). * - * @return array{built: bool, targets: list>, idx: int, files: int, bytes: int, counts: array{asset: int, storage: int, extension: int}, ext_descriptors: list>, has_composer: bool} + * @return array{built: bool, targets: list>, idx: int, files: int, bytes: int, counts: array{asset: int, storage: int, extension: int}, ext_descriptors: list>, has_composer: bool, has_root_extend: bool} */ private function buildScanPlan(JobState $state): array { @@ -350,6 +350,7 @@ private function buildScanPlan(JobState $state): array 'counts' => ['asset' => 0, 'storage' => 0, 'extension' => 0], 'ext_descriptors' => [], 'has_composer' => false, + 'has_root_extend' => false, ]; } @@ -393,13 +394,25 @@ private function scanOneTarget(JobState $state, array &$scan, array $target): vo break; case 'composer': - foreach (['composer.json', 'composer.lock'] as $rel) { + /* + * O extend.php da raiz viaja junto com o composer.json de + * propósito: é ele que CONSUME as dependências que o + * manifesto declara. Mandar só o manifesto foi o que + * produziu, num restore real, um extend.php apontando para + * uma classe que o composer install seguinte tinha podado — + * fatal no boot, antes de existir handler de erro. + */ + foreach (['composer.json', 'composer.lock', 'extend.php'] as $rel) { $abs = rtrim($this->appPaths->base, '/\\').DIRECTORY_SEPARATOR.$rel; if (is_file($abs)) { $size = filesize($abs) ?: 0; $files[] = ['name' => 'project/'.$rel, 'absolute' => $abs, 'size' => $size]; $bytes += $size; - $scan['has_composer'] = true; + if ($rel === 'extend.php') { + $scan['has_root_extend'] = true; + } else { + $scan['has_composer'] = true; + } } } break; @@ -465,6 +478,7 @@ private function finalizeScan(JobState $state, array $scan): void 'extension_count' => (int) $scan['counts']['extension'], 'extensions' => $bundledExts, 'has_composer' => (bool) $scan['has_composer'], + 'has_root_extend' => (bool) ($scan['has_root_extend'] ?? false), ]; $state->set('manifest_summary', $summary); diff --git a/src/Job/ImportJob.php b/src/Job/ImportJob.php index 68d3e4a..bd9c4b0 100644 --- a/src/Job/ImportJob.php +++ b/src/Job/ImportJob.php @@ -12,6 +12,8 @@ use Ramon\Backup\Database\DatabaseRestorer; use Ramon\Backup\Database\Dialect; use Ramon\Backup\Environment\StackSnapshot; +use Ramon\Backup\Project\ProjectReconciler; +use Ramon\Backup\Settings\SettingsPreserver; use Ramon\Backup\StoragePaths; use RuntimeException; use Throwable; @@ -20,19 +22,35 @@ * Mirror of ExportJob, but reversed: pull entries from a `.flarum` * archive and apply them to the running install. * - * Strategy is always "replace": + * Strategy is "replace" for o conteúdo do fórum e "reconcilia" para o + * que descreve o servidor: * - DB statements include `DROP TABLE IF EXISTS` per table — running * them recreates the schema cleanly. * - File entries overwrite anything currently at the destination * path. We never touch paths outside the four bundleable roots * (assets/, storage/, extensions/), so a malicious archive can't * escape the install. + * - `project/*` (composer.json, composer.lock, extend.php da raiz) NÃO + * é gravado direto: vai para um staging dentro do job e passa pelo + * {@see ProjectReconciler}, que faz merge em vez de overwrite. Um + * manifesto de outro servidor sobrescrito é o que já derrubou um + * fórum inteiro — poda o pacote, o extend.php fica apontando para a + * classe podada, e o boot morre antes do handler de erro. + * - As chaves de `settings` que descrevem ESTE servidor (SMTP, fila, + * tokens) são fotografadas antes do restore e regravadas depois — + * ver {@see SettingsPreserver}. * * Phases: * inspect → openHeader + decrypt prep (one tick) * extract → pull each entry; SQL goes into dump.sql, files go to - * their destination directly + * their destination directly, `project/*` vai para staging + * reconcile → merge do composer.json, extend.php opcional, snapshot + * das settings preservadas * restore → replay dump.sql through DatabaseRestorer + * rewrite → troca a URL de origem pela do destino e regrava as + * settings preservadas + * verify → procura classe referenciada pelo extend.php da raiz que + * o autoloader não resolve (o modo de falha silencioso) * finalize → cleanup */ class ImportJob @@ -42,6 +60,17 @@ class ImportJob /** Nomes de entradas não resolvidas guardados para diagnóstico. */ public const UNRESOLVED_SAMPLE_LIMIT = 20; + /** Subdiretório do job onde os `project/*` esperam a reconciliação. */ + public const PROJECT_STAGING_DIR = 'project'; + + /** + * Arquivo do snapshot de settings preservadas. Fica fora do job.json + * porque carrega segredo do destino (senha de SMTP, token OAuth) e o + * job.json é reescrito a cada tick. Mesma exposição do dump.sql, que + * já vive neste diretório 0700 e é apagado no finalize. + */ + public const PRESERVED_SETTINGS_FILE = 'preserved-settings.json'; + /** * Decryption private key for the current run. Held ONLY in memory * and NEVER written to the job-state file: persisting a user-pasted @@ -70,7 +99,9 @@ public function __construct( protected Paths $appPaths, ConnectionInterface $db, protected BackupCipher $cipher, - protected Config $config + protected Config $config, + protected ProjectReconciler $project, + protected SettingsPreserver $preserver ) { $this->db = $db; } @@ -90,9 +121,12 @@ public function withPrivateKey(?string $privateKey): self * dir as `upload.flarum` (the upload controller does that). */ /** - * @param array{db?: bool, assets?: bool, storage?: bool, extensions?: bool|list}|null $selection + * @param array{db?: bool, assets?: bool, storage?: bool, extensions?: bool|list, root_extend?: bool, preserve_settings?: bool}|null $selection * null = restore everything in the archive * extensions: true = all, false = none, array = specific extension dirs + * root_extend: sobrescrever o extend.php da raiz (opt-in) + * preserve_settings: manter as settings de infraestrutura deste + * servidor por cima do restore (opt-out) */ public function start(string $jobId, ?string $privateKey, bool $confirmReplace, ?array $selection, ?int $userId): JobState { @@ -171,14 +205,16 @@ public function runTick(JobState $state): JobState try { match ($phase) { - 'inspect' => $this->runInspect($state), - 'extract' => $this->runExtract($state), - 'restore' => $this->runRestore($state), - 'rewrite' => $this->runRewrite($state), - 'finalize' => $this->runFinalize($state), - 'done' => null, - 'error' => null, - default => throw new RuntimeException("Unknown phase: $phase"), + 'inspect' => $this->runInspect($state), + 'extract' => $this->runExtract($state), + 'reconcile' => $this->runReconcile($state), + 'restore' => $this->runRestore($state), + 'rewrite' => $this->runRewrite($state), + 'verify' => $this->runVerify($state), + 'finalize' => $this->runFinalize($state), + 'done' => null, + 'error' => null, + default => throw new RuntimeException("Unknown phase: $phase"), }; } catch (Throwable $e) { $state->set('phase', 'error'); @@ -406,21 +442,194 @@ private function runExtract(JobState $state): void $state->set('progress', $progress); $state->set('message', $cursor['extract_done'] - ? 'Restoring database…' + ? 'Reconciling project files…' : 'Extracting… '.$progress['extracted_entries'].' entries' ); if ($cursor['extract_done']) { - // No DB dump? Skip straight past the restore phase, but - // still run the rewrite — files might still want URL fixups. - $state->set('phase', is_file($paths['dump']) && filesize($paths['dump']) > 0 ? 'restore' : 'rewrite'); + /* + * A reconciliação roda SEMPRE, inclusive num restore só de + * banco: é ela que compara os manifestos e avisa o que falta. + */ + $state->set('phase', 'reconcile'); + } + + $state->save(); + } + + /** + * Phase 3 — reconcilia o que descreve o servidor, antes de o banco + * ser tocado. + * + * Duas responsabilidades, ambas com a mesma justificativa: o backup + * descreve o fórum de ORIGEM, e há estado que pertence ao destino. + * Aqui o composer.json é unido (nunca sobrescrito), o extend.php da + * raiz só é aplicado sob opt-in, e as settings de infraestrutura do + * destino são fotografadas enquanto a tabela ainda é a dele. + */ + private function runReconcile(JobState $state): void + { + $paths = $state->get('paths'); + $opts = (array) $state->get('options'); + $selection = $opts['selection'] ?? null; + + $staging = $this->projectStagingDir($state); + + $applyComposer = $selection === null || $this->hasAnyExtensionSelected($selection); + $applyRootExtend = $selection !== null && ! empty($selection['root_extend']); + + try { + $result = $this->project->reconcile($staging, $applyComposer, $applyRootExtend); + $this->addWarnings($state, $result['warnings']); + $state->set('project_applied', array_values($result['applied'])); + } catch (Throwable $e) { + $this->addWarnings($state, [ + 'Falha ao reconciliar os arquivos da raiz do projeto: '.$e->getMessage() + .'. O composer.json e o extend.php deste servidor ficaram como estavam.', + ]); } + $this->snapshotPreservedSettings($state, $selection); + + $hasDump = is_file($paths['dump']) && filesize($paths['dump']) > 0; + $state->set('phase', $hasDump ? 'restore' : 'rewrite'); + $state->set('message', $hasDump ? 'Restoring database…' : 'Rewriting URLs…'); $state->save(); } /** - * Phase 3 — replay dump.sql through DatabaseRestorer in chunks. We + * Fotografa as settings preservadas num arquivo próprio do job. Roda + * antes do DROP/CREATE; sem dump de banco não há nada a preservar, + * porque a tabela não vai ser tocada. + * + * @param array|null $selection + */ + private function snapshotPreservedSettings(JobState $state, ?array $selection): void + { + $wantsDb = $selection === null || ! empty($selection['db']); + $wantsPreserve = $selection === null || ! empty($selection['preserve_settings']); + + if (! $wantsDb || ! $wantsPreserve) { + return; + } + + try { + $snapshot = $this->preserver->snapshot(); + } catch (Throwable) { + return; + } + + if ($snapshot === []) { + return; + } + + $encoded = json_encode($snapshot, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + if ($encoded === false) { + return; + } + + $file = $this->preservedSettingsFile($state); + if (@file_put_contents($file, $encoded, LOCK_EX) === false) { + return; + } + @chmod($file, 0600); + + $state->set('preserved_settings_count', count($snapshot)); + } + + /** + * Regrava o snapshot depois do restore E depois do rewrite de URL, + * para ter a última palavra sobre qualquer chave que os dois toquem. + */ + private function reapplyPreservedSettings(JobState $state): void + { + $file = $this->preservedSettingsFile($state); + if (! is_file($file)) { + return; + } + + $raw = @file_get_contents($file); /* arquivo do próprio job, 0600; nosemgrep: flarum-v2-server-side-fetch */ + @unlink($file); + + if ($raw === false) { + return; + } + $snapshot = json_decode($raw, true); + if (! is_array($snapshot) || $snapshot === []) { + return; + } + + try { + $restored = $this->preserver->reapply($snapshot); + } catch (Throwable $e) { + $this->addWarnings($state, [ + 'Não foi possível regravar a configuração deste servidor após o restore: '.$e->getMessage() + .'. Reveja SMTP, fila e integrações no painel.', + ]); + return; + } + + if ($restored === []) { + return; + } + + $state->set('preserved_settings_restored', count($restored)); + $this->addWarnings($state, [sprintf( + '%d configuração(ões) deste servidor foram preservadas do backup (SMTP, fila, integrações). ' + .'Ajuste a lista em `%s` se algo específico do seu ambiente ainda estiver sendo sobrescrito.', + count($restored), + SettingsPreserver::EXTRA_KEY + )]); + } + + /** Caminho do staging dos `project/*`, sem criar nada. */ + private function projectStagingPath(JobState $state): string + { + $paths = (array) $state->get('paths'); + return rtrim((string) ($paths['dir'] ?? ''), '/\\') + .DIRECTORY_SEPARATOR.self::PROJECT_STAGING_DIR; + } + + /** + * Mesmo caminho, garantindo o diretório. 0700 pelo mesmo motivo do + * resto do job: o staging guarda o composer.json e o extend.php da + * origem enquanto esperam a reconciliação. + */ + private function projectStagingDir(JobState $state): string + { + $dir = $this->projectStagingPath($state); + if (! is_dir($dir)) { + @mkdir($dir, 0700, true); + } + return $dir; + } + + private function preservedSettingsFile(JobState $state): string + { + $paths = (array) $state->get('paths'); + return rtrim((string) ($paths['dir'] ?? ''), '/\\') + .DIRECTORY_SEPARATOR.self::PRESERVED_SETTINGS_FILE; + } + + /** + * Acrescenta avisos sem duplicar. O finalize também mexe nessa + * lista, então a deduplicação tem que valer para os dois. + * + * @param list $new + */ + private function addWarnings(JobState $state, array $new): void + { + if ($new === []) { + return; + } + $state->set('warnings', array_values(array_unique(array_merge( + array_map(fn ($w) => (string) $w, (array) $state->get('warnings', [])), + array_map(fn ($w) => (string) $w, $new) + )))); + } + + /** + * Phase 4 — replay dump.sql through DatabaseRestorer in chunks. We * track restore_offset so each tick resumes from where it left off. */ private function runRestore(JobState $state): void @@ -589,7 +798,7 @@ private function resyncPostgresSequences(JobState $state): void } /** - * Phase 4 — URL rewrite. Backups taken from one Flarum and + * Phase 5 — URL rewrite. Backups taken from one Flarum and * restored onto another would otherwise leave the OLD forum URL * everywhere — settings, post content, parsed_content. That breaks * redirects, login cookies, and any absolute link the old site had @@ -666,6 +875,52 @@ private function runRewrite(JobState $state): void $state->set('message', 'Skipping URL rewrite (URLs match or unknown).'); } + /* + * Depois do REPLACE de URL de propósito: a configuração deste + * servidor tem que ser a última a escrever nas chaves que as duas + * etapas encostam. + */ + $this->reapplyPreservedSettings($state); + + $state->set('phase', 'verify'); + $state->save(); + } + + /** + * Phase 6 — checagem de sanidade do boot. + * + * O core dá `require` no `extend.php` da raiz dentro de + * `Site::fromPaths()`, antes de existir qualquer handler de erro. Uma + * classe ausente ali não vira 500 com stack trace: vira exit 255 com + * saída vazia e log do Flarum vazio. Essa fase troca esse silêncio + * por um aviso nomeando a classe. + * + * Limite conhecido: a checagem usa o autoloader deste processo, então + * enxerga o vendor/ como está agora. Um `composer install` posterior + * ainda pode podar pacote — o que o merge do composer.json (ver + * ProjectReconciler) existe para evitar. + */ + private function runVerify(JobState $state): void + { + try { + $dangling = $this->project->danglingClassReferences(); + } catch (Throwable) { + $dangling = []; + } + + if ($dangling !== []) { + $state->set('boot_dangling_classes', array_values($dangling)); + $this->addWarnings($state, [sprintf( + 'BOOT EM RISCO: o extend.php da raiz referencia %s que o autoloader não resolve. ' + .'O Flarum carrega esse arquivo antes do handler de erro, então isso derruba o fórum ' + .'com HTTP 500 sem mensagem. Reinstale o pacote que fornece a classe ou comente o ' + .'extender antes de recarregar.', + count($dangling) === 1 + ? 'a classe `'.$dangling[0].'`' + : 'as classes `'.implode('`, `', array_slice($dangling, 0, 5)).'`' + )]); + } + $state->set('phase', 'finalize'); $state->save(); } @@ -699,9 +954,12 @@ private function runFinalize(JobState $state): void $paths = $state->get('paths'); @unlink($paths['dump']); @unlink($paths['archive']); + @unlink($this->preservedSettingsFile($state)); + $this->paths->deleteDir($this->projectStagingPath($state)); $progress = (array) $state->get('progress'); $unresolved = (int) ($progress['unresolved_entries'] ?? 0); + $dangling = (array) $state->get('boot_dangling_classes', []); $warnings = (array) $state->get('warnings', []); if ($unresolved > 0) { @@ -723,25 +981,40 @@ private function runFinalize(JobState $state): void fn ($w) => (string) $w, $warnings )))); - $state->set('incomplete', $unresolved > 0); + + /* + * Referência pendurada no extend.php da raiz conta como restore + * incompleto tanto quanto uma entrada não gravada: nos dois casos + * o operador precisa agir antes de recarregar, e nos dois a tela + * de sucesso limpo seria mentira. + */ + $state->set('incomplete', $unresolved > 0 || $dangling !== []); $state->set('phase', 'done'); - $state->set('message', $unresolved > 0 - ? sprintf('Restore finished with %d unrestored entr%s.', $unresolved, $unresolved === 1 ? 'y' : 'ies') - : 'Restore complete.' - ); + $state->set('message', match (true) { + $dangling !== [] => 'Restore finished — the root extend.php references classes that are not installed.', + $unresolved > 0 => sprintf('Restore finished with %d unrestored entr%s.', $unresolved, $unresolved === 1 ? 'y' : 'ies'), + default => 'Restore complete.', + }); $state->save(); } /** * Normalise the user's selection into a stable shape: * ['db' => bool, 'assets' => bool, 'storage' => bool, - * 'extensions' => true | false | list] + * 'extensions' => true | false | list, + * 'root_extend' => bool, 'preserve_settings' => bool] * * `null` short-circuits to restore-everything for backwards * compatibility with older import requests. * + * Os dois campos novos têm defaults assimétricos de propósito: + * `root_extend` é opt-in porque sobrescrever o extend.php da raiz + * troca configuração viva do destino; `preserve_settings` é opt-out + * porque perder a configuração de infraestrutura do destino é o + * defeito que essa camada existe para impedir. + * * @param array|null $raw - * @return array{db: bool, assets: bool, storage: bool, extensions: bool|list}|null + * @return array{db: bool, assets: bool, storage: bool, extensions: bool|list, root_extend: bool, preserve_settings: bool}|null */ private function normaliseSelection(?array $raw): ?array { @@ -758,10 +1031,12 @@ private function normaliseSelection(?array $raw): ?array } return [ - 'db' => ! empty($raw['db']), - 'assets' => ! empty($raw['assets']), - 'storage' => ! empty($raw['storage']), - 'extensions' => $extensions, + 'db' => ! empty($raw['db']), + 'assets' => ! empty($raw['assets']), + 'storage' => ! empty($raw['storage']), + 'extensions' => $extensions, + 'root_extend' => ! empty($raw['root_extend']), + 'preserve_settings' => ! array_key_exists('preserve_settings', $raw) || ! empty($raw['preserve_settings']), ]; } @@ -789,10 +1064,14 @@ private function shouldExtract(string $name, int $type, ?array $selection): bool 'assets' => ! empty($selection['assets']), 'storage' => ! empty($selection['storage']), 'extensions' => $this->isExtensionAllowed($name, $selection['extensions'] ?? null), - // composer.json / composer.lock follow the extensions - // toggle: if the admin opted out of all extensions, they - // didn't ask to restore composer either. - 'project' => $this->hasAnyExtensionSelected($selection), + /* + * `project/*` é sempre extraído, mesmo com tudo desmarcado: + * ele vai para um staging, não para o disco vivo, e é o que + * permite ao reconciliador comparar o manifesto do backup com + * o deste servidor e avisar quais pacotes faltam. Quem decide + * o que efetivamente sobrescreve algo é o runReconcile. + */ + 'project' => true, default => true, }; } @@ -900,14 +1179,21 @@ private function resolveDestination(string $name, JobState $state): ?string $base = $map[$extId] ?? null; if ($base === null) return null; } elseif ($root === 'project') { - // Project-root files. Only composer.json / composer.lock - // are accepted here — any other path is rejected outright - // so a hostile archive can't drop, say, a `.htaccess` or - // a public/ override into the install. - if (! in_array($rest, ['composer.json', 'composer.lock'], true)) { + /* + * Arquivos da raiz do projeto. A allow-list continua fechada + * — nada além dos três nomes conhecidos entra, para um + * arquivo hostil não conseguir largar um `.htaccess` ou um + * override de public/ na instalação. + * + * A diferença para as outras raízes: o destino aqui é um + * staging DENTRO do diretório do job, nunca a raiz viva. O + * que chega ao disco real é decidido depois pelo + * ProjectReconciler, que faz merge em vez de overwrite. + */ + if (! in_array($rest, ProjectReconciler::ALLOWED, true)) { return null; } - $base = rtrim($this->appPaths->base, '/\\'); + $base = $this->projectStagingDir($state); $inner = $rest; } else { $base = match ($root) { diff --git a/src/Project/ProjectReconciler.php b/src/Project/ProjectReconciler.php new file mode 100644 index 0000000..ebc5842 --- /dev/null +++ b/src/Project/ProjectReconciler.php @@ -0,0 +1,586 @@ +, applied: list} + */ + public function reconcile(string $stagingDir, bool $applyComposer, bool $applyRootExtend): array + { + $warnings = []; + $applied = []; + + $composerResult = $this->reconcileComposer($stagingDir, $applyComposer); + $warnings = array_merge($warnings, $composerResult['warnings']); + $applied = array_merge($applied, $composerResult['applied']); + + $extendResult = $this->reconcileRootExtend($stagingDir, $applyRootExtend); + $warnings = array_merge($warnings, $extendResult['warnings']); + $applied = array_merge($applied, $extendResult['applied']); + + return ['warnings' => $warnings, 'applied' => $applied]; + } + + /** + * Une o manifesto do arquivo ao do destino. Sempre calcula o diff dos + * requires — mesmo quando não vai gravar — porque saber quais pacotes + * as extensões restauradas exigem é o que evita descobrir a falta só + * quando o fórum não sobe. + * + * @return array{warnings: list, applied: list} + */ + private function reconcileComposer(string $stagingDir, bool $apply): array + { + $incomingPath = $stagingDir.DIRECTORY_SEPARATOR.'composer.json'; + if (! is_file($incomingPath)) { + return ['warnings' => [], 'applied' => []]; + } + + $incoming = $this->decodeJson($incomingPath); + if ($incoming === null) { + return [ + 'warnings' => ['O composer.json do arquivo não é JSON válido — manifesto do destino mantido intacto.'], + 'applied' => [], + ]; + } + + $destPath = rtrim($this->appPaths->base, '/\\').DIRECTORY_SEPARATOR.'composer.json'; + + if (! is_file($destPath)) { + if (! $apply) { + return ['warnings' => [], 'applied' => []]; + } + return $this->installFreshManifest($stagingDir, $destPath, $incomingPath); + } + + $current = $this->decodeJson($destPath); + if ($current === null) { + return [ + 'warnings' => ['O composer.json deste servidor não é JSON válido — merge abortado para não piorar o estado.'], + 'applied' => [], + ]; + } + + $warnings = $this->requireDiffWarnings($current, $incoming); + + if (! $apply) { + return ['warnings' => $warnings, 'applied' => []]; + } + + $merged = $this->mergeManifests($current, $incoming); + + if ($merged === $current) { + return ['warnings' => $warnings, 'applied' => []]; + } + + $encoded = json_encode( + $merged, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + ); + if ($encoded === false) { + return [ + 'warnings' => array_merge($warnings, ['Falha ao serializar o composer.json unido — manifesto do destino mantido intacto.']), + 'applied' => [], + ]; + } + + $this->backup($destPath); + if (@file_put_contents($destPath, $encoded."\n", LOCK_EX) === false) { + return [ + 'warnings' => array_merge($warnings, ['Não foi possível gravar o composer.json unido (permissão?).']), + 'applied' => [], + ]; + } + + $warnings[] = 'composer.json unido ao do destino (nada foi removido). ' + .'O composer.lock do arquivo NÃO foi aplicado: um lock de outro servidor poda pacotes ' + .'que só existem aqui. Rode `composer update --lock` para regravar o lock local.'; + + return ['warnings' => $warnings, 'applied' => ['composer.json']]; + } + + /** + * Servidor sem manifesto é recuperação em máquina limpa: aí o par + * composer.json + composer.lock da origem é exatamente o estado + * desejado e vai verbatim. + * + * @return array{warnings: list, applied: list} + */ + private function installFreshManifest(string $stagingDir, string $destPath, string $incomingPath): array + { + $applied = []; + if (@copy($incomingPath, $destPath)) { + $applied[] = 'composer.json'; + } + + $lock = $stagingDir.DIRECTORY_SEPARATOR.'composer.lock'; + $destLock = rtrim($this->appPaths->base, '/\\').DIRECTORY_SEPARATOR.'composer.lock'; + if (is_file($lock) && ! is_file($destLock) && @copy($lock, $destLock)) { + $applied[] = 'composer.lock'; + } + + return [ + 'warnings' => ['Servidor sem composer.json — manifesto e lock do arquivo aplicados verbatim. Rode `composer install`.'], + 'applied' => $applied, + ]; + } + + /** + * Diferença entre os requires dos dois manifestos, em linguagem de + * operador. O lado "só no destino" é o que um overwrite cego teria + * apagado; o lado "só no arquivo" é o que falta instalar para as + * extensões restauradas subirem. + * + * @param array $current + * @param array $incoming + * @return list + */ + private function requireDiffWarnings(array $current, array $incoming): array + { + $here = $this->requireMap($current); + $there = $this->requireMap($incoming); + + $warnings = []; + + $onlyHere = array_diff_key($here, $there); + if ($onlyHere !== []) { + $warnings[] = sprintf( + 'Pacotes que existem só neste servidor e NÃO estavam no backup (preservados pelo merge): %s. ' + .'Confirme que o extend.php da raiz e as extensões restauradas continuam compatíveis com eles.', + $this->summarise(array_keys($onlyHere)) + ); + } + + $onlyThere = array_diff_key($there, $here); + if ($onlyThere !== []) { + $warnings[] = sprintf( + 'Pacotes exigidos pelo backup e ausentes deste servidor: %s. ' + .'Rode `composer install` para materializá-los antes de considerar a migração concluída.', + $this->summarise(array_keys($onlyThere)) + ); + } + + return $warnings; + } + + /** + * Aplica o extend.php da raiz vindo do arquivo. Fica atrás de opt-in + * explícito porque esse arquivo é configuração do servidor de destino + * tanto quanto do fórum de origem — e o anterior sempre vira .bak. + * + * @return array{warnings: list, applied: list} + */ + private function reconcileRootExtend(string $stagingDir, bool $apply): array + { + $incoming = $stagingDir.DIRECTORY_SEPARATOR.'extend.php'; + if (! $apply || ! is_file($incoming)) { + return ['warnings' => [], 'applied' => []]; + } + + $destPath = rtrim($this->appPaths->base, '/\\').DIRECTORY_SEPARATOR.'extend.php'; + $this->backup($destPath); + + if (! @copy($incoming, $destPath)) { + return [ + 'warnings' => ['Não foi possível gravar o extend.php da raiz (permissão?).'], + 'applied' => [], + ]; + } + + return [ + 'warnings' => ['extend.php da raiz substituído pelo do backup (cópia do anterior salva como .bak-*).'], + 'applied' => ['extend.php'], + ]; + } + + /** + * Varre o extend.php da raiz procurando classes referenciadas que o + * autoloader não resolve. + * + * Existe por causa do modo de falha que motivou esta camada: o core + * dá `require` nesse arquivo dentro de `Site::fromPaths()`, antes de + * qualquer handler de erro — uma classe ausente vira fatal com exit + * 255, saída vazia e log do Flarum vazio. Detectar aqui troca o 500 + * mudo por um aviso nomeando a classe. + * + * Análise estática: o arquivo NUNCA é executado (dar `require` nele + * teria efeito colateral). A checagem usa o autoloader vivo, então + * enxerga o vendor/ como está no disco agora — não o que sobrará + * depois de um `composer install`. + * + * @return list FQCNs não resolvidos. + */ + public function danglingClassReferences(): array + { + $path = rtrim($this->appPaths->base, '/\\').DIRECTORY_SEPARATOR.'extend.php'; + if (! is_file($path)) { + return []; + } + + $source = $this->readLocalFile($path); + if ($source === null || $source === '') { + return []; + } + + try { + $candidates = $this->referencedClasses($source); + } catch (Throwable) { + return []; + } + + $missing = []; + foreach ($candidates as $fqcn) { + if (! class_exists($fqcn) && ! interface_exists($fqcn)) { + $missing[] = $fqcn; + } + } + + return array_values(array_unique($missing)); + } + + /** + * FQCNs citados em `new X(...)` e `X::` no código, resolvidos contra + * o `namespace` e os `use` do arquivo. + * + * @return list + */ + private function referencedClasses(string $source): array + { + $tokens = token_get_all($source); + $count = count($tokens); + + $namespace = ''; + $aliases = []; + $names = []; + + for ($i = 0; $i < $count; $i++) { + $token = $tokens[$i]; + if (! is_array($token)) { + continue; + } + + if ($token[0] === T_NAMESPACE) { + $namespace = trim($this->readName($tokens, $i, $count), '\\'); + continue; + } + + if ($token[0] === T_USE) { + $this->collectAliases($tokens, $i, $count, $aliases); + continue; + } + + if ($token[0] === T_NEW) { + $name = $this->readName($tokens, $i, $count); + if ($name !== '') { + $names[] = $name; + } + continue; + } + + if ($token[0] === T_DOUBLE_COLON) { + $name = $this->readNameBackwards($tokens, $i); + if ($name !== '') { + $names[] = $name; + } + } + } + + $resolved = []; + foreach ($names as $name) { + $fqcn = $this->resolveName($name, $namespace, $aliases); + if ($fqcn !== '') { + $resolved[] = $fqcn; + } + } + + return array_values(array_unique($resolved)); + } + + /** + * Lê o próximo nome de classe a partir de $i, pulando espaços. Só + * grupos `use A\{B, C}` são ignorados — não aparecem em extend.php + * real e resolvê-los não pagaria o custo. + * + * @param array $tokens + */ + private function readName(array $tokens, int $i, int $count): string + { + for ($j = $i + 1; $j < $count; $j++) { + $t = $tokens[$j]; + if (is_array($t) && in_array($t[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) { + continue; + } + if (is_array($t) && in_array($t[0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED, T_NAME_RELATIVE], true)) { + return (string) $t[1]; + } + return ''; + } + return ''; + } + + /** + * Lê o nome imediatamente antes de um `::`. + * + * @param array $tokens + */ + private function readNameBackwards(array $tokens, int $i): string + { + for ($j = $i - 1; $j >= 0; $j--) { + $t = $tokens[$j]; + if (is_array($t) && in_array($t[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) { + continue; + } + if (is_array($t) && in_array($t[0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED, T_NAME_RELATIVE], true)) { + return (string) $t[1]; + } + return ''; + } + return ''; + } + + /** + * Preenche o mapa alias → FQCN a partir de um `use A\B\C as D;`. + * + * @param array $tokens + * @param array $aliases + */ + private function collectAliases(array $tokens, int $i, int $count, array &$aliases): void + { + $fqcn = ''; + $alias = ''; + $sawAs = false; + + for ($j = $i + 1; $j < $count; $j++) { + $t = $tokens[$j]; + if ($t === ';' || $t === '{' || $t === '(') { + break; + } + if (! is_array($t)) { + continue; + } + if ($t[0] === T_AS) { + $sawAs = true; + continue; + } + if (in_array($t[0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED, T_NAME_RELATIVE], true)) { + if ($sawAs) { + $alias = (string) $t[1]; + } elseif ($fqcn === '') { + $fqcn = (string) $t[1]; + } + } + } + + if ($fqcn === '') { + return; + } + $fqcn = ltrim($fqcn, '\\'); + if ($alias === '') { + $parts = explode('\\', $fqcn); + $alias = (string) end($parts); + } + $aliases[strtolower($alias)] = $fqcn; + } + + /** + * Resolve um nome como o PHP resolveria: `\A\B` é absoluto, o + * primeiro segmento pode bater num `use`, e o resto herda o + * namespace do arquivo. + * + * @param array $aliases + */ + private function resolveName(string $name, string $namespace, array $aliases): string + { + if ($name === '' || in_array(strtolower($name), ['self', 'static', 'parent', 'class'], true)) { + return ''; + } + + if (str_starts_with($name, '\\')) { + return ltrim($name, '\\'); + } + + $segments = explode('\\', $name); + $head = strtolower($segments[0]); + + if (isset($aliases[$head])) { + $segments[0] = $aliases[$head]; + return implode('\\', $segments); + } + + return $namespace === '' ? $name : $namespace.'\\'.$name; + } + + /** + * União das seções de mapa e de `repositories`. O destino sempre + * vence: uma constraint local mais estrita costuma ser um pin de + * segurança e reverter isso silenciosamente é regressão. + * + * @param array $current + * @param array $incoming + * @return array + */ + private function mergeManifests(array $current, array $incoming): array + { + $merged = $current; + + foreach (self::MERGED_MAPS as $section) { + $here = is_array($current[$section] ?? null) ? $current[$section] : []; + $there = is_array($incoming[$section] ?? null) ? $incoming[$section] : []; + if ($here === [] && $there === []) { + continue; + } + $merged[$section] = $here + $there; + } + + $mergedRepos = $this->mergeRepositories( + is_array($current['repositories'] ?? null) ? $current['repositories'] : [], + is_array($incoming['repositories'] ?? null) ? $incoming['repositories'] : [] + ); + if ($mergedRepos !== []) { + $merged['repositories'] = $mergedRepos; + } + + return $merged; + } + + /** + * Repositórios são uma lista, não um mapa: a identidade usada para + * deduplicar é o JSON canônico da entrada. + * + * @param array $here + * @param array $there + * @return list + */ + private function mergeRepositories(array $here, array $there): array + { + $out = []; + $seen = []; + + foreach ([array_values($here), array_values($there)] as $set) { + foreach ($set as $repo) { + $key = json_encode($repo); + if ($key === false || isset($seen[$key])) { + continue; + } + $seen[$key] = true; + $out[] = $repo; + } + } + + return $out; + } + + /** + * Requires de um manifesto, sem as pseudo-dependências de plataforma + * (`php`, `ext-*`, `lib-*`) que só poluiriam o diff. + * + * @param array $manifest + * @return array + */ + private function requireMap(array $manifest): array + { + $raw = is_array($manifest['require'] ?? null) ? $manifest['require'] : []; + $out = []; + foreach ($raw as $package => $constraint) { + $package = (string) $package; + if ($package === 'php' || str_starts_with($package, 'ext-') || str_starts_with($package, 'lib-')) { + continue; + } + $out[$package] = (string) $constraint; + } + return $out; + } + + /** + * Lista truncada para o aviso não virar um parágrafo ilegível quando + * o diff é grande. + * + * @param list $items + */ + private function summarise(array $items): string + { + sort($items); + $shown = array_slice($items, 0, 8); + $rest = count($items) - count($shown); + return implode(', ', $shown).($rest > 0 ? sprintf(' (+%d)', $rest) : ''); + } + + /** + * Cópia datada antes de qualquer escrita destrutiva na raiz. Falha + * silenciosa é aceitável: é rede de segurança, não pré-requisito. + */ + private function backup(string $path): void + { + if (! is_file($path)) { + return; + } + @copy($path, $path.'.bak-'.date('Ymd-His')); + } + + /** @return array|null */ + private function decodeJson(string $path): ?array + { + $raw = $this->readLocalFile($path); + if ($raw === null) { + return null; + } + $decoded = json_decode($raw, true); + return is_array($decoded) ? $decoded : null; + } + + /** + * Lê um arquivo local do próprio install: o manifesto da raiz, o + * extend.php da raiz, ou o staging deste job. + * + * O caminho deriva SEMPRE de {@see Paths} ou do diretório do job — + * nunca de input de requisição —, então o alerta de SSRF do semgrep + * é falso positivo. Concentrar a leitura aqui deixa a supressão num + * ponto só, em vez de espalhada por cada chamada. + */ + private function readLocalFile(string $path): ?string + { + $raw = @file_get_contents($path); /* caminho derivado de Paths, nunca de request; nosemgrep: flarum-v2-server-side-fetch */ + return $raw === false ? null : $raw; + } +} diff --git a/src/Settings/SettingsPreserver.php b/src/Settings/SettingsPreserver.php new file mode 100644 index 0000000..c5a332c --- /dev/null +++ b/src/Settings/SettingsPreserver.php @@ -0,0 +1,143 @@ + + */ + public function snapshot(): array + { + try { + $all = $this->settings->all(); + } catch (Throwable) { + return []; + } + + $patterns = $this->patterns(); + $out = []; + + foreach ($all as $key => $value) { + $key = (string) $key; + if ($this->matches($key, $patterns)) { + $out[$key] = $value === null ? null : (string) $value; + } + } + + return $out; + } + + /** + * Regrava o snapshot por cima da tabela recém-restaurada. Roda depois + * do rewrite de URL para ter a última palavra sobre qualquer chave + * que os dois toquem. + * + * @param array $snapshot + * @return list Chaves efetivamente regravadas. + */ + public function reapply(array $snapshot): array + { + $restored = []; + + foreach ($snapshot as $key => $value) { + try { + $this->settings->set((string) $key, $value); + $restored[] = (string) $key; + } catch (Throwable) { + /* uma chave que não regrava não pode abortar as outras */ + } + } + + return $restored; + } + + /** + * Baseline mais o que o admin configurou. Lido do próprio repositório + * porque a lista precisa ser editável sem release da extensão. + * + * @return list + */ + public function patterns(): array + { + $patterns = self::BASELINE; + + try { + $extra = (string) ($this->settings->get(self::EXTRA_KEY) ?? ''); + } catch (Throwable) { + $extra = ''; + } + + foreach (preg_split('/[\r\n,]+/', $extra) ?: [] as $line) { + $line = trim((string) $line); + if ($line !== '') { + $patterns[] = $line; + } + } + + return array_values(array_unique($patterns)); + } + + /** + * @param list $patterns + */ + private function matches(string $key, array $patterns): bool + { + foreach ($patterns as $pattern) { + if (str_ends_with($pattern, '*')) { + if (str_starts_with($key, substr($pattern, 0, -1))) { + return true; + } + continue; + } + if ($key === $pattern) { + return true; + } + } + return false; + } +} diff --git a/tests/Integration/CliTransferE2ETest.php b/tests/Integration/CliTransferE2ETest.php index a875dba..a003451 100644 --- a/tests/Integration/CliTransferE2ETest.php +++ b/tests/Integration/CliTransferE2ETest.php @@ -141,13 +141,7 @@ public function test_export_then_import_round_trips_via_cli(): void $this->assertGreaterThan(0, filesize($archive)); // --- backup:import ------------------------------------------------- - $importJob = new ImportJob( - $this->storagePaths, - $this->paths, - $target, - $this->cipher(), - $this->config - ); + $importJob = $this->importJob($target); $importTester = new CommandTester(new ImportCommand($importJob, $this->storagePaths)); $importStatus = $importTester->execute([ 'archive' => $archive, @@ -165,7 +159,7 @@ public function test_export_then_import_round_trips_via_cli(): void public function test_import_refuses_without_yes(): void { $target = $this->capsule->getConnection('target'); - $importJob = new ImportJob($this->storagePaths, $this->paths, $target, $this->cipher(), $this->config); + $importJob = $this->importJob($target); $tester = new CommandTester(new ImportCommand($importJob, $this->storagePaths)); $status = $tester->execute(['archive' => __FILE__]); // any existing file @@ -219,7 +213,7 @@ public function test_decryption_key_is_never_persisted_to_job_state(): void $archive = $this->storagePaths->backupsDir().DIRECTORY_SEPARATOR.(string) $estate->get('result')['filename']; // Import with the private key, watching the job-state file. - $importJob = new ImportJob($this->storagePaths, $this->paths, $target, $this->cipher(), $this->config); + $importJob = $this->importJob($target); $importId = bin2hex(random_bytes(8)); $importDir = $this->storagePaths->importJobDir($importId); copy($archive, $importDir.DIRECTORY_SEPARATOR.'upload.flarum'); @@ -253,6 +247,29 @@ public function test_decryption_key_is_never_persisted_to_job_state(): void $this->assertSame($expected['posts'], $actual['posts']); } + /** + * ImportJob com as dependências de reconciliação. Os dois últimos + * argumentos entraram junto com o staging de `project/*`; sem eles o + * job não sabe reconciliar composer.json nem preservar settings. + */ + private function importJob(\Illuminate\Database\ConnectionInterface $target): ImportJob + { + $settings = Mockery::mock(\Flarum\Settings\SettingsRepositoryInterface::class); + $settings->shouldReceive('get')->andReturn(''); + $settings->shouldReceive('set')->andReturnNull(); + $settings->shouldReceive('all')->andReturn([]); + + return new ImportJob( + $this->storagePaths, + $this->paths, + $target, + $this->cipher(), + $this->config, + new \Ramon\Backup\Project\ProjectReconciler($this->paths), + new \Ramon\Backup\Settings\SettingsPreserver($settings) + ); + } + private function cipher(): BackupCipher { $settings = Mockery::mock(\Flarum\Settings\SettingsRepositoryInterface::class); diff --git a/tests/Integration/FullPipelineTransferTest.php b/tests/Integration/FullPipelineTransferTest.php index 40320f1..8f3a2d3 100644 --- a/tests/Integration/FullPipelineTransferTest.php +++ b/tests/Integration/FullPipelineTransferTest.php @@ -141,13 +141,7 @@ private function runExport(Connection $source, string $targetDialect): string private function runImport(Connection $target, string $archive): void { - $job = new ImportJob( - $this->storagePaths, - $this->paths, - $target, - $this->cipher(), - $this->config - ); + $job = $this->importJob($target); $jobId = bin2hex(random_bytes(8)); $dir = $this->storagePaths->importJobDir($jobId); @@ -191,6 +185,29 @@ private function buildBackupsTable(Connection $db): void }); } + /** + * ImportJob com as dependências de reconciliação. Os dois últimos + * argumentos entraram junto com o staging de `project/*`; sem eles o + * job não sabe reconciliar composer.json nem preservar settings. + */ + private function importJob(\Illuminate\Database\ConnectionInterface $target): ImportJob + { + $settings = Mockery::mock(\Flarum\Settings\SettingsRepositoryInterface::class); + $settings->shouldReceive('get')->andReturn(''); + $settings->shouldReceive('set')->andReturnNull(); + $settings->shouldReceive('all')->andReturn([]); + + return new ImportJob( + $this->storagePaths, + $this->paths, + $target, + $this->cipher(), + $this->config, + new \Ramon\Backup\Project\ProjectReconciler($this->paths), + new \Ramon\Backup\Settings\SettingsPreserver($settings) + ); + } + private function cipher(): BackupCipher { $settings = Mockery::mock(\Flarum\Settings\SettingsRepositoryInterface::class); diff --git a/tests/Integration/ImportPathTraversalTest.php b/tests/Integration/ImportPathTraversalTest.php index d2afe99..dbf23aa 100644 --- a/tests/Integration/ImportPathTraversalTest.php +++ b/tests/Integration/ImportPathTraversalTest.php @@ -25,6 +25,7 @@ final class ImportPathTraversalTest extends TestCase { private string $workdir; + private string $jobDir; private ImportJob $job; private ReflectionMethod $resolve; private ReflectionMethod $extMap; @@ -32,9 +33,10 @@ final class ImportPathTraversalTest extends TestCase protected function setUp(): void { $this->workdir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'bkp_pt_'.bin2hex(random_bytes(4)); - foreach (['', '/public', '/public/assets', '/storage', '/workbench', '/vendor'] as $sub) { + foreach (['', '/public', '/public/assets', '/storage', '/workbench', '/vendor', '/jobdir'] as $sub) { @mkdir($this->workdir.$sub, 0777, true); } + $this->jobDir = $this->workdir.DIRECTORY_SEPARATOR.'jobdir'; $paths = new Paths([ 'base' => $this->workdir, @@ -51,7 +53,15 @@ protected function setUp(): void // satisfies the (narrowed) Connection type is enough. $db = Mockery::mock(Connection::class); - $this->job = new ImportJob(new StoragePaths($paths), $paths, $db, $cipher, $config); + $this->job = new ImportJob( + new StoragePaths($paths), + $paths, + $db, + $cipher, + $config, + new \Ramon\Backup\Project\ProjectReconciler($paths), + new \Ramon\Backup\Settings\SettingsPreserver($settings) + ); // PHP 8.1+ reflection reaches private methods without // setAccessible() (which is a deprecated no-op since 8.5). @@ -68,6 +78,11 @@ protected function tearDown(): void private function state(array $data = []): JobState { $file = $this->workdir.DIRECTORY_SEPARATOR.'job_'.bin2hex(random_bytes(3)).'.json'; + + // `project/*` resolve para um staging dentro do diretório do job, + // então o state precisa carregar `paths.dir` como o job real faz. + $data['paths'] = $data['paths'] ?? ['dir' => $this->jobDir]; + return JobState::create($file, $data); } @@ -104,6 +119,33 @@ public function test_resolveDestination_accepts_legit_paths(): void $this->assertNotNull($this->resolve('assets/avatars/1.png')); $this->assertNotNull($this->resolve('storage/uploads/a.txt')); $this->assertNotNull($this->resolve('project/composer.json')); + $this->assertNotNull($this->resolve('project/extend.php')); + } + + /** + * Regressão do incidente que motivou o ProjectReconciler: um restore + * sobrescreveu o composer.json vivo com o do backup, o composer + * install seguinte podou o pacote, e o extend.php da raiz ficou + * apontando para uma classe inexistente — fatal antes do handler de + * erro. `project/*` nunca mais pode resolver para a raiz do install. + */ + public function test_project_entries_land_in_staging_not_the_install_root(): void + { + foreach (['composer.json', 'composer.lock', 'extend.php'] as $name) { + $resolved = $this->resolve('project/'.$name); + + $this->assertNotNull($resolved, "project/$name should resolve"); + $this->assertStringStartsWith( + $this->jobDir, + (string) $resolved, + "project/$name must stage inside the job dir" + ); + $this->assertNotSame( + $this->workdir.DIRECTORY_SEPARATOR.$name, + $resolved, + "project/$name must never be written straight to the install root" + ); + } } public function test_extensionDestinationMap_rejects_hostile_relative(): void diff --git a/tests/Unit/ProjectReconcilerTest.php b/tests/Unit/ProjectReconcilerTest.php new file mode 100644 index 0000000..48d94b1 --- /dev/null +++ b/tests/Unit/ProjectReconcilerTest.php @@ -0,0 +1,217 @@ +base = sys_get_temp_dir().DIRECTORY_SEPARATOR.'bkp_pr_'.bin2hex(random_bytes(4)); + $this->staging = $this->base.DIRECTORY_SEPARATOR.'staging'; + @mkdir($this->staging, 0777, true); + + $this->reconciler = new ProjectReconciler(new Paths([ + 'base' => $this->base, + 'public' => $this->base, + 'storage' => $this->base, + ])); + } + + protected function tearDown(): void + { + $this->rrmdir($this->base); + } + + public function test_merge_keeps_packages_the_backup_never_knew_about(): void + { + $this->writeDestManifest([ + 'require' => [ + 'php' => '^8.3', + 'flarum/core' => '^2.0', + 'fof/redis' => '^1.2', + 'predis/predis' => '^2.0', + 'guzzlehttp/guzzle' => '^7.9.2', + ], + ]); + $this->writeIncomingManifest([ + 'require' => [ + 'php' => '^8.3', + 'flarum/core' => '^2.0', + 'guzzlehttp/guzzle' => '^7.0', + 'ramon/marketplace' => '^1.0', + ], + ]); + + $result = $this->reconciler->reconcile($this->staging, true, false); + + $merged = $this->readDestManifest(); + + $this->assertArrayHasKey('fof/redis', $merged['require'], 'fof/redis must survive the restore'); + $this->assertArrayHasKey('predis/predis', $merged['require']); + $this->assertArrayHasKey('ramon/marketplace', $merged['require'], 'the backup\'s new requires must be added'); + $this->assertSame('^7.9.2', $merged['require']['guzzlehttp/guzzle'], 'a stricter local pin must win over the backup'); + $this->assertContains('composer.json', $result['applied']); + } + + public function test_lock_from_another_server_is_never_written(): void + { + $this->writeDestManifest(['require' => ['flarum/core' => '^2.0']]); + $this->writeIncomingManifest(['require' => ['flarum/core' => '^2.0']]); + file_put_contents($this->staging.DIRECTORY_SEPARATOR.'composer.lock', '{"packages":[]}'); + + $this->reconciler->reconcile($this->staging, true, false); + + $this->assertFileDoesNotExist( + $this->base.DIRECTORY_SEPARATOR.'composer.lock', + 'Applying a foreign lock is what prunes packages that only exist here' + ); + } + + public function test_missing_packages_are_reported_even_when_nothing_is_applied(): void + { + $this->writeDestManifest(['require' => ['flarum/core' => '^2.0']]); + $this->writeIncomingManifest([ + 'require' => ['flarum/core' => '^2.0', 'fof/nightmode' => '^1.0'], + ]); + + $result = $this->reconciler->reconcile($this->staging, false, false); + + $this->assertNotEmpty($result['warnings']); + $this->assertStringContainsString('fof/nightmode', implode(' ', $result['warnings'])); + $this->assertSame([], $result['applied'], 'apply=false must not touch the manifest'); + } + + public function test_fresh_server_without_a_manifest_gets_both_files_verbatim(): void + { + $this->writeIncomingManifest(['require' => ['flarum/core' => '^2.0']]); + file_put_contents($this->staging.DIRECTORY_SEPARATOR.'composer.lock', '{"packages":[]}'); + + $result = $this->reconciler->reconcile($this->staging, true, false); + + $this->assertFileExists($this->base.DIRECTORY_SEPARATOR.'composer.json'); + $this->assertFileExists($this->base.DIRECTORY_SEPARATOR.'composer.lock'); + $this->assertContains('composer.lock', $result['applied']); + } + + public function test_root_extend_is_only_written_when_opted_in_and_keeps_a_backup(): void + { + $original = "base.DIRECTORY_SEPARATOR.'extend.php', $original); + file_put_contents($this->staging.DIRECTORY_SEPARATOR.'extend.php', "reconciler->reconcile($this->staging, false, false); + $this->assertSame($original, $this->read($this->base.DIRECTORY_SEPARATOR.'extend.php')); + + $this->reconciler->reconcile($this->staging, false, true); + $this->assertStringContainsString( + 'from-backup', + $this->read($this->base.DIRECTORY_SEPARATOR.'extend.php') + ); + $this->assertNotEmpty( + glob($this->base.DIRECTORY_SEPARATOR.'extend.php.bak-*'), + 'The replaced extend.php must be recoverable' + ); + } + + public function test_dangling_class_in_root_extend_is_detected(): void + { + file_put_contents($this->base.DIRECTORY_SEPARATOR.'extend.php', <<<'PHP' + '127.0.0.1'])) + ->useDatabaseWith('cache', 1), + ]; + PHP); + + $dangling = $this->reconciler->danglingClassReferences(); + + $this->assertContains('FoF\Redis\Extend\Redis', $dangling); + } + + public function test_resolvable_classes_are_not_reported(): void + { + file_put_contents($this->base.DIRECTORY_SEPARATOR.'extend.php', <<<'PHP' + assertSame([], $this->reconciler->danglingClassReferences()); + } + + /** @param array $manifest */ + private function writeDestManifest(array $manifest): void + { + file_put_contents( + $this->base.DIRECTORY_SEPARATOR.'composer.json', + (string) json_encode($manifest, JSON_PRETTY_PRINT) + ); + } + + /** @param array $manifest */ + private function writeIncomingManifest(array $manifest): void + { + file_put_contents( + $this->staging.DIRECTORY_SEPARATOR.'composer.json', + (string) json_encode($manifest, JSON_PRETTY_PRINT) + ); + } + + /** @return array */ + private function readDestManifest(): array + { + return (array) json_decode($this->read($this->base.DIRECTORY_SEPARATOR.'composer.json'), true); + } + + /** + * Lê uma fixture criada pelo próprio teste. O caminho sai de + * `$this->base` / `$this->staging`, nunca de input — o alerta de SSRF + * do semgrep é falso positivo e fica suprimido num ponto só. + */ + private function read(string $path): string + { + $raw = @file_get_contents($path); /* fixture local do teste; nosemgrep: flarum-v2-server-side-fetch */ + return $raw === false ? '' : $raw; + } + + private function rrmdir(string $dir): void + { + if (! is_dir($dir)) { + @unlink($dir); + return; + } + foreach (scandir($dir) ?: [] as $e) { + if ($e === '.' || $e === '..') continue; + $this->rrmdir($dir.DIRECTORY_SEPARATOR.$e); + } + @rmdir($dir); + } +}