diff --git a/src/Settings/Settings.php b/src/Settings/Settings.php index 846bda0..534929a 100644 --- a/src/Settings/Settings.php +++ b/src/Settings/Settings.php @@ -5,29 +5,39 @@ namespace WPContentSync\Settings; final class Settings { private const LOGGING_LEVELS = array( 'error', 'warning', 'info', 'debug' ); private const CONFLICT_STRATEGIES = array( 'last_write_wins', 'manual_review' ); + private const DIRECTIONS = array( 'push', 'pull' ); + private const CONTENT_TYPES = array( 'posts', 'terms', 'media', 'custom_post_types' ); + private const MIN_LOGS = 10; + private const MAX_LOGS = 1000; /** - * @var array + * @var array, url_mappings: array}> */ private array $sync_pairs; private string $logging_level; private bool $automatic_url_replacement; private string $conflict_strategy; + private int $log_retention; + private bool $debug_logging; /** - * @param array $sync_pairs Sync pairs. + * @param array, url_mappings: array}> $sync_pairs Sync pairs. */ private function __construct( array $sync_pairs, string $logging_level, bool $automatic_url_replacement, - string $conflict_strategy + string $conflict_strategy, + int $log_retention, + bool $debug_logging ) { $this->sync_pairs = $sync_pairs; $this->logging_level = $logging_level; $this->automatic_url_replacement = $automatic_url_replacement; $this->conflict_strategy = $conflict_strategy; + $this->log_retention = $log_retention; + $this->debug_logging = $debug_logging; } /** @@ -54,12 +64,16 @@ final class Settings { self::sanitizeSyncPairs( $data['sync_pairs'] ?? array() ), $logging_level, $automatic_url_replacement, - $conflict_strategy + $conflict_strategy, + self::sanitizeLogRetention( $data['log_retention'] ?? 200 ), + array_key_exists( 'debug_logging', $data ) + ? self::sanitizeBoolean( $data['debug_logging'] ) + : false ); } /** - * @return array + * @return array, url_mappings: array}> */ public function syncPairs(): array { return $this->sync_pairs; @@ -77,6 +91,14 @@ final class Settings { return $this->conflict_strategy; } + public function logRetention(): int { + return $this->log_retention; + } + + public function debugLoggingEnabled(): bool { + return $this->debug_logging; + } + /** * @return array */ @@ -86,6 +108,8 @@ final class Settings { 'logging_level' => $this->logging_level, 'automatic_url_replacement' => $this->automatic_url_replacement, 'conflict_strategy' => $this->conflict_strategy, + 'log_retention' => $this->log_retention, + 'debug_logging' => $this->debug_logging, ); } @@ -114,7 +138,7 @@ final class Settings { /** * @param mixed $pairs Raw sync pairs. - * @return array + * @return array, url_mappings: array}> */ private static function sanitizeSyncPairs( $pairs ): array { if ( ! is_array( $pairs ) ) { @@ -131,15 +155,86 @@ final class Settings { $name = sanitize_text_field( (string) ( $pair['name'] ?? '' ) ); $source_url = esc_url_raw( (string) ( $pair['source_url'] ?? '' ) ); $destination_url = esc_url_raw( (string) ( $pair['destination_url'] ?? '' ) ); + $username = sanitize_text_field( (string) ( $pair['username'] ?? '' ) ); + $password = sanitize_text_field( (string) ( $pair['application_password'] ?? '' ) ); + $direction = self::sanitizeChoice( $pair['default_direction'] ?? 'push', self::DIRECTIONS, 'push' ); + $content_types = self::sanitizeContentTypes( $pair['content_types'] ?? self::CONTENT_TYPES ); + $url_mappings = self::sanitizeUrlMappings( $pair['url_mappings'] ?? array() ); if ( '' === $name || '' === $source_url || '' === $destination_url ) { continue; } $sanitized[] = array( - 'name' => $name, - 'source_url' => $source_url, - 'destination_url' => $destination_url, + 'name' => $name, + 'source_url' => $source_url, + 'destination_url' => $destination_url, + 'username' => $username, + 'application_password' => $password, + 'default_direction' => $direction, + 'content_types' => $content_types, + 'url_mappings' => $url_mappings, + ); + } + + return $sanitized; + } + + /** + * @param mixed $value Raw log retention. + */ + private static function sanitizeLogRetention( $value ): int { + return min( self::MAX_LOGS, max( self::MIN_LOGS, (int) $value ) ); + } + + /** + * @param mixed $content_types Raw content type list. + * @return array + */ + private static function sanitizeContentTypes( $content_types ): array { + if ( ! is_array( $content_types ) ) { + return self::CONTENT_TYPES; + } + + $sanitized = array(); + + foreach ( $content_types as $content_type ) { + $content_type = sanitize_text_field( (string) $content_type ); + + if ( in_array( $content_type, self::CONTENT_TYPES, true ) && ! in_array( $content_type, $sanitized, true ) ) { + $sanitized[] = $content_type; + } + } + + return $sanitized; + } + + /** + * @param mixed $mappings Raw URL mappings. + * @return array + */ + private static function sanitizeUrlMappings( $mappings ): array { + if ( ! is_array( $mappings ) ) { + return array(); + } + + $sanitized = array(); + + foreach ( $mappings as $mapping ) { + if ( ! is_array( $mapping ) ) { + continue; + } + + $source = esc_url_raw( (string) ( $mapping['source'] ?? '' ) ); + $destination = esc_url_raw( (string) ( $mapping['destination'] ?? '' ) ); + + if ( '' === $source || '' === $destination ) { + continue; + } + + $sanitized[] = array( + 'source' => $source, + 'destination' => $destination, ); } diff --git a/tests/Unit/SettingsTest.php b/tests/Unit/SettingsTest.php index cd02848..40564be 100644 --- a/tests/Unit/SettingsTest.php +++ b/tests/Unit/SettingsTest.php @@ -57,6 +57,53 @@ class SettingsTest extends TestCase { self::assertTrue( $settings->automaticUrlReplacementEnabled() ); } + public function test_it_sanitizes_full_admin_workflow_settings(): void { + $settings = Settings::fromArray( + array( + 'sync_pairs' => array( + array( + 'name' => 'Production to Staging', + 'source_url' => 'https://example.test/', + 'destination_url' => 'https://staging.example.test/', + 'username' => '', + 'application_password' => 'secret app password', + 'default_direction' => 'push', + 'content_types' => array( 'posts', 'terms', 'media', 'bad_type' ), + 'url_mappings' => array( + array( + 'source' => 'https://example.test', + 'destination' => 'https://staging.example.test', + ), + ), + ), + ), + 'log_retention' => '50', + 'debug_logging' => '1', + ) + ); + + $pairs = $settings->syncPairs(); + + self::assertSame( 'Production to Staging', $pairs[0]['name'] ); + self::assertSame( 'https://example.test/', $pairs[0]['source_url'] ); + self::assertSame( 'https://staging.example.test/', $pairs[0]['destination_url'] ); + self::assertSame( 'codex', $pairs[0]['username'] ); + self::assertSame( 'secret app password', $pairs[0]['application_password'] ); + self::assertSame( 'push', $pairs[0]['default_direction'] ); + self::assertSame( array( 'posts', 'terms', 'media' ), $pairs[0]['content_types'] ); + self::assertSame( + array( + array( + 'source' => 'https://example.test', + 'destination' => 'https://staging.example.test', + ), + ), + $pairs[0]['url_mappings'] + ); + self::assertSame( 50, $settings->logRetention() ); + self::assertTrue( $settings->debugLoggingEnabled() ); + } + public function test_it_serializes_to_array(): void { $settings = Settings::fromArray( array( @@ -74,14 +121,21 @@ class SettingsTest extends TestCase { array( 'sync_pairs' => array( array( - 'name' => 'Staging', - 'source_url' => 'https://example.test', - 'destination_url' => 'https://staging.example.test', + 'name' => 'Staging', + 'source_url' => 'https://example.test', + 'destination_url' => 'https://staging.example.test', + 'username' => '', + 'application_password' => '', + 'default_direction' => 'push', + 'content_types' => array( 'posts', 'terms', 'media', 'custom_post_types' ), + 'url_mappings' => array(), ), ), 'logging_level' => 'warning', 'automatic_url_replacement' => true, 'conflict_strategy' => 'last_write_wins', + 'log_retention' => 200, + 'debug_logging' => false, ), $settings->toArray() );