* from get_plugins(); file is relative to pluginsDir ('akismet/akismet.php', 'hello.php'). * - locale string Site package locale (global "wp local package" of version.php, else 'en_US'). * Never the user/site display locale (get_locale): the checksums follow the package * (N§A1, confirmed by test-fixtures/wp-env/PROBE.md on 2026-10-23). * - wpVersion string WordPress version (global of version.php). * - dropins string[] Keys of _get_dropins() at run time (N§A6). * - coreChecksums CoreChecksums|null * - coreUnavailable string|null Why coreChecksums is null: 'not_consented' | 'network_error' | * 'not_published' | 'unexpected_response'. * - pluginChecksums array slug => sums. * - pluginUnavailable array slug => 'not_consented' | 'network_error' | * 'no_sums_for_version' | 'not_on_wporg' (SPEC Tranché 1). * - roles array> role name => capabilities (WP_Roles::$roles). * - knownMuPlugins string[] mu-plugin paths the administrator recognized. Empty by default: then every * mu-plugin is "not recognized" for control 8 (the SPEC defines no recognition). * * @package Timaub\AfterHackChecklist */ namespace Timaub\AfterHackChecklist\Domain; final class Context { /** @var string */ public $referenceDate = ''; /** @var string[] */ public $siteHosts = array(); /** @var string */ public $absPath = ''; /** @var string */ public $contentDir = 'wp-content'; /** @var string */ public $uploadsDir = 'wp-content/uploads'; /** @var string */ public $pluginsDir = 'wp-content/plugins'; /** @var string */ public $muPluginsDir = 'wp-content/mu-plugins'; /** @var string[] */ public $activeThemeDirs = array(); /** @var array */ public $plugins = array(); /** @var string */ public $locale = 'en_US'; /** @var string */ public $wpVersion = ''; /** @var string[] */ public $dropins = array(); /** @var CoreChecksums|null */ public $coreChecksums; /** @var string|null */ public $coreUnavailable; /** @var array */ public $pluginChecksums = array(); /** @var array */ public $pluginUnavailable = array(); /** @var array> */ public $roles = array(); /** @var string[] */ public $knownMuPlugins = array(); public static function fromArray( array $a ): self { $c = new self(); foreach ( array( 'referenceDate', 'absPath', 'contentDir', 'uploadsDir', 'pluginsDir', 'muPluginsDir', 'locale', 'wpVersion' ) as $k ) { if ( isset( $a[ $k ] ) ) { $c->$k = self::path( (string) $a[ $k ], 'absPath' !== $k && 'referenceDate' !== $k && 'locale' !== $k && 'wpVersion' !== $k ); } } if ( '' !== $c->absPath ) { $c->absPath = rtrim( str_replace( '\\', '/', $c->absPath ), '/' ) . '/'; } foreach ( array( 'siteHosts', 'dropins', 'knownMuPlugins' ) as $k ) { if ( isset( $a[ $k ] ) ) { $c->$k = array_values( array_map( 'strval', (array) $a[ $k ] ) ); } } $c->siteHosts = array_map( 'strtolower', $c->siteHosts ); if ( isset( $a['activeThemeDirs'] ) ) { $c->activeThemeDirs = array_values( array_map( function ( $d ) { return self::path( (string) $d, true ); }, (array) $a['activeThemeDirs'] ) ); } if ( isset( $a['plugins'] ) ) { foreach ( (array) $a['plugins'] as $p ) { $c->plugins[] = array( 'file' => str_replace( '\\', '/', (string) $p['file'] ), 'name' => isset( $p['name'] ) ? (string) $p['name'] : '', 'version' => isset( $p['version'] ) ? (string) $p['version'] : '', 'active' => ! empty( $p['active'] ), ); } } if ( isset( $a['coreChecksums'] ) && is_array( $a['coreChecksums'] ) ) { $c->coreChecksums = CoreChecksums::fromArray( $a['coreChecksums'] ); } $c->coreUnavailable = isset( $a['coreUnavailable'] ) ? (string) $a['coreUnavailable'] : null; if ( isset( $a['pluginChecksums'] ) ) { foreach ( (array) $a['pluginChecksums'] as $slug => $p ) { $c->pluginChecksums[ (string) $slug ] = $p instanceof PluginChecksums ? $p : PluginChecksums::fromArray( $p ); } } if ( isset( $a['pluginUnavailable'] ) ) { $c->pluginUnavailable = array_map( 'strval', (array) $a['pluginUnavailable'] ); } if ( isset( $a['roles'] ) ) { $c->roles = (array) $a['roles']; } return $c; } private static function path( string $p, bool $relative ): string { return $relative ? trim( str_replace( '\\', '/', $p ), '/' ) : $p; } public function toArray(): array { $plugins = array(); foreach ( $this->pluginChecksums as $slug => $p ) { $plugins[ $slug ] = $p->toArray(); } return array( 'referenceDate' => $this->referenceDate, 'siteHosts' => $this->siteHosts, 'absPath' => $this->absPath, 'contentDir' => $this->contentDir, 'uploadsDir' => $this->uploadsDir, 'pluginsDir' => $this->pluginsDir, 'muPluginsDir' => $this->muPluginsDir, 'activeThemeDirs' => $this->activeThemeDirs, 'plugins' => $this->plugins, 'locale' => $this->locale, 'wpVersion' => $this->wpVersion, 'dropins' => $this->dropins, 'coreChecksums' => $this->coreChecksums ? $this->coreChecksums->toArray() : null, 'coreUnavailable' => $this->coreUnavailable, 'pluginChecksums' => $plugins, 'pluginUnavailable' => $this->pluginUnavailable, 'roles' => $this->roles, 'knownMuPlugins' => $this->knownMuPlugins, ); } /** Slug of a get_plugins() file: its folder, or the file name without .php for a single file. */ public static function slugOf( string $pluginFile ): string { $pos = strpos( $pluginFile, '/' ); if ( false !== $pos ) { return substr( $pluginFile, 0, $pos ); } return Text::endsWith( strtolower( $pluginFile ), '.php' ) ? substr( $pluginFile, 0, -4 ) : $pluginFile; } /** * get_plugins() entry owning a folder or single file of the plugins folder. * * @param string $first First path segment below pluginsDir ('akismet' or 'hello.php'). * @param bool $isDir Whether the segment is a folder. * @return array{file:string, name:string, version:string, active:bool}|null */ public function pluginFor( string $first, bool $isDir ) { foreach ( $this->plugins as $p ) { if ( $isDir ? Text::startsWith( $p['file'], $first . '/' ) : $p['file'] === $first ) { return $p; } } return null; } /** * Slugs of the installed plugins (active or not), derived from $plugins. * * @return string[] */ public function installedSlugs(): array { $out = array(); foreach ( $this->plugins as $p ) { $out[] = self::slugOf( $p['file'] ); } return array_values( array_unique( $out ) ); } /** Capabilities of a role, only those set to true. */ public function roleCaps( string $role ): array { if ( ! isset( $this->roles[ $role ] ) ) { return array(); } return array_keys( array_filter( (array) $this->roles[ $role ] ) ); } /** Whether $path is inside the relative folder $dir. */ public static function under( string $path, string $dir ): bool { return '' !== $dir && Text::startsWith( $path, $dir . '/' ); } }