1

I'm struggling to get tabbed plugin settings, created with WordPress Settings API to work.

The first tab saves settings correctly. However, every time when I click on "Save Changes" button for any other tab, a white screen with the following error appears:

ERROR: Options page not found

Chrome and Apache log show POST 500 error. I see that form data to submit appears to be correct and according to what is submitted for the first tab that works general_section :

option_page: account_section
action: update
_wpnonce: 2d40b0a357
_wp_http_referer: /staging/wp-admin/options-general.php?page=woo-extra-settings&tab=account
eswc_settingz[eswc_redirect]: 1
submit: Save Changes

The fields are registered only when the proper tab is selected to avoid resetting values in other tabs to defaults on save.

The code from this topic is included in my example, but it didn't fix the problem:

"Error: Options Page Not Found" on Settings Page Submission for an OOP Plugin

I used PHP console extension for Chrome and a plugin to print values to Chrome console for troubleshooting, for example:

PC::debug(  'general', 'tab name' );

This is now commented. The code can be used as a minimal part of my plugin to generate a 2-tab settings page in the default "Settings" menu and reproduce the issue that I'm having.

On tab activation, the console returns the following values (whitelist_custom_options_page function doesn't execute):

"General" (1st tab):

tab name:  general
add settings section - page:  woo-extra-settings
add settings section - id:  general_section

"Account" (2nd tab)

tab name:  account
add settings section - page:  woo-extra-settings
add settings section - id:  account_section

On settings save, the values are:

"General" (1st tab):

tab name:  general
add settings section - page:  woo-extra-settings
add settings section - id:  general_section
whitelist page:  woo-extra-settings
whitelist section:  general_section
whitelist option:  eswc_settingz

For "Account" (2nd tab) the values are all the same, which means that on saving the settings, my code incorrectly perceives any tab as 1st tab, what is the reason for the problem that I have.

How can I fix this issue in a way that is close to my current implementation?

Here is the code:

class extra_settings_settings {

private $options;
private $settings_page_name;
private $settings_menu_name;

public function __construct() {

    if ( is_admin() ) {

        $this->settings_page_name = 'woo-extra-settings';
        $this->settings_menu_name = 'Woo Extra Settings';

        // Tracks new sections for whitelist_custom_options_page()
        $this->page_sections = array();
        // Must run after option_update_filter(), so priority > 10
        add_action( 'whitelist_options', array( $this, 'whitelist_custom_options_page' ),11 );          

        // Initialize and register settings. 
        add_action( 'admin_init', array( $this, 'display_options' ) );
        // Add settings page.
        add_action( 'admin_menu', array( $this, 'add_settings_page' ) );    

    }
}

public function display_options() {
    $active_tab = isset( $_GET[ 'tab' ] ) ? $_GET[ 'tab' ] : 'general';
    if ( $active_tab == 'general' ) {
        //PC::debug(  'general', 'tab name:' );
        register_setting( 'general_section', 'eswc_settingz', array( $this, 'sanitize_general' ) );
        // ID / title / cb / page       
        $this->add_settings_section( 'general_section', null, array( $this, 'general_section_cb' ), $this->settings_page_name );
        // ID / title / cb / page / section /args
        add_settings_field( 'eswc_kill_gutenberg', __( 'Gutenberg', 'extra-settings-for-woocommerce' ), array( $this, 'eswc_kill_gutenberg_cb' ), $this->settings_page_name, 'general_section' );
        add_settings_field( 'eswc_change_email_author', __( 'WP core email author', 'extra-settings-for-woocommerce' ), array( $this, 'eswc_change_email_author_cb' ), $this->settings_page_name, 'general_section' );
    } else if ( $active_tab == 'account' ) {
        //PC::debug(  'account', 'tab name:' );
        register_setting( 'account_section', 'eswc_settingz', array( $this, 'sanitize_account' ) );
        $this->add_settings_section( 'account_section', null, array( $this, 'account_section_cb' ), $this->settings_page_name );
        add_settings_field( 'eswc_redirect', __( 'Login redirect', 'extra-settings-for-woocommerce' ), array( $this, 'eswc_redirect_cb' ), $this->settings_page_name, 'account_section' );      
    }
}

  public function add_settings_page() {
    // This page will be under "Settings"
    $this->plugin_hook_suffix = add_options_page(
        'Settings Admin', $this->settings_menu_name, 'manage_options', $this->settings_page_name, array( $this, 'create_settings_page' )
    );
}

// Wrapper for wp's `add_settings_section()` that tracks custom sections
private function add_settings_section( $id, $title, $cb, $page ){
    add_settings_section( $id, $title, $cb, $page );                
    if( $id != $page ){
        if( !isset($this->page_sections[$page]))
            //PC::debug(  $page, 'add settings section - page:' );
            //PC::debug(  $id, 'add settings section - id:' );  
            $this->page_sections[$page] = array();
        $this->page_sections[$page][$id] = $id;     
    }
}   

// White-lists options on custom pages.
public function whitelist_custom_options_page( $whitelist_options ){
    // Custom options are mapped by section id; Re-map by page slug.
    foreach($this->page_sections as $page => $sections ){           
        //PC::debug(  $page, 'whitelist page:' );           
        $whitelist_options[$page] = array();
        foreach( $sections as $section )            
            //PC::debug(  $section, 'whitelist section:' );         
            if( !empty( $whitelist_options[$section] ) )
                foreach( $whitelist_options[$section] as $option )
                    $whitelist_options[$page][] = $option;                      
                    //PC::debug(  $option, 'whitelist option:' );
            }
    return $whitelist_options;
}

/**
 * Get the option that is saved or the default.
 *
 * @param string $index. The option we want to get.
 */
public function eswc_get_settings( $index = false ) {

    $defaults = array ( 'eswc_kill_gutenberg' => false, 'eswc_change_email_author' => false, 'eswc_redirect' => false);
    $settings = get_option( 'eswc_settingz', $defaults );

    if ( $index && isset( $settings[ $index ] ) ) {
        return $settings[ $index ];
    }

    return $settings;
}

public function create_settings_page() {

    $this->options = $this->eswc_get_settings();

    ?>
    <div class="wrap">
        <h1>Extra Settings For WooCommerce</h1>
        <?php
        $active_tab = isset( $_GET[ 'tab' ] ) ? $_GET[ 'tab' ] : 'general';         
        ?>
        <h2 class="nav-tab-wrapper">
            <a href="?page=woo-extra-settings&tab=general" class="nav-tab <?php echo $active_tab == 'general' ? 'nav-tab-active' : ''; ?>">General</a>
            <a href="?page=woo-extra-settings&tab=account" class="nav-tab <?php echo $active_tab == 'account' ? 'nav-tab-active' : ''; ?>">Account</a>          
        <form method="post" action="options.php">
        <?php
            // Output nonce, action, and option_page fields for a settings page.
            if( $active_tab == 'general' ) {
                settings_fields( 'general_section' );               
                echo '<table class="form-table">';
                do_settings_fields ($this->settings_page_name, 'general_section');
                echo '</table>';
            } else if ( $active_tab == 'account' ){
                settings_fields( 'account_section' );               
                echo '<table class="form-table">';
                do_settings_fields ($this->settings_page_name, 'account_section');
                echo '</table>';
            }

            submit_button();
        ?>
        </form>
    </div>
    <?php
}

/**
 * Sanitize each setting field as needed
 *
 * @param array $input Contains all settings fields as array keys
 */
public function sanitize_general( $input ) {
    $new_input = array();
    if( isset( $input['eswc_kill_gutenberg'] ) )
        $new_input['eswc_kill_gutenberg'] = ( $input['eswc_kill_gutenberg'] == 1 ? 1 : 0 );
    if( isset( $input['eswc_change_email_author'] ) )
        $new_input['eswc_change_email_author'] = ( $input['eswc_change_email_author'] == 1 ? 1 : 0 );       
    return $new_input;
}

public function sanitize_account( $input ) {
    $new_input = array();
    if( isset( $input['eswc_redirect'] ) )
        $new_input['eswc_redirect'] = ( $input['eswc_redirect'] == 1 ? 1 : 0 );
    return $new_input;
}

/** 
 * Get the settings option array and print one of its values
 */

public function eswc_kill_gutenberg_cb() {
    printf(
    '<fieldset>
        <label><input id="eswc_kill_gutenberg" type="checkbox" name="eswc_settingz[eswc_kill_gutenberg]" value="1" %1$s />%2$s</label>
    </fieldset>',           
    isset( $this->options['eswc_kill_gutenberg'] ) && ( 1 == $this->options['eswc_kill_gutenberg'] )  ? 'checked="checked" ':'',
    __( 'Disable Gutenberg editor. Don\'t load Gutenberg-related stylesheets from WordPress core, WooCommerce and Storefront.', 'extra-settings-for-woocommerce' )      
    );
}

public function eswc_change_email_author_cb() {
    printf(
    '<fieldset>
        <label><input id="eswc_change_email_author" type="checkbox" name="eswc_settingz[eswc_change_email_author]" value="1" %1$s />%2$s</label>
    </fieldset>',           
    isset( $this->options['eswc_change_email_author'] ) && ( 1 == $this->options['eswc_change_email_author'] )  ? 'checked="checked" ':'',
    __( 'Default is WordPress, [email protected]. Change to: "Site Title" from Settings > General, custom email entered below.', 'extra-settings-for-woocommerce' )      
    );
}   

public function eswc_redirect_cb() {
    printf(
    '<fieldset>
        <label><input id="eswc_redirect" type="checkbox" name="eswc_settingz[eswc_redirect]" value="1" %1$s />%2$s</label>
    </fieldset>
    <p class="description">%3$s</p>',           
    isset( $this->options['eswc_redirect'] ) && ( 1 == $this->options['eswc_redirect'] )  ? 'checked="checked" ':'',
    __( 'After a successful login, redirect customers to previously visited page and users with admin access to dashboard.', 'extra-settings-for-woocommerce' ),
    __( 'By default WooCommerce redirects to "My account" page. Customer roles: customer, subscriber. Admin access roles: administrator, shop_manager, editor, author, contributor.', 'extra-settings-for-woocommerce' )
    );
}   
}

$eswc_settingz_page = new extra_settings_settings();

2 Answers 2

2

First Thing

The fields are registered only when the proper tab is selected to avoid resetting values in other tabs to defaults on save.

And because of that, you should pass the proper tab name/slug to the wp-admin/options.php page which is where the form is being submitted to.

  1. You can simply pass it as a query string in the form action attribute value:

    <form method="post" action="<?php echo esc_url( add_query_arg( // wrapped for clarity
      'tab', $active_tab, admin_url( 'options.php' )
    ) ); ?>">
    
  2. Or in create_settings_page(), add a hidden input field having the tab slug as the value:

    echo '<input type="hidden" name="tab" value="' . esc_attr( $active_tab ) . '" />';
    submit_button();
    

    And then in the display_options(), retrieve the tab slug using $_REQUEST:

    $active_tab = isset( $_REQUEST['tab'] ) ? $_REQUEST['tab'] : 'general';
    

Either way, the point is so that the register_setting() calls in the display_options() are invoked. Because otherwise, WordPress would throw the "options page not found" error because the option page is not registered.

You could use the whitelist_options filter, but I'd rather want the register_setting() calls be invoked, so I removed this and the associated callback (whitelist_custom_options_page()):

add_action( 'whitelist_options', array( $this, 'whitelist_custom_options_page' ),11 );

Second Thing

Since you're using the same database option name (eswc_settingz) for all tabs/fields, you should then merge the saved data with the one being submitted:

public function sanitize_general( $input ) {
    $new_input = (array) get_option( 'eswc_settingz' ); // get saved data
    ... add/update new data
    return $new_input;
}

public function sanitize_account( $input ) {
    $new_input = (array) get_option( 'eswc_settingz' ); // get saved data
    ... add/update new data
    return $new_input;
}

Or you could use different database option names:

register_setting( 'general_section', 'eswc_general_settingz', array( $this, 'sanitize_general' ) );
register_setting( 'account_section', 'eswc_account_settingz', array( $this, 'sanitize_account' ) );

Btw, is that "z" in the settingz a typo? Maybe it was meant to be settings?...

1
  • 1
    Many thanks! eswc_settingz is the result of me being paranoid about the prefix that could be the same in other plugins. In this one, the chance is really slim, but when there are only 3 letters it gets scarier and unlike function names, I think that many plugins use simply prefix_settings or prefix_options. Commented Jul 9, 2019 at 16:29
1

The solution of Sally CJ perfectly solves the problems that I had, but one new problem appeared with this implementation.

Merging settings from the current tab with all existing database settings (eswc_settingz) in this way makes the $new_input array populated with all the settings, instead of empty as before. isset( $input['eswc_redirect'] returns false if the checkbox is unchecked. Previously it would result in an empty array item being submitted and the database would treat the checkbox as unchecked. Now as this item is already populated with database value, it must also save a 0 (unchecked) checkbox state to an array, so I added else statement:

public function sanitize_account( $input ) {
    // $new_input = (array) get_option( 'eswc_settingz' );
    $new_input = $this->eswc_get_settings();
    if( isset( $input['eswc_redirect'] ) ) {
        $new_input['eswc_redirect'] = ( $input['eswc_redirect'] == 1 ? 1 : 0 );
    } else {
        $new_input['eswc_redirect'] = 0;
    }
    return $new_input;
}

Edit:

I commented out one line and replaced it with a line below, to use my function eswc_get_settings that merges existing database settings with an array of defaults.

1
  • +1. I actually forgot about checkbox issues. :)
    – Sally CJ
    Commented Jul 9, 2019 at 23:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.