"Ошибка: Options Page Not Found" при отправке страницы настроек для OOP плагина

30 мар. 2014 г., 08:36:44
Просмотры: 30.1K
Голосов: 20

Я разрабатываю плагин, используя репозиторий Tom McFarlin's Boilerplate в качестве шаблона, который использует ООП практики. Я пытаюсь понять, почему я не могу корректно отправить мои настройки. Я пробовал установить атрибут action в пустую строку, как предлагалось в другом вопросе, но это не помогло...

Ниже приведена общая структура кода, которую я использую...

Форма (/views/admin.php):

<div class="wrap">
    <h2><?php echo esc_html( get_admin_page_title() ); ?></h2>
    <form action="options.php" method="post">
        <?php
        settings_fields( $this->plugin_slug );
        do_settings_sections( $this->plugin_slug );
        submit_button( 'Save Settings' );
        ?>
    </form>
</div>

Для следующего кода предполагается, что все обратные вызовы для add_settings_field() и add_settings_section() существуют, кроме 'option_list_selection'.

Класс администратора плагина (/{plugin_name}-class-admin.php):

namespace wp_plugin_name;

class Plugin_Name_Admin
{
    /**
     * Примечание: Некоторые части кода класса и функций методов отсутствуют для краткости
     * Дайте знать, если вам нужна дополнительная информация...
     */

    private function __construct()
    {
        $plugin              = Plugin_Name::get_instance();

        $this->plugin_slug   = $plugin->get_plugin_slug();
        $this->friendly_name = $plugin->get_name(); // Получить "удобное для человека" отображаемое имя

        // Добавляет все опции для административных настроек
        add_action( 'admin_init', array( $this, 'plugin_options_init' ) );

        // Добавить страницу опций и пункт меню
        add_action( 'admin_menu', array( $this, 'add_plugin_admin_menu' ) );


    }

    public function add_plugin_admin_menu()
    {

        // Добавить страницу опций
        $this->plugin_screen_hook_suffix =
        add_options_page(
            __( $this->friendly_name . " Options", $this->plugin_slug ),
            __( $this->friendly_name, $this->plugin_slug ),
            "manage_options", 
            $this->plugin_slug,
            array( $this, "display_plugin_admin_page" )
        );

    }

    public function display_plugin_admin_page()
    {
        include_once( 'views/admin.php' );
    }

    public function plugin_options_init()
    {
        // Обновить настройки
        add_settings_section(
            'maintenance',
            'Maintenance',
            array( $this, 'maintenance_section' ),
            $this->plugin_slug
        );

        // Опция проверки обновлений
        register_setting( 
            'maintenance',
            'plugin-name_check_updates',
            'wp_plugin_name\validate_bool'
        );

        add_settings_field(
            'check_updates',
            'Should ' . $this->friendly_name . ' Check For Updates?',
            array( $this, 'check_updates_field' ),
            $this->plugin_slug,
            'maintenance'
        );

        // Опция периода обновления
        register_setting(
            'maintenance',
            'plugin-name_update_period',
            'wp_plugin_name\validate_int'
        );

        add_settings_field(
            'update_frequency',
            'How Often Should ' . $this->friendly_name . ' Check for Updates?',
            array( $this, 'update_frequency_field' ),
            $this->plugin_slug,
            'maintenance'
        );

        // Конфигурации опций плагина
        add_settings_section(
            'category-option-list', 'Widget Options List',
            array( $this, 'option_list_section' ),
            $this->plugin_slug
        );
    }
}

Некоторые запрошенные обновления:

Изменение атрибута action на:

<form action="../../options.php" method="post">

...просто приводит к ошибке 404. Ниже приведена выдержка из логов Apache. Обратите внимание, что стандартные скрипты WordPress и CSS en-queues удалены:

# Changed to ../../options.php
127.0.0.1 - - [01/Apr/2014:15:59:43 -0400] "GET /wp-admin/options-general.php?page=pluginname-widget HTTP/1.1" 200 18525
127.0.0.1 - - [01/Apr/2014:15:59:43 -0400] "GET /wp-content/plugins/PluginName/admin/assets/css/admin.css?ver=0.1.1 HTTP/1.1" 304 -
127.0.0.1 - - [01/Apr/2014:15:59:43 -0400] "GET /wp-content/plugins/PluginName/admin/assets/js/admin.js?ver=0.1.1 HTTP/1.1" 304 -
127.0.0.1 - - [01/Apr/2014:15:59:52 -0400] "POST /options.php HTTP/1.1" 404 1305
127.0.0.1 - - [01/Apr/2014:16:00:32 -0400] "POST /options.php HTTP/1.1" 404 1305

#Changed to options.php
127.0.0.1 - - [01/Apr/2014:16:00:35 -0400] "GET /wp-admin/options-general.php?page=pluginname-widget HTTP/1.1" 200 18519
127.0.0.1 - - [01/Apr/2014:16:00:35 -0400] "GET /wp-content/plugins/PluginName/admin/assets/css/admin.css?ver=0.1.1 HTTP/1.1" 304 -
127.0.0.1 - - [01/Apr/2014:16:00:35 -0400] "GET /wp-content/plugins/PluginName/admin/assets/js/admin.js?ver=0.1.1 HTTP/1.1" 304 -
127.0.0.1 - - [01/Apr/2014:16:00:38 -0400] "POST /wp-admin/options.php HTTP/1.1" 500 2958

Оба файла php-errors.log и debug.log при включенном WP_DEBUG пусты.

Класс плагина (/{plugin-name}-class.php)

namespace wp_plugin_name;

class Plugin_Name
{
    const VERSION = '1.1.2';
    const TABLE_VERSION = 1;
    const CHECK_UPDATE_DEFAULT = 1;
    const UPDATE_PERIOD_DEFAULT = 604800;

    protected $plugin_slug = 'pluginname-widget';
    protected $friendly_name = 'PluginName Widget';

    protected static $instance = null;

    private function __construct()
    {

        // Загрузить текстовый домен плагина
        add_action( 'init',
                    array(
            $this,
            'load_plugin_textdomain' ) );

        // Активировать плагин при добавлении нового блога
        add_action( 'wpmu_new_blog',
                    array(
            $this,
            'activate_new_site' ) );

        // Загрузить публичные стили и JavaScript.
        add_action( 'wp_enqueue_scripts',
                    array(
            $this,
            'enqueue_styles' ) );
        add_action( 'wp_enqueue_scripts',
                    array(
            $this,
            'enqueue_scripts' ) );

        /* Определить пользовательскую функциональность.
         * Обратиться к http://codex.wordpress.org/Plugin_API#Hooks.2C_Actions_and_Filters
         */

    }

    public function get_plugin_slug()
    {
        return $this->plugin_slug;
    }

    public function get_name()
    {
        return $this->friendly_name;
    }

    public static function get_instance()
    {

        // Если единственный экземпляр не был установлен, установить его сейчас.
        if ( null == self::$instance )
        {
            self::$instance = new self;
        }

        return self::$instance;

    }

    /**
     * Функции-члены activate(), deactivate(), и update() очень похожи.
     * См. плагин Boilerplate для получения более подробной информации...
     *
     */

    private static function single_activate()
    {
        if ( !current_user_can( 'activate_plugins' ) )
            return;

        $plugin_request = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';

        check_admin_referer( "activate-plugin_$plugin_request" );

        /**
         *  Проверка, является ли это новой установкой
         */
        if ( get_option( 'plugin-name_version' ) === false )
        {
            // Получить время как временную метку Unix и добавить одну неделю
            $unix_time_utc = time() + Plugin_Name::UPDATE_PERIOD_DEFAULT;

            add_option( 'plugin-name_version', Plugin_Name::VERSION );
            add_option( 'plugin-name_check_updates',
                        Plugin_Name::CHECK_UPDATE_DEFAULT );
            add_option( 'plugin-name_update_frequency',
                        Plugin_Name::UPDATE_PERIOD_DEFAULT );
            add_option( 'plugin-name_next_check', $unix_time_utc );

            // Создать таблицу опций
            table_update();

            // Сообщить пользователю, что PluginName был установлен успешно
            is_admin() && add_filter( 'gettext', 'finalization_message', 99, 3 );
        }
        else
        {
            // Сообщить пользователю, что PluginName был активирован успешно
            is_admin() && add_filter( 'gettext', 'activate_message', 99, 3 );
        }

    }

    private static function single_update()
    {
        if ( !current_user_can( 'activate_plugins' ) )
            return;

        $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';

        check_admin_referer( "activate-plugin_{$plugin}" );

        $cache_plugin_version         = get_option( 'plugin-name_version' );
        $cache_table_version          = get_option( 'plugin-name_table_version' );
        $cache_deferred_admin_notices = get_option( 'plugin-name_admin_messages',
                                                    array() );

        /**
         * Выяснить, какую версию нашего плагина мы запускаем, и сравнить ее с нашей
         * определенной здесь версией
         */
        if ( $cache_plugin_version > self::VERSION )
        {
            $cache_deferred_admin_notices[] = array(
                'error',
                "Похоже, вы пытаетесь вернуться к более старой версии " . $this->get_name() . ". Возврат через функцию обновления не поддерживается."
            );
        }
        else if ( $cache_plugin_version === self::VERSION )
        {
            $cache_deferred_admin_notices[] = array(
                'updated',
                "Вы уже используете последнюю версию " . $this->get_name() . "!"
            );
            return;
        }

        /**
         * Если мы не можем определить, на какой версии находится таблица, обновить ее...
         */
        if ( !is_int( $cache_table_version ) )
        {
            update_option( 'plugin-name_table_version', TABLE_VERSION );
            table_update();
        }

        /**
         * В противном случае, мы просто проверим, требуется ли обновление
         */
        else if ( $cache_table_version < TABLE_VERSION )
        {
            table_update();
        }

        /**
         * Таблица не нуждалась в обновлении.
         * Обратите внимание, что мы не можем обновить другие опции, потому что мы не можем предполагать, что они все еще
         * имеют значения по умолчанию для нашего плагина... (если только мы не сохранили их в базе данных)
         */

    }

    private static function single_deactivate()
    {

        // Определить, имеет ли текущий пользователь соответствующие разрешения
        if ( !current_user_can( 'activate_plugins' ) )
            return;

        // Есть ли данные запроса?
        $plugin = isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : '';

        // Проверить, был ли nonce действительным
        check_admin_referer( "deactivate-plugin_{$plugin}" );

        // Ну, технически плагин не включен при деактивации, так что...
        // Ничего не делать

    }

    public function load_plugin_textdomain()
    {

        $domain = $this->plugin_slug;
        $locale = apply_filters( 'plugin_locale', get_locale(), $domain );

        load_textdomain( $domain,
                         trailingslashit( WP_LANG_DIR ) . $domain . '/' . $domain . '-' . $locale . '.mo' );
        load_plugin_textdomain( $domain, FALSE,
                                basename( plugin_dir_path( dirname( __FILE__ ) ) ) . '/languages/' );

    }

    public function activate_message( $translated_text, $untranslated_text,
                                      $domain )
    {
        $old = "Plugin <strong>activated</strong>.";
        $new = FRIENDLY_NAME . " был <strong>успешно активирован</strong> ";

        if ( $untranslated_text === $old )
            $translated_text = $new;

        return $translated_text;

    }

    public function finalization_message( $translated_text, $untranslated_text,
                                          $domain )
    {
        $old = "Plugin <strong>activated</strong>.";
        $new = "Капитан, Ядро стабильно и PluginName был <strong>успешно установлен</strong> и готов к Варп-скорости";

        if ( $untranslated_text === $old )
            $translated_text = $new;

        return $translated_text;

    }

}

Ссылки:

7
Комментарии

В описании баунти указано: "Пожалуйста, предоставьте информацию о лучших практиках". Использование синглтонов с приватными конструкторами и множеством действий внутри них: плохая практика и сложность тестирования, но это не ваша вина.

gmazzap gmazzap
1 апр. 2014 г. 10:25:37

используйте ../../options.php после тестирования вашего кода.

Ravi Patel Ravi Patel
1 апр. 2014 г. 12:53:38

Пожалуйста, покажите get_plugin_slug().

vancoder vancoder
1 апр. 2014 г. 21:17:51

@vancoder Я отредактировал пост выше, добавив соответствующую информацию...

gate_engineer gate_engineer
1 апр. 2014 г. 23:22:42

Почему в ваших callback-функциях санитизации в register_settings есть обратные слеши? Не думаю, что это будет работать.

Bjorn Bjorn
2 апр. 2014 г. 00:46:39

Я разместил эти callback-функции санитизации в пространстве имен, однако это то же пространство имен, что и указано для каждого класса выше. Я удалю их и посмотрю, в этом ли проблема.

gate_engineer gate_engineer
2 апр. 2014 г. 01:33:16

По-прежнему получаю то же самое. Хочу добавить, что я встраиваю разделы в свою форму на той же странице. Интересно, может ли это влиять...

gate_engineer gate_engineer
2 апр. 2014 г. 01:48:07
Показать остальные 2 комментариев
Все ответы на вопрос 5
2
21

Ошибка: "Страница настроек не найдена"

Это известная проблема в WP Settings API. Тикет был создан несколько лет назад и помечен как решенный, но ошибка сохраняется в последних версиях WordPress. Вот что говорилось (теперь удалено) на странице Codex:

Проблема "Error: options page not found." (включая решение и объяснение):

Проблема заключается в том, что фильтр 'whitelist_options' не получает правильный индекс для ваших данных. Он применяется в options.php#98 (WP 3.4).

register_settings() добавляет ваши данные в глобальную переменную $new_whitelist_options. Затем она объединяется с глобальной переменной $whitelist_options внутри коллбэков option_update_filter() (соответственно add_option_whitelist()). Эти коллбэки добавляют ваши данные в глобальную переменную $new_whitelist_options с индексом $option_group.

Когда вы сталкиваетесь с "Error: options page not found.", это означает, что ваш индекс не был распознан. Вводит в заблуждение то, что первый аргумент используется как индекс и называется $options_group, тогда как фактическая проверка в options.php#112 происходит против $options_page, который является $hook_suffix, возвращаемым значением из add_submenu_page().

Короче говоря, простое решение — сделать так, чтобы $option_group совпадал с $option_name. Другой причиной этой ошибки может быть неверное значение параметра $page при вызове либо add_settings_section( $id, $title, $callback, $page ), либо add_settings_field( $id, $title, $callback, $page, $section, $args ).

Подсказка: $page должен совпадать с $menu_slug из Function Reference/add theme page.

Простое решение

Использование имени пользовательской страницы (в вашем случае: $this->plugin_slug) в качестве идентификатора раздела обойдет проблему. Однако все ваши настройки должны быть в одном разделе.

Решение

Для более надежного решения внесите следующие изменения в ваш класс Plugin_Name_Admin:

Добавьте в конструктор:

// Отслеживает новые разделы для whitelist_custom_options_page()
$this->page_sections = array();
// Должен выполняться после `option_update_filter()` WP, поэтому приоритет > 10
add_action( 'whitelist_options', array( $this, 'whitelist_custom_options_page' ),11 );

Добавьте эти методы:

// Белый список настроек на пользовательских страницах.
// Обходное решение для второй проблемы: http://j.mp/Pk3UCF
public function whitelist_custom_options_page( $whitelist_options ){
    // Пользовательские настройки сопоставляются по ID раздела; Пересопоставление по slug страницы.
    foreach($this->page_sections as $page => $sections ){
        $whitelist_options[$page] = array();
        foreach( $sections as $section )
            if( !empty( $whitelist_options[$section] ) )
                foreach( $whitelist_options[$section] as $option )
                    $whitelist_options[$page][] = $option;
            }
    return $whitelist_options;
}

// Обертка для `add_settings_section()` WP, которая отслеживает пользовательские разделы
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]))
            $this->page_sections[$page] = array();
        $this->page_sections[$page][$id] = $id;
    }
}

И измените вызовы add_settings_section() на: $this->add_settings_section().


Другие замечания по вашему коду

  • Ваш код формы правильный. Форма должна отправляться на options.php, как указано мне @Chris_O и как указано в документации WP Settings API.
  • Пространства имен имеют свои преимущества, но они могут усложнить отладку и снизить совместимость вашего кода (требуется PHP>=5.3, другие плагины/темы, использующие автозагрузчики и т.д.). Поэтому, если нет веской причины использовать пространства имен, не делайте этого. Вы уже избегаете конфликтов имен, обернув код в класс. Сделайте имена классов более специфичными и перенесите коллбэки validate() в класс как публичные методы.
  • Сравнивая указанный шаблон плагина с вашим кодом, похоже, ваш код основан на форке или старой версии шаблона. Даже имена файлов и пути отличаются. Вы можете перенести ваш плагин на последнюю версию, но учтите, что этот шаблон может не подходить для ваших нужд. Он использует синглтоны, которые обычно не рекомендуются. Есть случаи, когда шаблон синглтона уместен, но это должно быть осознанным решением, а не стандартным решением.
2 апр. 2014 г. 22:45:43
Комментарии

Приятно знать, что в API есть баг. Я всегда стараюсь проверять написанный мной код на наличие ошибок, которые мог допустить. Конечно, это предполагает, что я хоть что-то понимаю в этом.

gate_engineer gate_engineer
3 апр. 2014 г. 00:23:06

Для тех, кто столкнулся с этой проблемой: ознакомьтесь с примером ООП в кодексе: https://codex.wordpress.org/Creating_Options_Pages#Example_.232

maysi maysi
31 янв. 2019 г. 00:34:06
1

Я только что нашел этот пост, ища решение той же проблемы. Решение гораздо проще, чем кажется, потому что документация вводит в заблуждение: в register_setting() первый аргумент с именем $option_group — это slug вашей страницы, а не раздела, в котором вы хотите отобразить настройку.

В приведенном выше коде следует использовать:

    // Обновление настроек
    add_settings_section(
        'maintenance', // slug раздела
        'Обслуживание', // заголовок раздела
        array( $this, 'maintenance_section' ), // callback-функция для отображения раздела
        $this->plugin_slug // slug страницы
    );

    // Опция проверки обновлений
    register_setting( 
        $this->plugin_slug, // slug страницы, а не slug раздела
        'plugin-name_check_updates', // slug настройки
        'wp_plugin_name\validate_bool' // неверно, должен быть массив опций, см. документацию для подробностей
    );

    add_settings_field(
        'plugin-name_check_updates', // slug настройки
        'Должен ли ' . $this->friendly_name . ' проверять обновления?', // заголовок настройки
        array( $this, 'check_updates_field' ), // callback-функция для отображения настройки
        $this->plugin_slug, // slug страницы
        'maintenance' // slug раздела
    );
27 окт. 2017 г. 02:37:59
Комментарии

Это неверно. Пожалуйста, посмотрите этот рабочий пример (не мой) - https://gist.github.com/annalinneajohansson/5290405

Xdg Xdg
3 сент. 2018 г. 21:32:37
0

При регистрации страницы настроек с помощью:

add_submenu_page( string $parent_slug, string $page_title, string $menu_title, string $capability, string $menu_slug, callable $function = '' )

И при регистрации настроек с помощью:

register_setting( string $option_group, string $option_name );

Параметр $option_group должен совпадать с $menu_slug

6 окт. 2018 г. 21:49:20
1

Я тоже столкнулся с этой проблемой уже несколько дней назад. Ошибка перестала появляться, когда я закомментировал строку:

// settings_fields($this->plugin_slug);

После этого я делаю редирект на options.php, но пока не могу решить проблему с setting_fields.

15 февр. 2019 г. 11:07:32
Комментарии

Я исправил это в функции валидации!! ;)

G.Karles G.Karles
15 февр. 2019 г. 14:30:36
0

У меня была такая же ошибка, но я получил её другим способом:

// нет реального кода
// это не сработало
add_settings_field('id','title', /*callback*/ function($arguments) {
    // echo $htmlcode; 
    register_setting('option_group', 'option_name');
}), 'page', 'section');

Я не знаю, почему это произошло, но похоже, что register_setting не должен находиться в callback-функции для add_settings_field

// нет реального кода
// это сработало
add_settings_field('id','title', /*callback*/ function($arguments) {echo $htmlcode;}), 'page', 'section');
register_setting('option_group', 'option_name');

Надеюсь, это поможет

26 февр. 2019 г. 12:35:27