$requestData GET-like request data. */ public function render( array $requestData ): void { $brands = $this->brands( $requestData ); $categories = $this->categories( $requestData ); $sort = AmazonDiscountShortcodeRenderer::sortFromAttributes( array( 'sort' => $requestData['affilun_discount_sort'] ?? null ) ); $perCategory = AmazonDiscountShortcodeRenderer::perCategoryFromAttributes( array( 'per_category' => $requestData['affilun_discount_per_category'] ?? null ) ); $brandAttribute = $brands === array() ? '' : sprintf( ' brands="%s"', implode( ',', $brands ) ); $shortcode = sprintf( '[affilun_amazon_discounts%s categories="%s" sort="%s" per_category="%d"]', $brandAttribute, implode( ',', $categories ), $sort, $perCategory ); echo '
'; echo '

' . esc_html( 'Amazon割引商品ショートコード' ) . '

'; echo '

' . esc_html( 'Creators APIから日次取得した割引商品を、カテゴリ見出しとaffilun商品カードで表示します。' ) . '

'; $this->renderSyncNotice( $requestData ); $this->renderStatus(); $this->renderManualSyncForm(); $canConfigureBrands = ! function_exists( 'current_user_can' ) || current_user_can( AdminCapability::CAP_RUN_SYNC ); $path = $canConfigureBrands ? 'admin-post.php' : 'admin.php'; $url = function_exists( 'admin_url' ) ? admin_url( $path ) : $path; echo '
'; if ( $canConfigureBrands ) { echo ''; } else { echo ''; } if ( $canConfigureBrands && function_exists( 'wp_nonce_field' ) ) { wp_nonce_field( AmazonDiscountConfigFormToken::NONCE_ACTION, AmazonDiscountConfigFormToken::NONCE_FIELD ); } echo '

'; echo ''; echo ' ' . esc_html( $canConfigureBrands ? 'カンマ区切り、最大10ブランド。保存後の取得からブランド別商品が追加されます。' : '優先ブランドの変更には同期実行権限が必要です。' ) . '

'; echo '
' . esc_html( '表示カテゴリ' ) . ''; foreach ( AmazonDiscountCategory::LABELS as $value => $label ) { $checked = in_array( $value, $categories, true ) ? ' checked="checked"' : ''; echo ''; } echo '
'; echo ' '; echo ' '; echo ' '; echo ' '; echo ''; echo '
'; echo '

'; echo ''; echo '
'; echo '

' . esc_html( 'プレビュー' ) . '

'; $previewAttributes = array( 'brands' => implode( ',', $brands ), 'categories' => implode( ',', $categories ), 'sort' => $sort, 'per_category' => $perCategory, ); echo $this->shortcodeRenderer->render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Canonical renderer returns escaped plugin HTML. $previewAttributes, true ); echo '
'; } /** * Render the local snapshot status. */ private function renderStatus(): void { $count = $this->repository->count(); $latest = $this->repository->latestFetchedAt(); $text = $latest === null ? sprintf( '保存済み商品: %d件(初回の日次取得待ち)', $count ) : sprintf( '保存済み商品: %d件 / 最終取得: %s JST', $count, $latest->setTimezone( new DateTimeZone( 'Asia/Tokyo' ) )->format( 'Y-m-d H:i' ) ); echo '

' . esc_html( $text ) . '

'; if ( function_exists( 'esc_html' ) && $latest !== null && $latest <= AmazonDiscountDisplayPolicy::cutoff( $this->clock->now() ) ) { echo '

' . esc_html( '保存データは表示期限切れのため非表示です。これは取得失敗の通知とは別の鮮度判定です。' ) . '

'; } if ( function_exists( 'esc_html' ) ) { echo '

' . esc_html( '保存件数と表示件数は異なります。取得から24時間以上経過した商品、選択価格の割引を確認できない商品、旧形式のデータは非表示です。同期失敗時も表示期限は延長されません。再取得後に表示が再開します。' ) . '

'; } } /** * Render the nonce-protected background synchronization control. */ private function renderManualSyncForm(): void { $path = 'admin-post.php'; $url = function_exists( 'admin_url' ) ? admin_url( $path ) : $path; $canRunSync = ! function_exists( 'current_user_can' ) || current_user_can( AdminCapability::CAP_RUN_SYNC ); $enabled = $this->manualSyncAvailable && $canRunSync; echo '
'; echo ''; if ( function_exists( 'wp_nonce_field' ) ) { wp_nonce_field( AmazonDiscountSyncFormToken::NONCE_ACTION, AmazonDiscountSyncFormToken::NONCE_FIELD ); } echo ''; echo ' ' . esc_html( match ( true ) { ! $canRunSync => '手動取得には同期実行権限が必要です。', ! $this->manualSyncAvailable => 'Amazon Creators APIの資格情報を設定すると利用できます。', default => 'バックグラウンドで取得を開始します。完了後にこの画面を再読み込みしてください。', } ) . ''; echo '
'; } /** * Render the result of a manual synchronization request. * * @param array $requestData GET-like request data. */ private function renderSyncNotice( array $requestData ): void { $status = is_string( $requestData[ AmazonDiscountSyncFormToken::STATUS_QUERY ] ?? null ) ? $requestData[ AmazonDiscountSyncFormToken::STATUS_QUERY ] : ''; $notice = match ( $status ) { 'queued' => array( 'notice notice-success inline', 'Amazon割引商品の手動取得を開始しました。' ), 'forbidden' => array( 'notice notice-error inline', '手動取得を実行する権限がありません。' ), 'invalid_nonce' => array( 'notice notice-error inline', '手動取得リクエストを確認できませんでした。' ), 'unavailable' => array( 'notice notice-error inline', 'Amazon Creators APIが設定されていません。' ), 'error' => array( 'notice notice-error inline', '手動取得を開始できませんでした。' ), default => null, }; if ( $notice === null ) { return; } echo '

' . self::escapeHtml( $notice[1] ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by the local HTML helper. . '

'; } /** * Escape visible notice text. * * @param string $value Raw text. */ private static function escapeHtml( string $value ): string { return htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ); } /** * Escape a notice class attribute. * * @param string $value Raw attribute value. */ private static function escapeAttribute( string $value ): string { return htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ); } /** * @param array $requestData Request data. * * @return list */ private function categories( array $requestData ): array { $raw = $requestData['affilun_discount_categories'] ?? null; if ( is_array( $raw ) ) { $categories = AmazonDiscountCategory::normalize( array_values( $raw ) ); if ( $categories !== array() ) { return $categories; } } return AmazonDiscountCategory::DEFAULT_SELECTION; } /** * @param array $requestData Request data. * * @return list */ private function brands( array $requestData ): array { if ( array_key_exists( 'affilun_discount_brands', $requestData ) ) { return AmazonDiscountBrand::parse( $requestData['affilun_discount_brands'] ); } return $this->brandSettings !== null ? $this->brandSettings->load() : array(); } }