'unknown_module' ], 404 ); } $module = Module_Registry::get_module( $module_id ); if ( ! current_user_can( Module_Registry::get_activation_capability( $module['id'] ) ) ) { wp_send_json_error( [ 'code' => 'forbidden' ], 403 ); } check_ajax_referer( self::NONCE_ACTION, self::NONCE_FIELD ); $enabled = ! empty( $_POST['enabled'] ) && '0' !== $_POST['enabled']; $classification = $module['classification']; if ( ( Module_Registry::MODULE !== $classification && Module_Registry::SECTION_FEATURE !== $classification ) || ! Module_Registry::is_toggleable( $module_id ) ) { wp_send_json_error( [ 'code' => 'not_toggleable' ], 400 ); } if ( Module_Registry::MODULE === $classification && $enabled && ! Module_Registry::is_available( $module_id ) ) { wp_send_json_error( [ 'code' => 'unavailable' ], 400 ); } $result = Module_Registry::set_enabled( $module_id, $enabled ); if ( is_wp_error( $result ) ) { wp_send_json_error( [ 'code' => $result->get_error_code() ], 500 ); } wp_send_json_success( $this->canonical_state_payload( $module_id ) ); } /** * Handle a pin/unpin AJAX request: appends the module ID to * `PIN_OPTION` if absent, removes it if present. Unlike `handle()`, * pinning is presentation/navigation only -- there is no classification * or activation-mode gate, only that the ID resolve to a real, * currently-registered module. * * @return void */ public function handle_pin_toggle(): void { if ( ! current_user_can( $this->pin_required_capability() ) ) { wp_send_json_error( [ 'code' => 'forbidden' ], 403 ); } check_ajax_referer( self::PIN_NONCE_ACTION, self::NONCE_FIELD ); $module_id = isset( $_POST['module_id'] ) ? sanitize_key( wp_unslash( $_POST['module_id'] ) ) : ''; if ( '' === $module_id || ! Module_Registry::has_module( $module_id ) ) { wp_send_json_error( [ 'code' => 'unknown_module' ], 404 ); } $pinned = self::normalize_pinned( (array) get_option( self::PIN_OPTION, [] ) ); if ( in_array( $module_id, $pinned, true ) ) { $pinned = array_values( array_diff( $pinned, [ $module_id ] ) ); } else { $pinned[] = $module_id; } $pinned = self::normalize_pinned( $pinned ); update_option( self::PIN_OPTION, $pinned ); wp_send_json_success( [ 'module_id' => $module_id, 'pinned' => in_array( $module_id, $pinned, true ), ] ); } /** * Sanitize, dedupe, drop any slug that isn't a real currently-registered * module, and cap the list at MAX_PINS by dropping the oldest entries * (the front of the list, since new pins are appended at the end). * * @param array $slugs Raw slug list. * * @return array */ private static function normalize_pinned( array $slugs ): array { $normalized = []; foreach ( $slugs as $slug ) { $slug = sanitize_key( (string) $slug ); if ( '' === $slug || in_array( $slug, $normalized, true ) || ! Module_Registry::has_module( $slug ) ) { continue; } $normalized[] = $slug; } if ( count( $normalized ) > self::MAX_PINS ) { $normalized = array_slice( $normalized, -self::MAX_PINS ); } return $normalized; } /** * Pinning mutates one site-wide navigation option, not module runtime * state. It deliberately retains the console-administrator policy instead * of inheriting a module's activation capability. * * @return string */ private function pin_required_capability(): string { return 'manage_options'; } /** * Re-resolve and return a module's canonical state after a write, rather * than deriving the response from the submitted boolean (Phase 3.5B.2 §9). * * @param string $module_id Canonical module ID. * * @return array{module_id:string,enabled:bool,status:string,section:string} */ private function canonical_state_payload( string $module_id ): array { $module = Module_Registry::get_module( $module_id ); return [ 'module_id' => $module_id, 'enabled' => Module_Registry::is_enabled( $module_id ), 'status' => Module_Registry::get_status( $module_id ), 'section' => $module['section'] ?? '', ]; } }