28.5.5 regression: Theme Options > Backup History > Restore fails with a fatal TypeError
TITLE: 28.5.5 regression: Theme Options > Backup History > Restore fails with a fatal TypeError (still present in 28.5.7)
SUMMARY
The 28.5.5 changelog entry "Theme Options - Revisions - Restore revision - Stripping escaped backslashes" added one line to MFN_Options::_validate_options() in muffin-options/options.php. In the current 28.5.7 package that is line 1129.
The two lines immediately above it take the stored revision — a base64-encoded, serialized copy of the options blob — and turn it back into an array. The new line then passes that array through array_map, with stripslashes as the callback.
That is the defect. array_map applies the callback to each top-level element, but the options blob is not flat: a large share of its keys hold arrays, because that is how several of the theme's own field types store their values. stripslashes requires a string, so on PHP 8 the first array-valued key raises an uncaught TypeError and the request ends in a critical error. The revision is never restored.
A note on presentation: this site's firewall rejects both posts and uploads that contain PHP code — I have been blocked twice trying to submit this — which is why the code is described in prose here rather than quoted. The exact excerpts with line numbers are ready and I am happy to send them by whatever channel suits you. You may also want to know that the firewall is currently blocking bug reports about your own source.
WHY THIS IS NOT SITE-SPECIFIC
The array-valued keys are contributed by the theme's own field types, not by anything unusual in my configuration. Counting the field definitions in muffin-options/theme-options.php: color_multi about 40, typography about 36, checkbox about 34, gradient about 8, dimensions about 5, box_shadow about 4. On the options blob of an ordinary configured install I counted 803 top-level keys, of which 126 were arrays. Any site that has ever saved Theme Options will have them, so I would expect every installation to reproduce this.
STEPS TO REPRODUCE
1. On 28.5.5 or later, open Theme Options and click "Save revision" (or use any site that already has entries in Backup History).
2. Go to Theme Options > Backup History and click "Restore" on any revision.
3. The request ends in "There has been a critical error on this website." The PHP error log records an uncaught TypeError from stripslashes, reporting that argument 1 must be of type string and an array was given, at muffin-options/options.php line 1129.
EXPECTED: the selected revision is restored.
ACTUAL: fatal error, nothing restored. Theme Options are left unchanged — the exception is raised inside the sanitize callback that WordPress runs within update_option before the database write, so nothing is persisted and no data is lost. But the feature is unusable.
A SECOND, EASILY MISSED EFFECT
The failing click is not free. In muffin-options/js/options.js, revisions.restore() saves a fresh "backup" revision before it submits the restore form, guarded so that it only does so when the revision being restored is not itself a "backup" entry. set_revision() keeps a five-slot ring per revision type (options.php line 140) and evicts the oldest when full.
So a failed Restore launched from the "Update:" or "Revision:" list consumes one "Backup:" slot and delivers nothing. A Restore launched from the "Backup:" list skips the pre-write, per that guard. On a site already at the cap, a few retries — which the critical-error page naturally invites — will roll the "Backup:" ring forward to copies of the current state. The "Update:" and "Revision:" lists are separate rings and are unaffected.
To be clear, the pre-write itself is long-standing and correct behavior. It only becomes costly because the restore that follows it now always fails.
SUGGESTED FIX
Use WordPress's own recursive helper, which handles nested arrays:
stripslashes_deep
applied to the same variable, in place of the array_map line. That preserves the intent of the 28.5.5 change — revisions are stored from a slashed request context, so stripping is correct — without assuming the structure is flat.
If array_map is preferred, the callback needs a type check so non-strings pass through untouched. That avoids the fatal, but it still strips only the top level, so nested string values inside the typography and dimensions arrays would keep their slashes. The recursive helper is the closer match to the stated goal. Both forms are written out in the attached file.
ENVIRONMENT
Introduced in 28.5.5; verified byte-identical and still present in 28.5.6 and 28.5.7 (checked against the 28.5.7 package, options.php line 1129). Behavior above is PHP 8. On PHP 7, stripslashes with an array argument raises a warning and returns null rather than throwing, which would silently blank the restored value instead of erroring — arguably a worse outcome than the fatal.
While waiting for a response from one of our team members, we recommend to check Support Center where it is highly likely that you will find the answer to your question in no time.
FAQ | Video Tutorials | How to