*/ public static function defaults(): array { return array( 'site_id' => '', 'chat_enabled' => false, 'corner_button' => true, 'visibility' => array( 'mode' => 'all', 'items' => array(), ), 'non_production' => 'hide', 'consent' => array( 'mode' => 'immediate', 'category' => 'functional', ), 'hide_for_admins' => false, 'loader' => false, 'identity' => array( 'enabled' => false, 'roles' => array( 'subscriber', 'customer' ), ), 'read_on_publish' => true, ); } /** * All settings merged over the defaults. * * @return array */ public static function all(): array { $stored = get_option( self::OPTION, array() ); return self::sanitize( is_array( $stored ) ? $stored : array() ); } /** * One setting. * * @param string $key Setting key. * @return mixed */ public static function get( string $key ) { $all = self::all(); return $all[ $key ] ?? null; } /** * Merges changes into the stored settings and saves them. The merge starts * from what's stored when it's saved, so a change another request saved * meanwhile, such as on another tab, stays. * * @param array $changes Changed keys. * @return array The saved settings. */ public static function update( array $changes ): array { $saved = Option_Queue::change( self::OPTION, static function ( array $stored ) use ( $changes ): array { $current = self::sanitize( $stored ); foreach ( $changes as $key => $value ) { if ( ! array_key_exists( $key, $current ) ) { continue; } if ( is_array( $current[ $key ] ) && is_array( $value ) ) { // Nested settings merge one level deep; lists inside them are replaced whole. $current[ $key ] = array_merge( $current[ $key ], $value ); } else { $current[ $key ] = $value; } } return self::sanitize( $current ); }, true ); // Null when other requests kept changing them: then this change isn't // saved, and what's stored is. return self::sanitize( $saved ?? Option_Queue::read( self::OPTION ) ); } /** * Validates every setting, falling back to defaults for anything unknown. * * @param array $input Raw settings. * @return array */ public static function sanitize( array $input ): array { $defaults = self::defaults(); $out = $defaults; $out['site_id'] = self::sanitize_site_id( $input['site_id'] ?? '' ); $out['chat_enabled'] = ! empty( $input['chat_enabled'] ); $out['corner_button'] = array_key_exists( 'corner_button', $input ) ? (bool) $input['corner_button'] : true; $out['hide_for_admins'] = ! empty( $input['hide_for_admins'] ); $out['loader'] = ! empty( $input['loader'] ); $out['read_on_publish'] = array_key_exists( 'read_on_publish', $input ) ? (bool) $input['read_on_publish'] : true; $mode = $input['non_production'] ?? 'hide'; $out['non_production'] = in_array( $mode, self::NON_PRODUCTION_MODES, true ) ? $mode : 'hide'; $visibility = is_array( $input['visibility'] ?? null ) ? $input['visibility'] : array(); $visibility_mode = (string) ( $visibility['mode'] ?? 'all' ); $out['visibility'] = array( 'mode' => in_array( $visibility_mode, self::VISIBILITY_MODES, true ) ? $visibility_mode : 'all', 'items' => self::sanitize_visibility_items( $visibility['items'] ?? array() ), ); $consent = is_array( $input['consent'] ?? null ) ? $input['consent'] : array(); $consent_category = (string) ( $consent['category'] ?? '' ); $out['consent'] = array( 'mode' => 'consent' === ( $consent['mode'] ?? '' ) ? 'consent' : 'immediate', 'category' => in_array( $consent_category, self::CONSENT_CATEGORIES, true ) ? $consent_category : 'functional', ); $identity = is_array( $input['identity'] ?? null ) ? $input['identity'] : array(); $roles = is_array( $identity['roles'] ?? null ) ? $identity['roles'] : $defaults['identity']['roles']; $out['identity'] = array( 'enabled' => ! empty( $identity['enabled'] ), 'roles' => array_values( array_unique( array_filter( array_map( 'sanitize_key', $roles ) ) ) ), ); return $out; } /** * A Stand Site ID: the UUID shown in Sites, or "demo". * * @param mixed $value Raw value. */ public static function sanitize_site_id( $value ): string { $value = is_string( $value ) ? trim( $value ) : ''; return preg_match( '/^[A-Za-z0-9][A-Za-z0-9-]{2,63}$/', $value ) ? $value : ''; } /** * Where-chat-appears entries: pages and posts by ID, the front page, content * types, and archives and search results. * * @param mixed $items Raw entries. * @return array> */ private static function sanitize_visibility_items( $items ): array { if ( ! is_array( $items ) ) { return array(); } $out = array(); $seen = array(); foreach ( $items as $item ) { if ( ! is_array( $item ) ) { continue; } $type = $item['type'] ?? ''; if ( 'post' === $type && absint( $item['id'] ?? 0 ) > 0 ) { $clean = array( 'type' => 'post', 'id' => absint( $item['id'] ), ); } elseif ( 'post_type' === $type && '' !== sanitize_key( $item['name'] ?? '' ) ) { $clean = array( 'type' => 'post_type', 'name' => sanitize_key( $item['name'] ), ); } elseif ( 'front' === $type || 'archives' === $type ) { $clean = array( 'type' => $type ); } else { continue; } $key = wp_json_encode( $clean ); if ( isset( $seen[ $key ] ) ) { continue; } $seen[ $key ] = true; $out[] = $clean; if ( count( $out ) >= 500 ) { break; } } return $out; } }