add file transport implementation #2

Open
keith wants to merge 30 commits from feature/url-transformer-plan into main
2 changed files with 161 additions and 12 deletions
Showing only changes of commit 27919ff11f - Show all commits
+101 -6
View File
@@ -5,29 +5,39 @@ namespace WPContentSync\Settings;
final class Settings { final class Settings {
private const LOGGING_LEVELS = array( 'error', 'warning', 'info', 'debug' ); private const LOGGING_LEVELS = array( 'error', 'warning', 'info', 'debug' );
private const CONFLICT_STRATEGIES = array( 'last_write_wins', 'manual_review' ); 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<int, array{name: string, source_url: string, destination_url: string}> * @var array<int, array{name: string, source_url: string, destination_url: string, username: string, application_password: string, default_direction: string, content_types: array<int, string>, url_mappings: array<int, array{source: string, destination: string}>}>
*/ */
private array $sync_pairs; private array $sync_pairs;
private string $logging_level; private string $logging_level;
private bool $automatic_url_replacement; private bool $automatic_url_replacement;
private string $conflict_strategy; private string $conflict_strategy;
private int $log_retention;
private bool $debug_logging;
/** /**
* @param array<int, array{name: string, source_url: string, destination_url: string}> $sync_pairs Sync pairs. * @param array<int, array{name: string, source_url: string, destination_url: string, username: string, application_password: string, default_direction: string, content_types: array<int, string>, url_mappings: array<int, array{source: string, destination: string}>}> $sync_pairs Sync pairs.
*/ */
private function __construct( private function __construct(
array $sync_pairs, array $sync_pairs,
string $logging_level, string $logging_level,
bool $automatic_url_replacement, bool $automatic_url_replacement,
string $conflict_strategy string $conflict_strategy,
int $log_retention,
bool $debug_logging
) { ) {
$this->sync_pairs = $sync_pairs; $this->sync_pairs = $sync_pairs;
$this->logging_level = $logging_level; $this->logging_level = $logging_level;
$this->automatic_url_replacement = $automatic_url_replacement; $this->automatic_url_replacement = $automatic_url_replacement;
$this->conflict_strategy = $conflict_strategy; $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() ), self::sanitizeSyncPairs( $data['sync_pairs'] ?? array() ),
$logging_level, $logging_level,
$automatic_url_replacement, $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<int, array{name: string, source_url: string, destination_url: string}> * @return array<int, array{name: string, source_url: string, destination_url: string, username: string, application_password: string, default_direction: string, content_types: array<int, string>, url_mappings: array<int, array{source: string, destination: string}>}>
*/ */
public function syncPairs(): array { public function syncPairs(): array {
return $this->sync_pairs; return $this->sync_pairs;
@@ -77,6 +91,14 @@ final class Settings {
return $this->conflict_strategy; return $this->conflict_strategy;
} }
public function logRetention(): int {
return $this->log_retention;
}
public function debugLoggingEnabled(): bool {
return $this->debug_logging;
}
/** /**
* @return array<string, mixed> * @return array<string, mixed>
*/ */
@@ -86,6 +108,8 @@ final class Settings {
'logging_level' => $this->logging_level, 'logging_level' => $this->logging_level,
'automatic_url_replacement' => $this->automatic_url_replacement, 'automatic_url_replacement' => $this->automatic_url_replacement,
'conflict_strategy' => $this->conflict_strategy, '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. * @param mixed $pairs Raw sync pairs.
* @return array<int, array{name: string, source_url: string, destination_url: string}> * @return array<int, array{name: string, source_url: string, destination_url: string, username: string, application_password: string, default_direction: string, content_types: array<int, string>, url_mappings: array<int, array{source: string, destination: string}>}>
*/ */
private static function sanitizeSyncPairs( $pairs ): array { private static function sanitizeSyncPairs( $pairs ): array {
if ( ! is_array( $pairs ) ) { if ( ! is_array( $pairs ) ) {
@@ -131,6 +155,11 @@ final class Settings {
$name = sanitize_text_field( (string) ( $pair['name'] ?? '' ) ); $name = sanitize_text_field( (string) ( $pair['name'] ?? '' ) );
$source_url = esc_url_raw( (string) ( $pair['source_url'] ?? '' ) ); $source_url = esc_url_raw( (string) ( $pair['source_url'] ?? '' ) );
$destination_url = esc_url_raw( (string) ( $pair['destination_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 ) { if ( '' === $name || '' === $source_url || '' === $destination_url ) {
continue; continue;
@@ -140,6 +169,72 @@ final class Settings {
'name' => $name, 'name' => $name,
'source_url' => $source_url, 'source_url' => $source_url,
'destination_url' => $destination_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<int, string>
*/
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<int, array{source: string, destination: string}>
*/
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,
); );
} }
+54
View File
@@ -57,6 +57,53 @@ class SettingsTest extends TestCase {
self::assertTrue( $settings->automaticUrlReplacementEnabled() ); self::assertTrue( $settings->automaticUrlReplacementEnabled() );
} }
public function test_it_sanitizes_full_admin_workflow_settings(): void {
$settings = Settings::fromArray(
array(
'sync_pairs' => array(
array(
'name' => '<b>Production to Staging</b>',
'source_url' => 'https://example.test/',
'destination_url' => 'https://staging.example.test/',
'username' => '<script>codex</script>',
'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 { public function test_it_serializes_to_array(): void {
$settings = Settings::fromArray( $settings = Settings::fromArray(
array( array(
@@ -77,11 +124,18 @@ class SettingsTest extends TestCase {
'name' => 'Staging', 'name' => 'Staging',
'source_url' => 'https://example.test', 'source_url' => 'https://example.test',
'destination_url' => 'https://staging.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', 'logging_level' => 'warning',
'automatic_url_replacement' => true, 'automatic_url_replacement' => true,
'conflict_strategy' => 'last_write_wins', 'conflict_strategy' => 'last_write_wins',
'log_retention' => 200,
'debug_logging' => false,
), ),
$settings->toArray() $settings->toArray()
); );