👁 查看:class-rest-api.php
路径:/home/forge/kingkrunch.com/wp/wp-content/plugins/simple-history/inc/services/class-rest-api.php
大小:7.91 KB · 修改:2026-09-25 00:42:29 · 权限:0644 · 可写
<?php

namespace Simple_History\Services;

use Simple_History\Helpers;
use Simple_History\WP_REST_Events_Controller;
use Simple_History\WP_REST_SearchOptions_Controller;
use Simple_History\WP_REST_Stats_Controller;
use Simple_History\WP_REST_Support_Info_Controller;
use Simple_History\WP_REST_User_Card_Controller;
use Simple_History\WP_REST_Devtools_Controller;
use WP_REST_Server;

/**
 * Load the Simple History REST API.
 */
class REST_API extends Service {
	/**
	 * User meta key holding the user's chosen event log view, "detailed", "compact" or "table".
	 */
	const EVENTS_VIEW_USER_META_KEY = 'simple_history_events_view';

	/**
	 * User meta key holding the views the user wants the page sidebar hidden in.
	 *
	 * A comma-separated list of view names rather than one boolean, because
	 * the three views want different things: a table can use the sidebar's
	 * width for columns, while the detailed list is a column of text that
	 * gains nothing from being wider. Stored per view so hiding it in one
	 * does not quietly hide it in the others.
	 *
	 * Stored as a string, not an array: a scalar meta needs no serialization
	 * and cannot come back from the database as something unexpected.
	 */
	const HIDDEN_SIDEBAR_VIEWS_USER_META_KEY = 'simple_history_hidden_sidebar_views';

	/**
	 * The views the event log page offers.
	 */
	const EVENTS_VIEWS = [ 'detailed', 'compact', 'table' ];

	/** @inheritDoc */
	public function loaded() {
		add_action( 'rest_api_init', [ $this, 'register_routes' ] );

		// On init, not rest_api_init, so the registered default also applies
		// when the events page reads the value outside a REST request.
		add_action( 'init', [ $this, 'register_user_meta' ] );
	}

	/**
	 * Register the user meta that stores the event log view.
	 *
	 * Not exposed in REST (no `show_in_rest`, no `auth_callback`): it used to
	 * be readable/writable through /wp/v2/users, but that also let it be
	 * saved through /wp/v2/users/me, and that endpoint always runs
	 * wp_update_user(), firing `profile_update` on every toggle. Third-party
	 * plugins act on that hook, so switching the view logged unrelated
	 * "profile updated" activity. The value is now saved through the
	 * dedicated /simple-history/v1/events-view route instead; see
	 * register_routes() and save_events_view().
	 */
	public function register_user_meta() {
		register_meta(
			'user',
			self::HIDDEN_SIDEBAR_VIEWS_USER_META_KEY,
			[
				'type'              => 'string',
				'single'            => true,
				// The table view starts without the sidebar and the other two
				// start with it. Measured at 1280px: with the sidebar in place
				// the table overflows its column by 69px, and without it the
				// Message column goes from 304px to 484. The detailed and
				// compact lists have no such problem, so nothing is taken away
				// from anyone who has not asked.
				'default'           => 'table',
				'sanitize_callback' => function ( $value ) {
					$views = is_string( $value ) ? explode( ',', $value ) : [];
					$views = array_intersect( $views, self::EVENTS_VIEWS );

					// Sorted and de-duplicated so the same set of views always
					// stores as the same string, whatever order it arrived in.
					$views = array_unique( $views );
					sort( $views );

					return implode( ',', $views );
				},
			]
		);

		register_meta(
			'user',
			self::EVENTS_VIEW_USER_META_KEY,
			[
				'type'              => 'string',
				'single'            => true,
				'default'           => 'detailed',
				// Collapse anything but the known values to the default,
				// since the value no longer goes through a REST enum check.
				'sanitize_callback' => function ( $value ) {
					return in_array( $value, [ 'compact', 'table' ], true )
						? $value
						: 'detailed';
				},
			]
		);
	}

	/**
	 * Register the REST API routes.
	 */
	public function register_routes() {
		$rest_api_controller = new WP_REST_Events_Controller();
		$rest_api_controller->register_routes();

		$search_options_controller = new WP_REST_SearchOptions_Controller();
		$search_options_controller->register_routes();

		$stats_controller = new WP_REST_Stats_Controller();
		$stats_controller->register_routes();

		$support_info_controller = new WP_REST_Support_Info_Controller();
		$support_info_controller->register_routes();

		$user_card_controller = new WP_REST_User_Card_Controller();
		$user_card_controller->register_routes();

		// A dedicated route instead of the core /wp/v2/users/me endpoint —
		// see register_user_meta() for why.
		register_rest_route(
			'simple-history/v1',
			'/events-view',
			[
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => [ $this, 'save_events_view' ],
				'permission_callback' => [ $this, 'events_view_permissions_check' ],
				'args'                => [
					'view' => [
						'description' => __( 'The event log view to remember for the current user.', 'simple-history' ),
						'type'        => 'string',
						'required'    => true,
						'enum'        => [ 'detailed', 'compact', 'table' ],
					],
				],
			]
		);

		// The whole set in one call, rather than one view at a time. Writing
		// the set is idempotent, so a request that arrives twice, or out of
		// order with another, still leaves the stored value describing what
		// the reader last saw.
		register_rest_route(
			'simple-history/v1',
			'/sidebar-visibility',
			[
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => [ $this, 'save_sidebar_visibility' ],
				'permission_callback' => [ $this, 'events_view_permissions_check' ],
				'args'                => [
					'views' => [
						'description' => __( 'The event log views to hide the page sidebar in.', 'simple-history' ),
						'type'        => 'array',
						'required'    => true,
						'items'       => [
							'type' => 'string',
							'enum' => self::EVENTS_VIEWS,
						],
					],
				],
			]
		);

		// Only register dev tools routes when dev mode is enabled.
		if ( ! Helpers::dev_mode_is_enabled() ) {
			return;
		}

		$dev_tools_controller = new WP_REST_Devtools_Controller();
		$dev_tools_controller->register_routes();
	}

	/**
	 * Permission check for POST /simple-history/v1/events-view.
	 *
	 * The toggle only appears on the history page, so require the same
	 * capability that page requires. Returning false here also fails
	 * logged-out requests (they get a 401, a logged-in user without the
	 * capability gets a 403 — both from WP_REST_Server's own handling of a
	 * false permission callback).
	 *
	 * @return bool
	 */
	public function events_view_permissions_check() {
		// phpcs:ignore WordPress.WP.Capabilities.Undetermined -- Dynamic capability from Helpers::get_view_history_capability().
		return current_user_can( Helpers::get_view_history_capability() );
	}

	/**
	 * Save the current user's chosen event log view.
	 *
	 * No user id parameter: this can only ever write the current user's own
	 * meta, unlike /wp/v2/users/<id>.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @return \WP_REST_Response
	 */
	public function save_events_view( $request ) {
		$view = $request->get_param( 'view' );

		update_user_meta( get_current_user_id(), self::EVENTS_VIEW_USER_META_KEY, $view );

		return rest_ensure_response( [ 'view' => $view ] );
	}

	/**
	 * Save the views the current user wants the page sidebar hidden in.
	 *
	 * No user id parameter, for the same reason save_events_view() has none:
	 * this can only ever write the current user's own meta.
	 *
	 * @param \WP_REST_Request $request Full details about the request.
	 * @return \WP_REST_Response
	 */
	public function save_sidebar_visibility( $request ) {
		$views = (array) $request->get_param( 'views' );

		// The registered sanitize_callback does the filtering, so whatever
		// survived the route's own enum check is narrowed again on the way in.
		update_user_meta(
			get_current_user_id(),
			self::HIDDEN_SIDEBAR_VIEWS_USER_META_KEY,
			implode( ',', $views )
		);

		return rest_ensure_response( [ 'views' => $views ] );
	}
}