Skip to content

Bug: Theme Options form binds to User model when global $user_id is left set (fields render empty) #268

Description

@levanthach

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

  1. Use the ThemeOptions extension with some saved values (incl. repeaters).
  2. On an admin request, run global $user_id; $user_id = 1; (any plugin doing this).
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions