Skip to content

Make an encrypted export importable again - #735

Merged
blaipr merged 1 commit into
mainfrom
fix/encrypted-export-can-be-imported
Aug 13, 2026
Merged

Make an encrypted export importable again#735
blaipr merged 1 commit into
mainfrom
fix/encrypted-export-can-be-imported

Conversation

@blaipr

@blaipr blaipr commented Aug 13, 2026

Copy link
Copy Markdown
Member

The bug

An encrypted export could not be restored. Not "failed with an error" — it died on an uncaught PHP fatal, for any export containing so much as one category, client, tag or account. That is every real backup anybody has taken.

SyspassImport::processEncrypted() parsed the decrypted fragment into a DOMDocument without setting preserveWhiteSpace = false — unlike XmlFile, which does set it for the file itself. XmlExport writes with formatOutput = true, so the fragment is pretty-printed: the indentation arrives as DOMText nodes and is grafted into the main tree by importNode(…, true).

The first one then reaches a CallbackFilterIterator callback typed DOMElement $element, and the TypeError that raises is an Error, not an Exception — so neither SyspassImport::doImport()'s catch (Exception) nor the repository transaction's catches it.

Reproduced standalone before touching anything:

TypeError: {closure}(): Argument #1 ($e) must be of type DOMElement, DOMText given

The fix

One line: the same preserveWhiteSpace = false the unencrypted path has always had.

The test that found it

A new export→import round trip, which is the property neither half's tests covered: both were tested against themselves, so nothing said what the export writes is what the import can read.

It creates a category, a client, two tags and an account with a real password through the real services, exports, imports back, and requires every field to match — name, login, url, notes, category and client by name, tags, and the decrypted password. Not "no exception was thrown".

Three cases: plain, encrypted with the right password, and encrypted with the wrong one (still refused cleanly). The encrypted case fails against the old code with exactly the TypeError above.

It builds a real container against a real database by hand, the way CliTestCase does, because IntegrationTestCase mocks the database away and nothing would actually persist to read back.

Testing

Integration suite green: 855. Import unit tests: 54. PHPStan and PHPCS clean.

An encrypted export could not be restored. Not "failed with an error" — it died
on a PHP fatal, for any export containing so much as one category, which is
every real backup anybody has taken.

The decrypted fragment was parsed into a document that had not been told to drop
whitespace, unlike the one XmlFile builds for the file itself, and the export is
written pretty-printed. So the indentation arrived as text nodes and was grafted
into the main tree; the first one reached a callback typed for elements, and the
TypeError that raised is an Error, which neither the importer's own catch nor
the transaction's catches.

One line, matching what the unencrypted path has always done.

Comes with the test that found it: an account, its category, client and tags are
created through the real services, exported, imported back, and every field —
including the decrypted password — is required to match. Plain and encrypted
both, and a wrong password still refused.
@blaipr
blaipr merged commit 2e5cae6 into main Aug 13, 2026
8 checks passed
@blaipr
blaipr deleted the fix/encrypted-export-can-be-imported branch August 13, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant