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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions