Hi TypeRocket team,
I hit a reproducible bug in the Theme Options extension (TypeRocket Professional, core ~5.1.7, PHP 8.0.28, WordPress 6.x).
SUMMARY
On the Theme Options admin page, if any other active plugin leaves the PHP superglobal $user_id populated, the Theme Options form binds to the User model instead of the Option model. Every field — text, image and especially repeaters — then renders empty, even though the data is fully intact in wp_options. Disabling the other plugin makes the values reappear.
In my case the trigger was Rank Math SEO
(includes/replace-variables/class-author-variables.php), which declares global $user_id and assigns it, leaving it set in the global scope.
ROOT CAUSE
BaseForm::autoConfigModel() (src/Elements/BaseForm.php, ~L125–166) infers the form model from ambient globals ($post, $comment, $user_id, $taxonomy...). The ThemeOptions controller calls Helper::form() with no explicit model, so it fully relies on that inference:
elseif ( isset( $user_id ) ) {
$model = Helper::appNamespace('Models\User'); // wrongly picked
}
...
else {
$model = Helper::appNamespace('Models\Option'); // correct default
}
With $user_id set, getBaseFieldValue() reads user meta instead of wp_options, so theme_options.* resolves to null. Verified at runtime: getFieldValue() reports model=App\Models\User, base=NULL while get_option('theme_options') has all keys.
STEPS TO REPRODUCE
- Use the ThemeOptions extension with some saved values (incl. repeaters).
- On an admin request, run
global $user_id; $user_id = 1; (any plugin doing this).
- Open Appearance → Theme Options → all fields render empty.
SUGGESTED FIX
Bind the option model explicitly instead of relying on ambient globals, e.g. in ThemeOptions::controller():
$form = Helper::form(new \TypeRocket\Models\WPOption)->setGroup($this->getName());
More generally, autoConfigModel() guessing from leaked superglobals is fragile on shared WP installs; consider skipping it on admin screens that aren't a post/user/term edit screen (get_current_screen() check).
MINOR
src/Core/Resolver.php:92 uses ReflectionParameter::getClass(), deprecated in PHP 8.0 and removed in 8.4. Suggest switching to $param->getType().
WORKAROUND (for reference)
add_filter('typerocket_theme_options_controller', function ($controller) {
return function ($ext) use ($controller) {
unset($GLOBALS['user_id']);
return call_user_func($controller, $ext);
};
}, 999);
Thanks for a great framework — happy to provide more details or test a patch.
Best regards,
Le Thach
Hi TypeRocket team,
I hit a reproducible bug in the Theme Options extension (TypeRocket Professional, core ~5.1.7, PHP 8.0.28, WordPress 6.x).
SUMMARY
On the Theme Options admin page, if any other active plugin leaves the PHP superglobal $user_id populated, the Theme Options form binds to the User model instead of the Option model. Every field — text, image and especially repeaters — then renders empty, even though the data is fully intact in wp_options. Disabling the other plugin makes the values reappear.
In my case the trigger was Rank Math SEO
(includes/replace-variables/class-author-variables.php), which declares
global $user_idand assigns it, leaving it set in the global scope.ROOT CAUSE
BaseForm::autoConfigModel() (src/Elements/BaseForm.php, ~L125–166) infers the form model from ambient globals ($post, $comment, $user_id, $taxonomy...). The ThemeOptions controller calls Helper::form() with no explicit model, so it fully relies on that inference:
With $user_id set, getBaseFieldValue() reads user meta instead of wp_options, so theme_options.* resolves to null. Verified at runtime: getFieldValue() reports model=App\Models\User, base=NULL while get_option('theme_options') has all keys.
STEPS TO REPRODUCE
global $user_id; $user_id = 1;(any plugin doing this).SUGGESTED FIX
Bind the option model explicitly instead of relying on ambient globals, e.g. in ThemeOptions::controller():
More generally, autoConfigModel() guessing from leaked superglobals is fragile on shared WP installs; consider skipping it on admin screens that aren't a post/user/term edit screen (get_current_screen() check).
MINOR
src/Core/Resolver.php:92 uses ReflectionParameter::getClass(), deprecated in PHP 8.0 and removed in 8.4. Suggest switching to $param->getType().
WORKAROUND (for reference)
Thanks for a great framework — happy to provide more details or test a patch.
Best regards,
Le Thach