|null */ private ?array $site_list = null; /** * The network list about to be deleted, likewise. * * @var array|null */ private ?array $network_list = null; /** * Note the site list before it is deleted. * * @param string $option Option being deleted. */ public function note_site_list( $option ): void { $list = 'active_plugins' === $option ? get_option( 'active_plugins' ) : null; $this->site_list = is_array( $list ) && in_array( $this->basename, $list, true ) && $this->guarded() ? $list : null; } /** * Put the site list back once deleted. */ public function restore_site_list(): void { if ( null === $this->site_list ) { return; } $list = $this->site_list; $this->site_list = null; $this->record(); $this->as_the_firewall( static fn () => update_option( 'active_plugins', $list ) ); } /** * Note the network list before it is deleted. */ public function note_network_list(): void { $list = get_site_option( 'active_sitewide_plugins' ); $this->network_list = is_array( $list ) && array_key_exists( $this->basename, $list ) && $this->guarded() ? $list : null; } /** * Put the network list back once deleted. */ public function restore_network_list(): void { if ( null === $this->network_list ) { return; } $list = $this->network_list; $this->network_list = null; $this->record(); $this->as_the_firewall( static fn () => update_site_option( 'active_sitewide_plugins', $list ) ); } /** * Put a list back as the firewall rather than the agent: the write is * the firewall undoing the agent's, and the effect backstop would * otherwise judge it as one more of the agent's own. * * @param callable(): mixed $write The write. */ private function as_the_firewall( callable $write ): void { $context = AgentContext::current(); AgentContext::reset(); try { $write(); } finally { if ( null !== $context ) { AgentContext::establish( $context->agent ); } } } /** * The site's active plugin list: a list of basenames. * * @param mixed $value New value. * @param mixed $old_value Current value. * @return mixed */ public function keep_site_list( $value, $old_value ) { $had = is_array( $old_value ) && in_array( $this->basename, $old_value, true ); $has = is_array( $value ) && in_array( $this->basename, $value, true ); return $this->keep( $had && ! $has, $value, $old_value ); } /** * The network's active plugin list: basenames as keys. * * @param mixed $value New value. * @param mixed $old_value Current value. * @return mixed */ public function keep_network_list( $value, $old_value ) { $had = is_array( $old_value ) && array_key_exists( $this->basename, $old_value ); $has = is_array( $value ) && array_key_exists( $this->basename, $value ); return $this->keep( $had && ! $has, $value, $old_value ); } /** * The old list when the firewall would leave it under an * administrator-run agent; otherwise the new one. * * @param bool $removed Whether the new list drops the firewall. * @param mixed $value New value. * @param mixed $old_value Current value. * @return mixed */ private function keep( bool $removed, $value, $old_value ) { if ( ! $removed || ! $this->guarded() ) { return $value; } $this->record(); return $old_value; } /** * Whether an agent whose user holds manage_options is acting. */ private function guarded(): bool { $context = AgentContext::current(); return null !== $context && user_can( $context->agent->wp_user_id, 'manage_options' ); } /** * Ledger the refused attempt. */ private function record(): void { $context = AgentContext::current(); if ( null === $context ) { return; // @codeCoverageIgnore } $this->pipeline->record_refusal( $context->agent, new ActionDescriptor( 'effect', 'update', 'plugin', null, null, array(), $this->basename ), 'firewall_protected', 'adaf_firewall_protected' ); } }