*/ private $signal_source; /** * @param callable():array|null $signal_source Testable presence-only adapter. */ public function __construct( FindingFactory $findings, ?callable $signal_source = null ) { $this->findings = $findings; $this->signal_source = $signal_source ?? array( $this, 'wordpress_signals' ); } public function id(): string { return 'privacy.rights_retention'; } /** @return array */ public function execute( string $timestamp ): array { try { $signals = call_user_func( $this->signal_source ); } catch ( Throwable $exception ) { return $this->error_finding( $timestamp ); } if ( ! is_array( $signals ) || ! isset( $signals['privacy_policy'] ) || ! is_string( $signals['privacy_policy'] ) || ! in_array( $signals['privacy_policy'], array( 'published', 'unpublished', 'missing' ), true ) ) { return $this->error_finding( $timestamp ); } $observations = array( array( 'signal' => 'privacy_policy', 'state' => $signals['privacy_policy'], ), ); $ready = 'published' === $signals['privacy_policy'] ? 1 : 0; foreach ( array_slice( self::SIGNALS, 1 ) as $signal ) { if ( ! array_key_exists( $signal, $signals ) || ! is_bool( $signals[ $signal ] ) ) { return $this->error_finding( $timestamp ); } $observations[] = array( 'signal' => $signal, 'state' => $signals[ $signal ] ? 'present' : 'not_observed', ); if ( $signals[ $signal ] ) { ++$ready; } } $status = count( self::SIGNALS ) === $ready ? 'info' : 'warning'; return $this->findings->build( $this->id(), $status, array( 'observations' => $observations, 'method' => 'wordpress_privacy_api_presence_only', 'limitations' => array( 'Only local WordPress policy status, registered-hook presence, API presence and cleanup scheduling were observed. Plugin callbacks, request records, personal data, policy content, retention periods and legal compliance were not inspected.' ), ), count( self::SIGNALS ), count( self::SIGNALS ), 'complete', $timestamp ); } /** * @return array{privacy_policy:string,exporter_registry:bool,eraser_registry:bool,request_api:bool,export_cleanup_cron:bool} */ private function wordpress_signals(): array { $policy_id = (int) get_option( 'wp_page_for_privacy_policy', 0 ); $policy_status = $policy_id > 0 ? get_post_status( $policy_id ) : false; return array( 'privacy_policy' => 'publish' === $policy_status ? 'published' : ( $policy_id > 0 ? 'unpublished' : 'missing' ), 'exporter_registry' => false !== has_filter( 'wp_privacy_personal_data_exporters' ), 'eraser_registry' => false !== has_filter( 'wp_privacy_personal_data_erasers' ), 'request_api' => function_exists( 'wp_create_user_request' ) && function_exists( 'wp_send_user_request' ), 'export_cleanup_cron' => false !== wp_next_scheduled( 'wp_privacy_delete_old_export_files' ), ); } /** @return array */ private function error_finding( string $timestamp ): array { return $this->findings->build( $this->id(), 'error', array( 'observations' => array(), 'method' => 'wordpress_privacy_api_presence_only', 'limitations' => array( 'The local presence-only privacy adapter failed. No callback was invoked and no policy content, request record, personal data or exception was stored.' ), ), 0, count( self::SIGNALS ), 'error', $timestamp ); } }