LW IT Solutions logo LW IT Solutions logo LW IT Solutions
« Przegląd bloga /Wtyczki i triki WordPress/Tutorials / Strony ustawień wtyczki z Settings API i...
Ten artykuł w innych językach:

Strony ustawień wtyczki z Settings API i register_setting

Strony ustawień wtyczki z Settings API i register_setting

Management Summary

Strony ustawień pozwalają administratorom WordPressa zmieniać działanie rozszerzenia bez edycji jego plików programu. Artykuł wyjaśnia to na przykładzie paska komunikatu z treścią, pozycją i przełącznikiem. WordPress dostarcza gotowe elementy formularzy oraz zapisu wartości. Ułatwia to wygodne i spójne zarządzanie. Zapis musi jednak pozostać niezawodny: zmiany wymagają uprawnień, a jedna zapisana opcja nie powinna przypadkowo usuwać innych ustawień.

Spis treści
  1. Zadania poszczególnych funkcji
  2. Wspólne wartości w jednej opcji, osobne formularze w osobnych grupach
  3. Autoload: kiedy WordPress wczytuje opcję z wyprzedzeniem
  4. Wtyczka i kontrola danych wejściowych
  5. Dlaczego rejestracja jest oddzielona od układu formularza?
  6. Co dzieje się przy zapisie formularza?
  7. Reguły interfejsu /wp/v2/settings
  8. Przycisk REST bez nieaktualnego formularza
  9. Zakres testów i ograniczenia
  10. Pytania i odpowiedzi
  11. Źródła

Strona ustawień wtyczki wymaga pól formularza, kontroli uprawnień i bezpiecznego zapisu wartości. Settings API WordPressa udostępnia elementy do wykonania tych zadań. Przykład zarządza paskiem komunikatu z trzema wartościami: włączony lub wyłączony, treść oraz pozycja na stronie.

Wartości są zebrane w opcji lwsd_settings. Opcja to nazwany wpis w tabeli WordPressa wp_options. Wtyczka udostępnia zwykły formularz oraz drugi przycisk korzystający z REST API, czyli interfejsu odczytu i zapisu ustawień przez żądania HTTP. Opis dotyczy WordPressa 7.1; pierwotny przykład przetestowano z wersją 7.1.2 i PHP 8.4.

Zadania poszczególnych funkcji

WordPress uruchamia funkcje w określonych miejscach, nazywanych hookami. Na przykład admin_menu jest odpowiednim momentem na dodanie pozycji menu. Poniższy podział oddziela widoczne pola od rejestracji i zapisu danych.

FunkcjaMoment wykonaniaZadanie
add_options_page()admin_menuPozycja menu i wymagane uprawnienie
register_setting()initRejestruje opcję, oczyszczanie, wartość domyślną i reguły REST
add_settings_section(), add_settings_field()admin_initNagłówki i pola formularza w panelu
settings_fields(), do_settings_sections()W funkcji wyświetlającej stronęUkryte pola kontrolne oraz widoczne sekcje i pola

Capability oznacza pojedyncze uprawnienie użytkownika. Tutaj wymagane uprawnienie to manage_options. add_options_page() stosuje je do menu i dostępu do strony. Funkcja wyświetlająca stronę, czyli callback strony, również sprawdza je przez current_user_can(). Przy zapisie options.php ponownie kontroluje uprawnienie.

Trzy nazwy pełnią różne role: lwsd_settings to zapisana opcja, lwsd_notice_bar to grupa opcji formularza, a lwsd-notice-bar oznacza stronę administracyjną. Nazwa grupy w register_setting() musi odpowiadać settings_fields(). Nazwa strony łączy sekcje i pola z miejscem wyświetlania.

Wspólne wartości w jednej opcji, osobne formularze w osobnych grupach

Tablica łączy kilka nazwanych wartości. Przełącznik, treść i pozycja zajmują wtedy jeden wiersz bazy, zapisywany wspólnie przy zmianie. Nie oznacza to dodatkowej transakcji obejmującej dowolne odczyty i zapisy ani ochrony przed konfliktem między równocześnie edytującymi przeglądarkami.

Formularze zapisywane niezależnie wymagają osobnych grup opcji. options.php przetwarza wszystkie opcje przesłanej grupy. Dla brakującej opcji przekazuje funkcji zapisu null. Przykład: formularz A wysyła tylko opcję A, lecz A i B należą do tej samej grupy. Funkcja oczyszczająca opcję B może wtedy przywrócić domyślne wartości B. Jedna opcja tablicowa na formularz nie wystarczy; grupy także muszą być osobne.

Tablica jest automatycznie serializowana, czyli zapisywana jako jeden wspólny ciąg znaków. Poszczególne wartości nie są więc osobnymi kolumnami SQL z odpowiednimi indeksami. Do częstego wyszukiwania w bazie według pojedynczych właściwości lepiej pasują inne modele danych. Baza danych WordPressa: ograniczenia wydajności wp_postmeta i budowa własnych tabel.

Autoload: kiedy WordPress wczytuje opcję z wyprzedzeniem

Autoload oznacza wczesne wczytanie opcji razem z innymi, zanim osobne wywołanie poprosi o konkretną wartość. Ma to sens dla małej konfiguracji paska, potrzebnej na każdej zwykłej stronie witryny. Dla rzadko używanych ustawień administracyjnych zwykle lepiej pasuje false.

W add_option() wartość true jawnie ustawia w bazie on, a false ustawia off. Domyślne null od WordPressa 6.6 pozostawia decyzję regułom automatycznym. Wynikiem może być auto, auto-on lub auto-off. Bez jawnej decyzji WordPress domyślnie wyłącza autoload, gdy zserializowana wartość przekracza 150 000 bajtów. Filtry mogą zmieniać próg lub decyzję; ta reguła rozmiaru nie zastępuje jawnego true.

Aktywacja tworzy brakującą opcję przez add_option( …, true ). Nie zmienia już istniejącej opcji. Później options.php nie przekazuje osobnej wartości autoload. Przy rzeczywistej zmianie istniejącej opcji jawne on/off pozostają zachowane, a wartości automatyczne mogą zostać ocenione ponownie.

Funkcja wp_set_option_autoload(), dostępna od WordPressa 6.4, zmienia autoload bez zmiany wartości opcji. Natomiast update_option() kończy działanie wcześniej, jeśli wartość się nie zmieniła. Stare argumenty API yes/no są przestarzałe od wersji 6.7, ale pozostają rozpoznawane ze względu na zgodność. Autoload w WordPressie: wartości, zapytania i zużycie pamięci.

Wtyczka i kontrola danych wejściowych

Plik główny może znajdować się pod ścieżką lw-settings-demo/lw-settings-demo.php. Pokazany dalej JavaScript należy do sąsiedniego katalogu js/settings-rest.js. Przedrostek lwsd_ oddziela własne nazwy funkcji od nazw innych wtyczek.

Callback oczyszczający to funkcja tworząca dozwoloną wartość do zapisu. Tutaj usuwa HTML z komunikatu, ogranicza tekst do 200 znaków i dopuszcza tylko pozycję top lub bottom. Pusty komunikat wyłącza pasek. Włączają wyłącznie true, liczba 1 i wartość formularza "1"; ciąg "false" nie włącza paska przypadkowo.

PHP
<?php
/**
 * Plugin Name:       LW Settings Demo
 * Description:       Notice bar with a settings page built on the Settings API, also available via /wp/v2/settings.
 * Version:           1.0.0
 * Requires at least: 6.6
 * Requires PHP:      7.4
 * Author:            Lukas Wojcik
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       lw-settings-demo
 *
 * @package LW_Settings_Demo
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

const LWSD_OPTION = 'lwsd_settings';   // Row in wp_options.
const LWSD_GROUP  = 'lwsd_notice_bar'; // Option group, sent as option_page.
const LWSD_PAGE   = 'lwsd-notice-bar'; // Menu slug and settings page ID.

/**
 * Default values of the bundled option.
 *
 * @return array
 */
function lwsd_default_settings() {
	return array(
		'enabled'  => false,
		'message'  => '',
		'position' => 'bottom',
	);
}

/**
 * Returns the stored settings merged with the defaults.
 *
 * @return array
 */
function lwsd_get_settings() {
	$settings = get_option( LWSD_OPTION );

	if ( ! is_array( $settings ) ) {
		$settings = array();
	}

	return wp_parse_args( $settings, lwsd_default_settings() );
}

/**
 * Registers the option on init, so admin screens and REST requests both know it.
 */
function lwsd_register_setting() {
	register_setting(
		LWSD_GROUP,
		LWSD_OPTION,
		array(
			'type'              => 'object',
			'label'             => __( 'Notice bar', 'lw-settings-demo' ),
			'description'       => __( 'Visibility, text and position of the notice bar.', 'lw-settings-demo' ),
			'sanitize_callback' => 'lwsd_sanitize_settings',
			'default'           => lwsd_default_settings(),
			'show_in_rest'      => array(
				'schema' => array(
					'type'       => 'object',
					'properties' => array(
						'enabled'  => array(
							'type' => 'boolean',
						),
						'message'  => array(
							'type'      => 'string',
							'maxLength' => 200,
						),
						'position' => array(
							'type' => 'string',
							'enum' => array( 'top', 'bottom' ),
						),
					),
				),
			),
		)
	);
}
add_action( 'init', 'lwsd_register_setting' );

/**
 * Sanitizes the whole settings array.
 *
 * Runs for options.php and for REST writes, because both end in update_option().
 * Repeated sanitizing must leave the result unchanged; a new row can cause two passes.
 *
 * @param mixed $input Raw value.
 * @return array
 */
function lwsd_sanitize_settings( $input ) {
	$defaults = lwsd_default_settings();

	if ( ! is_array( $input ) ) {
		return $defaults;
	}

	$message  = isset( $input['message'] ) ? sanitize_text_field( $input['message'] ) : '';
	$position = isset( $input['position'] ) ? sanitize_key( $input['position'] ) : '';

	$output = array(
		'enabled'  => in_array( $input['enabled'] ?? false, array( true, 1, '1' ), true ),
		'message'  => mb_substr( $message, 0, 200 ),
		'position' => in_array( $position, array( 'top', 'bottom' ), true ) ? $position : $defaults['position'],
	);

	if ( $output['enabled'] && '' === $output['message'] ) {
		$output['enabled'] = false;

		// add_settings_error() lives in wp-admin/includes/template.php,
		// which a REST request does not load.
		if ( function_exists( 'add_settings_error' ) ) {
			add_settings_error(
				LWSD_OPTION,
				'lwsd_empty_message',
				__( 'The notice bar stays off until a message is entered.', 'lw-settings-demo' ),
				'warning'
			);
		}
	}

	return $output;
}

/**
 * Adds the page below Settings and loads its script only there.
 */
function lwsd_add_settings_page() {
	$hook_suffix = add_options_page(
		__( 'Notice bar', 'lw-settings-demo' ),
		__( 'Notice bar', 'lw-settings-demo' ),
		'manage_options',
		LWSD_PAGE,
		'lwsd_render_settings_page'
	);

	if ( $hook_suffix ) {
		add_action( 'load-' . $hook_suffix, 'lwsd_load_settings_page' );
	}
}
add_action( 'admin_menu', 'lwsd_add_settings_page' );

/**
 * Runs only when the settings page is requested.
 */
function lwsd_load_settings_page() {
	add_action( 'admin_enqueue_scripts', 'lwsd_enqueue_admin_script' );
}

/**
 * Enqueues the REST demo script.
 */
function lwsd_enqueue_admin_script() {
	wp_enqueue_script(
		'lwsd-settings-rest',
		plugins_url( 'js/settings-rest.js', __FILE__ ),
		array( 'wp-api-fetch', 'wp-i18n' ),
		'1.0.0',
		array( 'in_footer' => true )
	);
	wp_set_script_translations( 'lwsd-settings-rest', 'lw-settings-demo' );
}

/**
 * Declares section and fields. Only needed on admin screens.
 */
function lwsd_register_fields() {
	add_settings_section(
		'lwsd_section_main',
		__( 'Display', 'lw-settings-demo' ),
		'lwsd_render_section',
		LWSD_PAGE
	);

	add_settings_field(
		'lwsd_enabled',
		__( 'Show notice bar', 'lw-settings-demo' ),
		'lwsd_render_enabled_field',
		LWSD_PAGE,
		'lwsd_section_main',
		array( 'label_for' => 'lwsd_enabled' )
	);

	add_settings_field(
		'lwsd_message',
		__( 'Message', 'lw-settings-demo' ),
		'lwsd_render_message_field',
		LWSD_PAGE,
		'lwsd_section_main',
		array( 'label_for' => 'lwsd_message' )
	);

	add_settings_field(
		'lwsd_position',
		__( 'Position', 'lw-settings-demo' ),
		'lwsd_render_position_field',
		LWSD_PAGE,
		'lwsd_section_main',
		array( 'label_for' => 'lwsd_position' )
	);
}
add_action( 'admin_init', 'lwsd_register_fields' );

/**
 * Section intro.
 */
function lwsd_render_section() {
	echo '<p>' . esc_html__( 'All three values are stored together in one option.', 'lw-settings-demo' ) . '</p>';
}

/**
 * Checkbox field.
 */
function lwsd_render_enabled_field() {
	$settings = lwsd_get_settings();

	printf(
		'<input type="checkbox" id="lwsd_enabled" name="%1$s[enabled]" value="1" %2$s />',
		esc_attr( LWSD_OPTION ),
		checked( $settings['enabled'], true, false )
	);
}

/**
 * Text field.
 */
function lwsd_render_message_field() {
	$settings = lwsd_get_settings();

	printf(
		'<input type="text" class="regular-text" id="lwsd_message" name="%1$s[message]" value="%2$s" maxlength="200" />',
		esc_attr( LWSD_OPTION ),
		esc_attr( $settings['message'] )
	);
}

/**
 * Select field.
 */
function lwsd_render_position_field() {
	$settings = lwsd_get_settings();
	$choices  = array(
		'top'    => __( 'Top of the page', 'lw-settings-demo' ),
		'bottom' => __( 'Bottom of the page', 'lw-settings-demo' ),
	);

	printf( '<select id="lwsd_position" name="%s[position]">', esc_attr( LWSD_OPTION ) );

	foreach ( $choices as $value => $label ) {
		printf(
			'<option value="%1$s" %2$s>%3$s</option>',
			esc_attr( $value ),
			selected( $settings['position'], $value, false ),
			esc_html( $label )
		);
	}

	echo '</select>';
}

/**
 * Settings page. The form posts to options.php, not to this page.
 */
function lwsd_render_settings_page() {
	if ( ! current_user_can( 'manage_options' ) ) {
		return;
	}
	?>
	<div class="wrap">
		<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
		<form id="lwsd-settings-form" action="options.php" method="post">
			<?php
			settings_fields( LWSD_GROUP );
			do_settings_sections( LWSD_PAGE );
			submit_button();
			?>
		</form>
		<p>
			<button type="button" class="button" id="lwsd-rest-toggle"><?php esc_html_e( 'Toggle via REST API', 'lw-settings-demo' ); ?></button>
			<span id="lwsd-rest-status" aria-live="polite"></span>
		</p>
	</div>
	<?php
}

/**
 * Prints the notice bar on the front end.
 *
 * @param string $position Where the calling hook prints: top or bottom.
 */
function lwsd_print_notice( $position ) {
	$settings = lwsd_get_settings();

	if ( ! $settings['enabled'] || $position !== $settings['position'] || '' === $settings['message'] ) {
		return;
	}

	printf(
		'<div class="lwsd-notice lwsd-notice-%1$s" role="status">%2$s</div>',
		esc_attr( $position ),
		esc_html( $settings['message'] )
	);
}

/**
 * Top position, needs a theme that calls wp_body_open().
 */
function lwsd_print_top_notice() {
	lwsd_print_notice( 'top' );
}
add_action( 'wp_body_open', 'lwsd_print_top_notice' );

/**
 * Bottom position.
 */
function lwsd_print_bottom_notice() {
	lwsd_print_notice( 'bottom' );
}
add_action( 'wp_footer', 'lwsd_print_bottom_notice' );

/**
 * Creates the option with an explicit autoload value.
 */
function lwsd_activate() {
	// Read on every front-end request, so autoload it on purpose.
	add_option( LWSD_OPTION, lwsd_default_settings(), '', true );
}
register_activation_hook( __FILE__, 'lwsd_activate' );

Dlaczego rejestracja jest oddzielona od układu formularza?

register_setting() działa tutaj na init. Ten hook wykonuje się w panelu, przy zwykłych wywołaniach stron oraz przy żądaniach REST. Rejestracja wyłącznie na admin_init wystarczyłaby formularzowi administracyjnemu, ale nie zwykłemu żądaniu REST. Zarejestrowana wartość domyślna ma być dostępna także na stronie publicznej. Sekcje i pola pozostają na admin_init, ponieważ są potrzebne tylko w panelu.

Zapis formularza i zapis REST dochodzą do update_option(), a więc do zarejestrowanej funkcji oczyszczającej. Schemat REST dodatkowo sprawdza żądanie interfejsu. Na przykład komunikat REST dłuższy niż 200 znaków zostaje odrzucony. Funkcja oczyszczająca skraca natomiast dłuższy tekst przesłany formularzem do 200 znaków. Sam atrybut HTML maxlength nie zapewnia kontroli po stronie serwera.

Ponowne oczyszczenie musi dawać tę samą wartość. Przy brakującej opcji oczyszczanie mogą wykonać kolejno update_option() i add_option(). Jeśli jednak nowa wartość jest już równa zarejestrowanej wartości domyślnej, pierwsze wywołanie może zakończyć się bez utworzenia wiersza. Dwa przebiegi są zatem możliwe, ale nie gwarantowane przy każdym pierwszym zapisie.

add_settings_error() nie jest załadowana przy zwykłym żądaniu REST. Sprawdzenie function_exists() zapobiega wywołaniu brakującej funkcji. Na stronie w menu „Ustawienia” WordPress wyświetla komunikaty już przez options-head.php. Dodatkowe settings_errors() w funkcji strony powielałoby komunikaty. Ostrzeżenie z oczyszczania może zastąpić ogólne potwierdzenie sukcesu, choć pozostałe wartości zostały zapisane.

Co dzieje się przy zapisie formularza?

Formularz wysyła dane do options.php. settings_fields() dodaje cztery ukryte pola: grupę opcji, akcję update, nonce i adres powrotu. Nonce to tutaj ograniczona czasowo wartość kontrolna związana z akcją oraz zalogowaną sesją. Pomaga chronić przed żądaniami wywołanymi przez inną witrynę, ale nie zastępuje logowania ani kontroli uprawnień.

Dwie drogi do tego samego ustawienia. Osobne grupy dla niezależnych formularzy. Nonce nie nadaje uprawnień.
Ukryte pola: grupa, akcja update, nonce i adres powrotu. Osobne grupy dla niezależnych formularzy. Nonce nie nadaje uprawnień.

options.php najpierw sprawdza uprawnienie, następnie nonce i dozwoloną grupę opcji. WordPress odczytuje potem wartości zarejestrowanych opcji, usuwa znaki ucieczki dodane podczas przesyłania i wywołuje update_option(). Po oczyszczeniu i filtrach poprzedzających aktualizację zapisywana jest tylko zmieniona wartość. Komunikaty są przechowywane krótko, a następnie następuje przekierowanie na stronę ustawień.

Menu, funkcja wyświetlająca stronę i zapis muszą wymagać odpowiednich uprawnień. Menu z edit_pages nie wystarczy, jeśli options.php nadal wymaga manage_options. Uprawnieniem formularza steruje filtr option_page_capability_lwsd_notice_bar. Zmiana tego filtra nie zmienia automatycznie uprawnienia interfejsu REST. Sanityzacja, escaping, Nonce i Capability w przeglądzie wtyczek.

Reguły interfejsu /wp/v2/settings

show_in_rest udostępnia zarejestrowaną opcję przez ten interfejs REST. Dla obiektu properties opisuje dozwolone właściwości, a dla tablicy items opisuje elementy. Schemat jest więc zbiorem reguł typów i dozwolonych wartości. Bez jawnej odmiennej reguły kontroler dodaje obiektom additionalProperties: false, przez co nieznane właściwości są niedozwolone.

Odczyt i zapis w standardowym kontrolerze wymagają manage_options. Klucz odpowiedzi odpowiada nazwie opcji, chyba że show_in_rest.name określa inną nazwę. Wartości publicznie dostępne wymagałyby osobnej trasy z celowo ograniczoną odpowiedzią i własną funkcją kontroli uprawnień.

Cała opcja pominięta w żądaniu pozostaje bez zmian. Przesłany obiekt nie jest jednak automatycznie łączony ze starą wartością. Jeśli wewnątrz lwsd_settings brakuje enabled, funkcja oczyszczająca tego przykładu wstawia false; brakująca pozycja staje się bottom. Częściowy obiekt może więc zresetować inne właściwości.

Nieprawidłowa zapisana wartość pojawia się w wyniku GET jako null. Możliwą przyczyną jest stara właściwość, której schemat już nie dopuszcza. Wartość POST null usuwa prawidłową opcję i pozwala zadziałać wartości domyślnej. Przy nieprawidłowej zapisanej wartości kontroler odrzuca takie usunięcie z kodem rest_invalid_stored_value i statusem 500. Niezgodność wymaga migracji danych albo odpowiednio poprawionego schematu.

Przycisk REST bez nieaktualnego formularza

Skrypt ładuje się tylko na własnej stronie ustawień. Zależność wp-api-fetch dostarcza tam między innymi adres REST i obsługę nonce. Nie tworzy nowego logowania: nadal wymagane są prawidłowe ciasteczka logowania, odpowiedni nonce REST i uprawnienie.

Przycisk odczytuje zapisany obiekt, odwraca przełącznik i przesyła cały obiekt. Niezapisane zmiany formularza blokują rozpoczęcie żądania. Podczas żądania pola formularza pozostają zablokowane. Po sukcesie formularz przyjmuje wartości rzeczywiście zwrócone przez serwer. Zapobiega to natychmiastowemu cofnięciu przełącznika przez późniejszy zapis starego formularza.

Jeśli po wysłaniu wynik zapisu jest niepewny, formularz pozostaje zablokowany do przeładowania. Utrata odpowiedzi nie dowodzi, że zapis się nie udał. Odczyt i późniejszy zapis całego obiektu nie chronią też przed równoczesną zmianą w innej przeglądarce. Takie sytuacje wymagają wykrywania konfliktów po stronie serwera. Przykład: przeglądarka odczytała wersję 4, ale istnieje już wersja 5. Serwer odrzuca nieaktualną próbę zmiany zamiast nadpisywać nowsze wartości.

JavaScript
/* global wp */
( function () {
    'use strict';
    const { __ } = wp.i18n;
    const form = document.getElementById( 'lwsd-settings-form' );
    const button = document.getElementById( 'lwsd-rest-toggle' );
    const status = document.getElementById( 'lwsd-rest-status' );
    const enabled = document.getElementById( 'lwsd_enabled' );
    const message = document.getElementById( 'lwsd_message' );
    const position = document.getElementById( 'lwsd_position' );
    if ( ! form || ! button || ! status || ! enabled || ! message || ! position ) {
        return;
    }
    const readForm = () => JSON.stringify( [ enabled.checked, message.value, position.value ] );
    let baseline = readForm();
    let busy = false;
    form.addEventListener( 'submit', ( event ) => {
        if ( busy ) {
            event.preventDefault();
        }
    } );
    button.addEventListener( 'click', async () => {
        if ( busy ) {
            return;
        }
        if ( readForm() !== baseline ) {
            status.textContent = __( 'Unsaved form changes: saving the form is required before REST toggling.', 'lw-settings-demo' );
            return;
        }
        busy = true;
        button.disabled = true;
        const controls = Array.from( form.elements );
        const disabledBefore = controls.map( ( control ) => control.disabled );
        controls.forEach( ( control ) => { control.disabled = true; } );
        status.textContent = __( 'Request in progress.', 'lw-settings-demo' );
        let writeStarted = false;
        let uncertain = false;
        try {
            const settings = await wp.apiFetch( { path: '/wp/v2/settings' } );
            const current = settings.lwsd_settings;
            if ( ! current || typeof current.enabled !== 'boolean' ) {
                throw new Error( __( 'Stored value does not match the schema.', 'lw-settings-demo' ) );
            }
            writeStarted = true;
            const saved = await wp.apiFetch( {
                path: '/wp/v2/settings',
                method: 'POST',
                data: { lwsd_settings: { ...current, enabled: ! current.enabled } },
            } );
            const value = saved.lwsd_settings;
            if ( ! value || typeof value.enabled !== 'boolean' ||
                typeof value.message !== 'string' || ! [ 'top', 'bottom' ].includes( value.position ) ) {
                throw new Error( __( 'Unexpected response. Reloading is required to verify the stored state.', 'lw-settings-demo' ) );
            }
            enabled.checked = value.enabled;
            message.value = value.message;
            position.value = value.position;
            baseline = readForm();
            status.textContent = value.enabled
                ? __( 'Notice bar is on.', 'lw-settings-demo' )
                : __( 'Notice bar is off.', 'lw-settings-demo' );
        } catch ( error ) {
            uncertain = writeStarted;
            status.textContent = uncertain
                ? __( 'Save result uncertain. Reloading is required before further changes.', 'lw-settings-demo' )
                : ( error && error.message ? error.message : __( 'Request failed.', 'lw-settings-demo' ) );
        } finally {
            if ( ! uncertain ) {
                controls.forEach( ( control, index ) => { control.disabled = disabledBefore[ index ]; } );
                button.disabled = false;
                busy = false;
            }
        }
    } );
} )();

Zakres testów i ograniczenia

Archiwalne testy pierwotnego przykładu na WordPressie 7.1.2 i PHP 8.4 dokumentują zapis formularza oraz REST. Poprawioną funkcję oczyszczającą sprawdzono z funkcjami pomocniczymi WordPressa. Nowy JavaScript przetestowano z symulowanym sukcesem, niezapisanymi zmianami, nieprawidłowymi danymi i utraconymi odpowiedziami. Te kontrole nie zastępują nowego pełnego testu instalacji oraz działania w przeglądarce.

Górna pozycja wymaga wywołania wp_body_open() przez motyw, a dolna wp_footer(). Przykład nie zawiera jeszcze usuwania danych przy odinstalowaniu i nie został sprawdzony dla ustawień sieci Multisite. Cykl życia wtyczki: aktywacja, dezaktywacja, odinstalowanie.

Pytania i odpowiedzi

Czy rejestracja na admin_init wystarczy?

Dla formularza administracyjnego tak. Zwykłe żądania REST i zarejestrowane wartości domyślne na stronie publicznej wymagają rejestracji także w tych miejscach. Przykład używa więc init.

Dlaczego REST API zwraca null?

Zapisana wartość może być niezgodna ze schematem, na przykład przez niedozwoloną już właściwość. null wskazuje wtedy problem w wyniku, a nie polecenie odesłania tej wartości bez sprawdzenia.

Czy można zmienić jedną właściwość obiektu?

Standardowy kontroler nie łączy automatycznie przesłanego częściowego obiektu ze starymi właściwościami. Pełny odczyt i zapis zachowuje pozostałe wartości, ale nadal może kolidować z równoczesnymi zmianami. Własna trasa może jawnie obsługiwać częściowe zmiany i wykrywanie konfliktów.

Czy sam nonce zapewnia ochronę?

Nie. Logowanie i uprawnienia są sprawdzane osobno. Prawidłowa wartość kontrolna nie nadaje dodatkowych uprawnień.

Lukas Wójcik

Lukas Wójcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Kontakt

Masz pytanie lub pomysł na projekt internetowy? Napisz krótko, o co chodzi. Formularz korzysta z reCAPTCHA do ochrony przed spamem.

Napisanie komentarza

Doświadczenia z innymi wtyczkami lub środowiskami hostingowymi oraz pytania o konfigurację są tu mile widziane.

Adres e-mail nie jest publikowany. Pola obowiązkowe oznaczono gwiazdką.