*/ private $theme_source; private ComponentEvidenceSanitizer $sanitizer; /** * @param callable():array|null $theme_source Testable WordPress theme adapter. */ public function __construct( FindingFactory $findings, ?callable $theme_source = null, ?string $hash_key = null ) { $this->findings = $findings; $this->theme_source = $theme_source ?? array( $this, 'wordpress_themes' ); $this->sanitizer = new ComponentEvidenceSanitizer( $hash_key ?? ( new LocalHashKey() )->derive() ); } public function id(): string { return 'components.theme_state'; } /** * @return array */ public function execute( string $timestamp ): array { try { $themes = call_user_func( $this->theme_source ); } catch ( Throwable $exception ) { return $this->error_finding( $timestamp ); } if ( ! is_array( $themes ) ) { return $this->error_finding( $timestamp ); } $total = count( $themes ); $truncated = $total > self::MAX_THEMES; $themes = array_slice( $themes, 0, self::MAX_THEMES ); $objects = array(); $active_count = 0; $active_error = false; foreach ( $themes as $theme ) { if ( ! is_array( $theme ) || ! isset( $theme['identifier'], $theme['name'], $theme['version'], $theme['state'], $theme['errors'] ) || ! is_string( $theme['identifier'] ) || ! is_string( $theme['name'] ) || ! is_string( $theme['version'] ) || ! is_string( $theme['state'] ) || ! is_array( $theme['errors'] ) || '' === $theme['identifier'] ) { continue; } if ( ! in_array( $theme['state'], array( 'active', 'parent', 'inactive' ), true ) ) { continue; } $error_codes = array_values( array_filter( array_map( static fn ( $code ): string => is_string( $code ) && 1 === preg_match( '/^[a-z0-9_-]+$/', $code ) ? $code : 'redacted_error', $theme['errors'] ) ) ); $objects[] = array( 'reference' => $this->sanitizer->reference( $theme['identifier'] ), 'name' => $this->sanitizer->text( $theme['name'] ), 'version' => $this->sanitizer->version( $theme['version'] ), 'state' => $theme['state'], 'errors' => $error_codes, ); if ( 'active' === $theme['state'] ) { ++$active_count; $active_error = $active_error || ! empty( $error_codes ); } } $observed = count( $objects ); if ( 0 === $observed ) { $status = 'not_tested'; $coverage = 'none'; } elseif ( 1 !== $active_count || $active_error ) { $status = 'fail'; $coverage = $observed === $total ? 'complete' : 'partial'; } elseif ( $truncated ) { $status = 'warning'; $coverage = 'partial'; } elseif ( $observed < $total ) { $status = 'not_tested'; $coverage = 'partial'; } else { $status = 'pass'; $coverage = 'complete'; } return $this->findings->build( $this->id(), $status, array( 'objects' => $objects, 'observed_count' => $observed, 'total_count' => $total, 'truncated' => $truncated, 'limitations' => array( 'Theme headers and WordPress error codes were observed locally; theme files and remote update services were not inspected.' ), ), $observed, $total, $coverage, $timestamp ); } /** * @return array */ private function wordpress_themes(): array { return ( new LocalThemeAdapter() )->themes(); } /** * @return array */ private function error_finding( string $timestamp ): array { return $this->findings->build( $this->id(), 'error', array( 'objects' => array(), 'observed_count' => 0, 'total_count' => 0, 'truncated' => false, 'limitations' => array( 'The local theme adapter failed; no exception, path or theme value was stored.' ), ), 0, 0, 'error', $timestamp ); } }