has( $key ) ) {
throw new LogicException( 'AffiliateRoutingAdminServiceProvider requires OptionStore.' );
}
$options = $registry->get( $key );
if ( ! $options instanceof OptionStore ) {
throw new LogicException( 'AffiliateRoutingAdminServiceProvider expected OptionStore.' );
}
$this->repository = new OptionStoreAffiliateRoutingRepository( $options );
}
/**
* Register hooks.
*
* @param HookRegistrar $hooks Hooks.
*/
public function boot( HookRegistrar $hooks ): void {
$hooks->addAction( 'admin_post_' . self::FORM_ACTION, array( $this, 'onSavePost' ) );
}
/**
* Add submenu page.
*/
public function onAdminMenu(): void {
if ( ! function_exists( 'add_submenu_page' ) ) {
return;
}
add_submenu_page(
'affilun-products',
'affilun ASPルーティング(上級者向け)',
'ASPルーティング',
'manage_options',
'affilun-affiliate-routing',
array( $this, 'renderPage' )
);
}
/**
* Render page.
*/
public function renderPage(): void {
if ( $this->repository === null ) {
return;
}
$templates = $this->repository->loadRaw();
echo '
' . esc_html( 'ASPルーティング(上級者向け)' ) . '
';
echo '
' . esc_html( '通常は各プロバイダーの設定タブを使用してください。独自ASPや移行用URLテンプレートが必要な場合のみ、プロバイダー別テンプレートを任意設定できます。空欄は通常URLのままです。' ) . '
';
echo '
' . esc_html( '利用可能な変数: {url}, {encoded_url}, {provider}, {product_id}' ) . '
';
echo '
';
}
/**
* Save posted templates.
*/
public function onSavePost(): void {
if ( $this->repository === null || ! self::canManage() || ! self::verifyNonce() ) {
self::redirect( 'forbidden' );
return;
}
$postData = self::currentPostData();
$raw = $postData['templates'] ?? array();
$values = array();
if ( is_array( $raw ) ) {
foreach ( OptionStoreAffiliateRoutingRepository::PROVIDERS as $provider ) {
$value = $raw[ $provider ] ?? '';
$values[ $provider ] = is_scalar( $value ) ? (string) $value : '';
}
}
try {
$this->repository->saveRaw( $values, self::readBool( $postData, 'enabled' ) );
self::redirect( 'saved' );
} catch ( Throwable $unused ) {
unset( $unused );
self::redirect( 'error' );
}
}
/**
* Read a checkbox-style boolean from a form payload.
*
* @param array $source Source array.
* @param string $key Field key.
*/
private static function readBool( array $source, string $key ): bool {
if ( ! array_key_exists( $key, $source ) ) {
return false;
}
$value = $source[ $key ];
return ! ( $value === false || $value === 0 || $value === '0' || $value === '' || $value === null );
}
/**
* Capability check.
*/
private static function canManage(): bool {
return function_exists( 'current_user_can' ) && current_user_can( 'manage_options' );
}
/**
* Nonce check.
*/
private static function verifyNonce(): bool {
$postData = self::currentPostData();
$value = $postData[ self::NONCE_FIELD ] ?? '';
$nonce = is_scalar( $value ) ? self::sanitizeTextField( (string) $value ) : '';
return $nonce !== '' && function_exists( 'wp_verify_nonce' )
&& (bool) wp_verify_nonce( $nonce, self::NONCE_ACTION );
}
/**
* Return sanitized POST data for the affiliate-routing form.
*
* @return array
*/
private static function currentPostData(): array {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified before callers use the payload.
$raw = $_POST;
$unslashed = function_exists( 'wp_unslash' ) ? wp_unslash( $raw ) : $raw;
if ( ! is_array( $unslashed ) ) {
return array();
}
$result = array();
foreach ( $unslashed as $key => $value ) {
if ( ! is_string( $key ) ) {
continue;
}
$result[ $key ] = self::sanitizePostValue( $key, $value );
}
return $result;
}
/**
* Sanitize one POST value recursively.
*
* @param string $key Field key.
* @param mixed $value Field value.
*
* @return mixed
*/
private static function sanitizePostValue( string $key, $value ) {
if ( is_array( $value ) ) {
$result = array();
foreach ( $value as $childKey => $childValue ) {
if ( ! is_string( $childKey ) ) {
continue;
}
$result[ $childKey ] = self::sanitizePostValue( $childKey, $childValue );
}
return $result;
}
if ( ! is_scalar( $value ) ) {
return '';
}
$string = (string) $value;
if ( $key === 'action' || $key === 'enabled' || $key === self::NONCE_FIELD ) {
return self::sanitizeTextField( $string );
}
return self::sanitizeRoutingTemplate( $string );
}
/**
* Sanitize a routing template while preserving URL-template placeholders.
*
* @param string $value Raw template.
*/
private static function sanitizeRoutingTemplate( string $value ): string {
$value = self::sanitizeTextField( $value );
return trim( preg_replace( '/[\x00-\x1F\x7F]/', '', $value ) ?? $value );
}
/**
* Sanitize a single-line text field.
*
* @param string $value Raw value.
*/
private static function sanitizeTextField( string $value ): string {
if ( function_exists( 'sanitize_text_field' ) ) {
return (string) sanitize_text_field( $value );
}
$value = preg_replace( '/<[^>]*>/', '', $value ) ?? $value;
return trim( (string) preg_replace( '/\s+/', ' ', $value ) );
}
/**
* Redirect back to settings page.
*
* @param string $status Status token.
*/
private static function redirect( string $status ): void {
$url = self::adminPageUrl();
$url .= ( str_contains( $url, '?' ) ? '&' : '?' ) . 'affilun_status=' . rawurlencode( $status );
if ( function_exists( 'wp_safe_redirect' ) ) {
wp_safe_redirect( $url );
exit;
}
}
/**
* Admin-post URL.
*/
private static function adminPostUrl(): string {
return function_exists( 'admin_url' ) ? (string) admin_url( 'admin-post.php' ) : 'admin-post.php';
}
/**
* Admin page URL.
*/
private static function adminPageUrl(): string {
return function_exists( 'admin_url' )
? (string) admin_url( 'options-general.php?page=affilun-settings#affilun-settings-tab-asp' )
: 'options-general.php?page=affilun-settings#affilun-settings-tab-asp';
}
}