resolveRepository( $registry ); $factory = $this->resolveViewModelFactory( $registry ); $renderer = $this->resolveProductCardRenderer( $registry ); $settings = $this->resolveSettingsRepository( $registry ); $brands = $this->resolveBrandSettingsRepository( $registry ); $catalog = $this->resolveOptionalCatalog( $registry ); $this->brandSettings = $brands; $this->repository = $repository; if ( $catalog !== null ) { $this->syncService = new AmazonDiscountSyncService( $catalog, $repository, null, $brands->load() ); $registry->bind( self::BINDING_SYNC_SERVICE, $this->syncService ); } $shortcode = new AmazonDiscountShortcodeRenderer( $repository, $factory, $renderer, $settings ); $adminCard = new AmazonDiscountShortcodeCardRenderer( $repository, $shortcode, $this->syncService !== null, $brands ); $registry->bind( self::BINDING_SHORTCODE_RENDERER, $shortcode ); $registry->bind( self::BINDING_ADMIN_CARD_RENDERER, $adminCard ); } /** * Register scheduling and cron callbacks. * * @param HookRegistrar $hooks Hook registrar. */ public function boot( HookRegistrar $hooks ): void { $hooks->addAction( 'affilun_activate', array( $this, 'ensureScheduled' ) ); $hooks->addAction( 'init', array( $this, 'ensureScheduled' ) ); $hooks->addAction( self::HOOK_DAILY_SYNC, array( $this, 'runSync' ) ); $hooks->addAction( self::HOOK_MANUAL_SYNC, array( $this, 'runSync' ) ); $hooks->addAction( 'admin_post_' . AmazonDiscountSyncFormToken::FORM_ACTION, array( $this, 'handleManualSyncRequest' ) ); $hooks->addAction( 'admin_post_' . AmazonDiscountConfigFormToken::FORM_ACTION, array( $this, 'handleConfigRequest' ) ); } /** * Ensure the daily synchronization event exists. */ public function ensureScheduled(): void { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_event' ) ) { return; } if ( ! wp_next_scheduled( self::HOOK_DAILY_SYNC ) ) { wp_schedule_event( time() + 300, 'daily', self::HOOK_DAILY_SYNC ); } } /** * Run one complete discount synchronization. */ public function runSync(): void { if ( $this->syncService === null ) { return; } try { $result = $this->syncService->sync(); if ( $result->errorsByCategory() !== array() && function_exists( 'error_log' ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Cron partial-failure surface. error_log( 'AFFILUN Amazon discount sync partial failure: ' . wp_json_encode( $result->errorsByCategory() ) ); } } catch ( Throwable $error ) { if ( function_exists( 'error_log' ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Cron failure surface. error_log( 'AFFILUN Amazon discount sync failed: ' . $error->getMessage() ); } } } /** * Validate the admin request and queue a due-now background sync. */ public function handleManualSyncRequest(): void { if ( ! function_exists( 'current_user_can' ) || ! current_user_can( AdminCapability::CAP_RUN_SYNC ) ) { $this->redirectManualSyncStatus( 'forbidden' ); return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Unslashed and sanitized by sanitizeText(), then verified immediately below. $rawNonce = $_POST[ AmazonDiscountSyncFormToken::NONCE_FIELD ] ?? ''; $nonce = is_scalar( $rawNonce ) ? $this->sanitizeText( (string) $rawNonce ) : ''; if ( $nonce === '' ) { $this->redirectManualSyncStatus( 'invalid_nonce' ); return; } if ( ! function_exists( 'wp_verify_nonce' ) ) { $this->redirectManualSyncStatus( 'invalid_nonce' ); return; } if ( ! wp_verify_nonce( $nonce, AmazonDiscountSyncFormToken::NONCE_ACTION ) ) { $this->redirectManualSyncStatus( 'invalid_nonce' ); return; } if ( $this->syncService === null ) { $this->redirectManualSyncStatus( 'unavailable' ); return; } if ( ! function_exists( 'wp_schedule_single_event' ) ) { $this->redirectManualSyncStatus( 'error' ); return; } $alreadyQueued = function_exists( 'wp_next_scheduled' ) && wp_next_scheduled( self::HOOK_MANUAL_SYNC ) !== false; if ( ! $alreadyQueued && ! wp_schedule_single_event( time(), self::HOOK_MANUAL_SYNC ) ) { $this->redirectManualSyncStatus( 'error' ); return; } if ( function_exists( 'spawn_cron' ) ) { spawn_cron( time() ); } $this->redirectManualSyncStatus( 'queued' ); } /** * Save priority brands and redirect to the updated shortcode preview. */ public function handleConfigRequest(): void { if ( ! function_exists( 'current_user_can' ) || ! current_user_can( AdminCapability::CAP_RUN_SYNC ) ) { $this->redirectConfigRequest( array() ); return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Unslashed and sanitized below, then verified. $rawNonce = $_POST[ AmazonDiscountConfigFormToken::NONCE_FIELD ] ?? ''; $nonce = is_scalar( $rawNonce ) ? $this->sanitizeText( (string) $rawNonce ) : ''; if ( $nonce === '' ) { $this->redirectConfigRequest( array() ); return; } if ( ! function_exists( 'wp_verify_nonce' ) ) { $this->redirectConfigRequest( array() ); return; } if ( ! wp_verify_nonce( $nonce, AmazonDiscountConfigFormToken::NONCE_ACTION ) ) { $this->redirectConfigRequest( array() ); return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above; normalized through closed value objects below. $rawBrands = $_POST['affilun_discount_brands'] ?? ''; $brands = AmazonDiscountBrand::parse( is_string( $rawBrands ) && function_exists( 'wp_unslash' ) ? wp_unslash( $rawBrands ) : $rawBrands ); $previousBrands = $this->brandSettings !== null ? $this->brandSettings->load() : array(); if ( $this->brandSettings !== null ) { $this->brandSettings->save( $brands ); } if ( $this->repository !== null ) { foreach ( array_diff( $previousBrands, $brands ) as $removedBrand ) { $this->repository->deleteBrand( $removedBrand ); } } $rawCategories = $_POST['affilun_discount_categories'] ?? array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above; unslashed next. $rawCategories = function_exists( 'wp_unslash' ) ? wp_unslash( $rawCategories ) : $rawCategories; $categories = is_array( $rawCategories ) ? AmazonDiscountCategory::normalize( array_values( $rawCategories ) ) : array(); if ( $categories === array() ) { $categories = AmazonDiscountCategory::DEFAULT_SELECTION; } $rawSort = $_POST['affilun_discount_sort'] ?? ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above; sanitizeText unslashes and sanitizes next. $sort = AmazonDiscountShortcodeRenderer::sortFromAttributes( array( 'sort' => is_scalar( $rawSort ) ? $this->sanitizeText( (string) $rawSort ) : '' ) ); $rawCount = $_POST['affilun_discount_per_category'] ?? ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above; sanitizeText unslashes and sanitizes next. $count = AmazonDiscountShortcodeRenderer::perCategoryFromAttributes( array( 'per_category' => is_scalar( $rawCount ) ? $this->sanitizeText( (string) $rawCount ) : '' ) ); $this->redirectConfigRequest( array( 'affilun_discount_brands' => implode( ',', $brands ), 'affilun_discount_categories' => $categories, 'affilun_discount_sort' => $sort, 'affilun_discount_per_category' => $count, ) ); } /** * Sanitize a submitted scalar without requiring WordPress in unit tests. * * @param string $value Submitted value. */ private function sanitizeText( string $value ): string { $value = function_exists( 'wp_unslash' ) ? wp_unslash( $value ) : $value; return function_exists( 'sanitize_text_field' ) ? sanitize_text_field( $value ) : trim( $value ); } /** * Redirect back to the products page with a status token. * * @param string $status Closed status token. */ private function redirectManualSyncStatus( string $status ): void { $path = 'admin.php?page=affilun-products&' . AmazonDiscountSyncFormToken::STATUS_QUERY . '=' . rawurlencode( $status ); $url = function_exists( 'admin_url' ) ? admin_url( $path ) : $path; if ( function_exists( 'wp_safe_redirect' ) ) { wp_safe_redirect( $url ); exit; } } /** * Redirect back to the products page with normalized preview values. * * @param array $query Preview query. */ private function redirectConfigRequest( array $query ): void { $params = array_merge( array( 'page' => 'affilun-products' ), $query ); $path = 'admin.php?' . http_build_query( $params, '', '&', PHP_QUERY_RFC3986 ); $url = function_exists( 'admin_url' ) ? admin_url( $path ) : $path; if ( function_exists( 'wp_safe_redirect' ) ) { wp_safe_redirect( $url ); exit; } } /** * Resolve the discount repository. * * @param ServiceProviderRegistry $registry Service registry. */ private function resolveRepository( ServiceProviderRegistry $registry ): AmazonDiscountRepository { $key = PersistenceServiceProvider::BINDING_AMAZON_DISCOUNT_REPOSITORY; if ( ! $registry->has( $key ) ) { throw new LogicException( 'AmazonDiscountServiceProvider requires AmazonDiscountRepository.' ); } $value = $registry->get( $key ); if ( ! $value instanceof AmazonDiscountRepository ) { throw new LogicException( 'AmazonDiscountServiceProvider expected AmazonDiscountRepository.' ); } return $value; } /** * Resolve the standard card view-model factory. * * @param ServiceProviderRegistry $registry Service registry. */ private function resolveViewModelFactory( ServiceProviderRegistry $registry ): ProductCardViewModelFactory { $key = RenderServiceProvider::BINDING_PRODUCT_CARD_VIEW_MODEL_FACTORY; $value = $registry->has( $key ) ? $registry->get( $key ) : null; if ( ! $value instanceof ProductCardViewModelFactory ) { throw new LogicException( 'AmazonDiscountServiceProvider requires ProductCardViewModelFactory.' ); } return $value; } /** * Resolve the standard card renderer. * * @param ServiceProviderRegistry $registry Service registry. */ private function resolveProductCardRenderer( ServiceProviderRegistry $registry ): ProductCardRenderer { $key = RenderServiceProvider::BINDING_PRODUCT_CARD_RENDERER; $value = $registry->has( $key ) ? $registry->get( $key ) : null; if ( ! $value instanceof ProductCardRenderer ) { throw new LogicException( 'AmazonDiscountServiceProvider requires ProductCardRenderer.' ); } return $value; } /** * Resolve the same option-backed settings used by normal product cards. * * @param ServiceProviderRegistry $registry Service registry. */ private function resolveSettingsRepository( ServiceProviderRegistry $registry ): SettingsRepository { $key = PersistenceServiceProvider::BINDING_OPTION_STORE; $value = $registry->has( $key ) ? $registry->get( $key ) : null; if ( ! $value instanceof OptionStore ) { throw new LogicException( 'AmazonDiscountServiceProvider requires OptionStore.' ); } return new OptionStoreSettingsRepository( $value ); } /** * Resolve option-backed priority-brand settings. * * @param ServiceProviderRegistry $registry Service registry. */ private function resolveBrandSettingsRepository( ServiceProviderRegistry $registry ): AmazonDiscountBrandSettingsRepository { $key = PersistenceServiceProvider::BINDING_OPTION_STORE; $value = $registry->has( $key ) ? $registry->get( $key ) : null; if ( ! $value instanceof OptionStore ) { throw new LogicException( 'AmazonDiscountServiceProvider requires OptionStore.' ); } return new OptionStoreAmazonDiscountBrandSettingsRepository( $value ); } /** * Resolve Amazon discount discovery when credentials are available. * * @param ServiceProviderRegistry $registry Service registry. */ private function resolveOptionalCatalog( ServiceProviderRegistry $registry ): ?AmazonDiscountCatalog { $key = ProviderServiceProvider::BINDING_PROVIDER_REGISTRY; $value = $registry->has( $key ) ? $registry->get( $key ) : null; if ( ! $value instanceof ProviderRegistry || ! $value->has( 'amazon' ) ) { return null; } $provider = $value->get( 'amazon' ); return $provider instanceof AmazonDiscountCatalog ? $provider : null; } }