HEX
Server: LiteSpeed
System: Linux prometheus.hongkongserver.net 4.18.0-553.134.1.el8_10.x86_64 #1 SMP Tue Jun 16 16:05:57 EDT 2026 x86_64
User: ayxmplky (1112)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/ayxmplky/public_html/wp-content/themes/tactic/functions.php
<?php
/**
 * TACTIC Industrial — functions.php
 * WordPress 6.0+ | PHP 8.0+ | Polylang (двуязычность: 中文 / English)
 *
 * @package tactic
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

define( 'TACTIC_VERSION', '1.0.0' );
define( 'TACTIC_DIR', get_template_directory() );
define( 'TACTIC_URI', get_template_directory_uri() );

/* ═══════════════════════════════════════════
   Базовая настройка темы
═══════════════════════════════════════════ */
function tactic_setup() {
	load_theme_textdomain( 'tactic', TACTIC_DIR . '/languages' );

	add_theme_support( 'title-tag' );
	add_theme_support( 'post-thumbnails' );
	add_theme_support( 'html5', [
		'search-form', 'comment-form', 'comment-list',
		'gallery', 'caption', 'script', 'style',
	] );
	add_theme_support( 'custom-logo', [
		'height'      => 60,
		'width'       => 200,
		'flex-height' => true,
		'flex-width'  => true,
	] );
	add_theme_support( 'align-wide' );
	add_theme_support( 'responsive-embeds' );

	// Размеры изображений
	add_image_size( 'tactic-hero',        1920, 900,  true );
	add_image_size( 'tactic-news-thumb',  800,  500,  true );
	add_image_size( 'tactic-news-list',   400,  250,  true );
	add_image_size( 'tactic-testimonial', 480,  360,  true );
	add_image_size( 'tactic-landscape',   1920, 650,  true );

	// Меню навигации
	register_nav_menus( [
		'primary' => __( '主导航 / Primary Navigation', 'tactic' ),
		'footer'  => __( '页脚导航 / Footer Navigation',  'tactic' ),
	] );
}
add_action( 'after_setup_theme', 'tactic_setup' );

/* ═══════════════════════════════════════════
   Подключение стилей и скриптов
═══════════════════════════════════════════ */
function tactic_enqueue() {
	$main_css_path = TACTIC_DIR . '/assets/css/main.css';
	$css_modules_glob = TACTIC_DIR . '/assets/css/modules/*.css';
	$main_js_path  = TACTIC_DIR . '/assets/js/main.js';
	$css_version_seed = file_exists( $main_css_path ) ? (int) filemtime( $main_css_path ) : 0;
	$css_module_files = glob( $css_modules_glob ) ?: [];
	foreach ( $css_module_files as $css_module_file ) {
		$css_module_mtime = file_exists( $css_module_file ) ? (int) filemtime( $css_module_file ) : 0;
		if ( $css_module_mtime > $css_version_seed ) {
			$css_version_seed = $css_module_mtime;
		}
	}
	$main_css_ver  = $css_version_seed > 0 ? (string) $css_version_seed : TACTIC_VERSION;
	$main_js_ver   = file_exists( $main_js_path ) ? (string) filemtime( $main_js_path ) : TACTIC_VERSION;

	wp_enqueue_style(
		'tactic-main',
		TACTIC_URI . '/assets/css/main.css',
		[],
		$main_css_ver
	);

	wp_enqueue_script(
		'tactic-main',
		TACTIC_URI . '/assets/js/main.js',
		[],
		$main_js_ver,
		true
	);

	wp_localize_script( 'tactic-main', 'tacticData', [
		'ajaxUrl' => admin_url( 'admin-ajax.php' ),
		'nonce'   => wp_create_nonce( 'tactic_nonce' ),
		'lang'    => tactic_current_lang(),
		'contactMessages' => [
			'success'         => tactic_s( 'contact_success' ),
			'error'           => tactic_s( 'contact_error' ),
			'requiredName'    => tactic_s( 'contact_required_name' ),
			'requiredCompany' => tactic_s( 'contact_required_company' ),
			'requiredMobile'  => tactic_s( 'contact_required_mobile' ),
			'requiredEmail'   => tactic_s( 'contact_required_email' ),
			'requiredPhone'   => tactic_s( 'contact_required_phone' ),
			'requiredAddress' => tactic_s( 'contact_required_address' ),
			'requiredGender'  => tactic_s( 'contact_required_gender' ),
			'requiredMessage' => tactic_s( 'contact_required_message' ),
			'requiredTerms'   => tactic_s( 'contact_required_terms' ),
			'invalidEmail'    => tactic_s( 'contact_invalid_email' ),
			'invalidPhone'    => tactic_s( 'contact_invalid_phone' ),
		],
	] );
}
add_action( 'wp_enqueue_scripts', 'tactic_enqueue' );

/* ═══════════════════════════════════════════
   Виджет-области
═══════════════════════════════════════════ */
function tactic_widgets_init() {
	register_sidebar( [
		'name'          => __( '博客侧边栏 / Blog Sidebar', 'tactic' ),
		'id'            => 'sidebar-blog',
		'before_widget' => '<div id="%1$s" class="widget %2$s">',
		'after_widget'  => '</div>',
		'before_title'  => '<h3 class="widget__title">',
		'after_title'   => '</h3>',
	] );
}
add_action( 'widgets_init', 'tactic_widgets_init' );

/* ═══════════════════════════════════════════
   Вспомогательные функции
═══════════════════════════════════════════ */

/**
 * Возвращает строку на текущем языке (inline-вариант).
 * Использование: tactic_t('中文', 'English')
 */
function tactic_t( string $zh, string $en ): string {
	return tactic_current_lang() === 'en' ? $en : $zh;
}

/**
 * Возвращает строку по ключу из strings.php с поддержкой
 * переопределений через WP Admin → TACTIC → Переводы.
 * Использование: tactic_s('hero_title')
 */
function tactic_s( string $key ): string {
	static $strings = null;

	// Сброс статического кэша (вызывается после сохранения в админке)
	if ( $key === '__reset__' ) {
		$strings = null;
		return '';
	}

	if ( $strings === null ) {
		$defaults  = require TACTIC_DIR . '/inc/strings.php';
		$overrides = (array) get_option( 'tactic_translations', [] );
		$strings   = array_replace_recursive( $defaults, $overrides );
	}

	$lang = tactic_current_lang();
	return $strings[ $key ][ $lang ] ?? $strings[ $key ]['en'] ?? $key;
}

/**
 * Текущий язык интерфейса: 'zh' или 'en'
 */
function tactic_current_lang(): string {
	// Приоритет: GET-параметр → cookie → по умолчанию ZH
	$allowed = [ 'zh', 'en' ];

	if ( isset( $_GET['lang'] ) && in_array( $_GET['lang'], $allowed, true ) ) {
		$lang = sanitize_key( $_GET['lang'] );
		setcookie( 'tactic_lang', $lang, time() + 30 * DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
		return $lang;
	}

	if ( isset( $_COOKIE['tactic_lang'] ) && in_array( $_COOKIE['tactic_lang'], $allowed, true ) ) {
		return sanitize_key( $_COOKIE['tactic_lang'] );
	}

	return 'zh';
}

/**
 * Язык для новостей (zh/en) на основе текущего языка интерфейса.
 */
function tactic_news_lang(): string {
	$lang = tactic_current_lang();
	return in_array( $lang, [ 'zh', 'en' ], true ) ? $lang : 'zh';
}

/**
 * Meta query для выборки новостей по текущему языку.
 *
 * @return array<int, array<string, string>>
 */
function tactic_news_lang_meta_query(): array {
	return [
		[
			'key'     => '_tactic_news_lang',
			'value'   => tactic_news_lang(),
			'compare' => '=',
		],
	];
}

/**
 * Добавляет фильтр языка к произвольным WP_Query/get_posts аргументам новостей.
 */
function tactic_apply_news_lang_to_query_args( array $args ): array {
	$meta_query = $args['meta_query'] ?? [];
	if ( ! is_array( $meta_query ) ) {
		$meta_query = [];
	}

	$meta_query[] = [
		'key'     => '_tactic_news_lang',
		'value'   => tactic_news_lang(),
		'compare' => '=',
	];

	if ( count( $meta_query ) > 1 && ! isset( $meta_query['relation'] ) ) {
		$meta_query['relation'] = 'AND';
	}

	$args['meta_query'] = $meta_query;

	return $args;
}

/**
 * Язык для карточек продукции (zh/en) на основе текущего языка интерфейса.
 */
function tactic_product_lang(): string {
	$lang = tactic_current_lang();
	return in_array( $lang, [ 'zh', 'en' ], true ) ? $lang : 'en';
}

/**
 * Meta query для выборки карточек продукции по текущему языку.
 *
 * @return array<int, array<string, string>>
 */
function tactic_product_lang_meta_query(): array {
	return [
		[
			'key'     => '_tactic_product_lang',
			'value'   => tactic_product_lang(),
			'compare' => '=',
		],
	];
}

/**
 * Добавляет фильтр языка к произвольным WP_Query/get_posts аргументам продукции.
 */
function tactic_apply_product_lang_to_query_args( array $args ): array {
	$meta_query = $args['meta_query'] ?? [];
	if ( ! is_array( $meta_query ) ) {
		$meta_query = [];
	}

	$meta_query[] = [
		'key'     => '_tactic_product_lang',
		'value'   => tactic_product_lang(),
		'compare' => '=',
	];

	if ( count( $meta_query ) > 1 && ! isset( $meta_query['relation'] ) ) {
		$meta_query['relation'] = 'AND';
	}

	$args['meta_query'] = $meta_query;

	return $args;
}

/**
 * Определяем язык новости по заголовку/контенту для миграции старых записей.
 */
function tactic_detect_news_lang( WP_Post $post ): string {
	$haystack = (string) $post->post_title . ' ' . (string) $post->post_content . ' ' . (string) $post->post_excerpt;
	return preg_match( '/[\x{3400}-\x{9FFF}]/u', $haystack ) ? 'zh' : 'en';
}

/**
 * One-time: проставляем язык для старых новостей без метки языка.
 */
function tactic_maybe_migrate_news_lang_meta(): void {
	$version = '2026-04-26-news-lang-v1';
	if ( (string) get_option( 'tactic_news_lang_migration', '' ) === $version ) {
		return;
	}

	$ids = get_posts( [
		'post_type'      => 'tactic_news',
		'post_status'    => [ 'publish', 'draft', 'pending', 'future', 'private' ],
		'posts_per_page' => -1,
		'fields'         => 'ids',
		'no_found_rows'  => true,
	] );

	foreach ( $ids as $news_id ) {
		if ( get_post_meta( (int) $news_id, '_tactic_news_lang', true ) ) {
			continue;
		}

		$post = get_post( (int) $news_id );
		if ( ! $post instanceof WP_Post ) {
			continue;
		}

		update_post_meta( (int) $news_id, '_tactic_news_lang', tactic_detect_news_lang( $post ) );
	}

	update_option( 'tactic_news_lang_migration', $version );
}
add_action( 'init', 'tactic_maybe_migrate_news_lang_meta', 40 );

/**
 * One-time: проставляем EN всем существующим карточкам продукции.
 */
function tactic_maybe_migrate_product_lang_meta(): void {
	$version = '2026-04-30-product-lang-v1';
	if ( (string) get_option( 'tactic_product_lang_migration', '' ) === $version ) {
		return;
	}

	$ids = get_posts( [
		'post_type'      => 'tactic_product',
		'post_status'    => [ 'publish', 'draft', 'pending', 'future', 'private' ],
		'posts_per_page' => -1,
		'fields'         => 'ids',
		'no_found_rows'  => true,
	] );

	foreach ( $ids as $product_id ) {
		update_post_meta( (int) $product_id, '_tactic_product_lang', 'en' );
	}

	update_option( 'tactic_product_lang_migration', $version );
}
add_action( 'init', 'tactic_maybe_migrate_product_lang_meta', 41 );

/**
 * Фильтрация основного архива /news по языку.
 */
function tactic_filter_news_archive_by_lang( WP_Query $query ): void {
	if ( is_admin() || ! $query->is_main_query() ) {
		return;
	}

	if ( ! $query->is_post_type_archive( 'tactic_news' ) ) {
		return;
	}

	// Реальная пагинация архива: не более 7 новостей на страницу.
	$query->set( 'posts_per_page', 7 );
	$query->set( 'meta_query', tactic_news_lang_meta_query() );
}
add_action( 'pre_get_posts', 'tactic_filter_news_archive_by_lang' );

/**
 * Переключатель языков ZH / EN
 */
function tactic_language_switcher(): void {
	$current = tactic_current_lang();
	$other   = ( $current === 'zh' ) ? 'en' : 'zh';
	$label   = ( $other === 'en' ) ? 'EN' : '中文';
	$url     = esc_url( add_query_arg( 'lang', $other ) );

	echo '<ul class="lang-switcher" role="list">';
	echo '<li><a href="' . $url . '" hreflang="' . esc_attr( $other ) . '">' . esc_html( $label ) . '</a></li>';
	echo '</ul>';
}

/**
 * Динамический перевод пунктов меню
 */
add_filter( 'nav_menu_item_title', function( string $title, WP_Post $item ): string {
	$map = [
		home_url( '/about/' )   => tactic_s( 'nav_about' ),
		home_url( '/news/' )    => tactic_s( 'nav_blog' ),
		home_url( '/awards/' )  => tactic_s( 'nav_awards' ),
		home_url( '/contact/' ) => tactic_s( 'nav_contact' ),
	];
	return $map[ trailingslashit( $item->url ) ] ?? $title;
}, 10, 2 );

/**
 * SVG-иконки для футера
 */
function tactic_icon_instagram(): string {
	return '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"/></svg>';
}

function tactic_icon_wechat(): string {
	return '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.5 4C5.36 4 2 6.91 2 10.5c0 2.02 1.06 3.82 2.72 5.02L4 18l2.5-1.25A8.2 8.2 0 0 0 9.5 17c.17 0 .34 0 .5-.01A5.44 5.44 0 0 1 9.5 15c0-3.04 2.87-5.5 6.5-5.5.17 0 .34 0 .5.01C15.77 6.64 12.94 4 9.5 4zm-2 4.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM16 11c-3.04 0-5.5 2.02-5.5 4.5S12.96 20 16 20c.69 0 1.35-.1 1.96-.29L20 21l-.62-2.08A4.26 4.26 0 0 0 21.5 15.5C21.5 13.02 19.04 11 16 11zm-1.5 3a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5zm3 0a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5z"/></svg>';
}

function tactic_icon_linkedin(): string {
	return '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6zM2 9h4v12H2z"/><circle cx="4" cy="4" r="2"/></svg>';
}

/* ═══════════════════════════════════════════
   Длина анонса
═══════════════════════════════════════════ */
function tactic_excerpt_length(): int {
	return 20;
}
add_filter( 'excerpt_length', 'tactic_excerpt_length' );

function tactic_excerpt_more(): string {
	return '…';
}
add_filter( 'excerpt_more', 'tactic_excerpt_more' );

/**
 * Проверка, можно ли показывать запись в карточках блога.
 * Скрываем записи, где миниатюра совпадает с логотипом сайта.
 */
function tactic_is_valid_blog_card_post( int $post_id ): bool {
	if ( ! has_post_thumbnail( $post_id ) ) {
		return true;
	}

	$thumb_id = (int) get_post_thumbnail_id( $post_id );
	$logo_id  = (int) get_theme_mod( 'custom_logo' );

	if ( $logo_id > 0 && $thumb_id === $logo_id ) {
		return false;
	}

	return true;
}

/* ═══════════════════════════════════════════
   Body class — язык
═══════════════════════════════════════════ */
function tactic_body_class_lang( array $classes ): array {
	$classes[] = 'lang-' . tactic_current_lang();
	return $classes;
}
add_filter( 'body_class', 'tactic_body_class_lang' );

/* ═══════════════════════════════════════════
   Admin: медиа-загрузчик для метабоксов
═══════════════════════════════════════════ */
function tactic_admin_scripts(): void {
	wp_enqueue_media();
	wp_enqueue_script(
		'tactic-admin',
		TACTIC_URI . '/assets/js/admin.js',
		[ 'jquery' ],
		TACTIC_VERSION,
		true
	);
}
add_action( 'admin_enqueue_scripts', 'tactic_admin_scripts' );

/* ═══════════════════════════════════════════
   Обработчик формы «Контакты»
═══════════════════════════════════════════ */
function tactic_handle_contact_form(): void {
	// Проверка nonce
	if ( ! isset( $_POST['contact_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['contact_nonce'] ), 'tactic_contact_form' ) ) {
		wp_send_json_error( [ 'message' => tactic_s( 'contact_security_error' ) ], 403 );
	}

	$name     = sanitize_text_field( wp_unslash( $_POST['cf_name']     ?? '' ) );
	$company  = sanitize_text_field( wp_unslash( $_POST['cf_company']  ?? '' ) );
	$country  = sanitize_text_field( wp_unslash( $_POST['cf_country']  ?? '' ) );
	$mobile   = sanitize_text_field( wp_unslash( $_POST['cf_mobile']   ?? '' ) );
	$email    = sanitize_email(      wp_unslash( $_POST['cf_email']    ?? '' ) );
	$phone    = sanitize_text_field( wp_unslash( $_POST['cf_phone']    ?? '' ) );
	$address  = sanitize_text_field( wp_unslash( $_POST['cf_address']  ?? '' ) );
	$gender   = sanitize_text_field( wp_unslash( $_POST['cf_gender']   ?? '' ) );
	$industry = sanitize_text_field( wp_unslash( $_POST['cf_industry'] ?? '' ) );
	$message  = sanitize_textarea_field( wp_unslash( $_POST['cf_message'] ?? '' ) );
	$terms    = isset( $_POST['cf_terms'] ) ? '1' : '0';

	$errors = [];

	if ( ! $name ) {
		$errors['cf_name'] = tactic_s( 'contact_required_name' );
	}

	if ( ! $company ) {
		$errors['cf_company'] = tactic_s( 'contact_required_company' );
	}

	if ( ! $mobile ) {
		$errors['cf_mobile'] = tactic_s( 'contact_required_mobile' );
	}

	if ( ! $email ) {
		$errors['cf_email'] = tactic_s( 'contact_required_email' );
	} elseif ( ! is_email( $email ) ) {
		$errors['cf_email'] = tactic_s( 'contact_invalid_email' );
	}

	if ( ! $phone ) {
		$errors['cf_phone'] = tactic_s( 'contact_required_phone' );
	} elseif ( ! preg_match( '/^[+]?[\d\s\-().]{7,25}$/', $phone ) ) {
		$errors['cf_phone'] = tactic_s( 'contact_invalid_phone' );
	}

	if ( ! $address ) {
		$errors['cf_address'] = tactic_s( 'contact_required_address' );
	}

	if ( ! $gender ) {
		$errors['cf_gender'] = tactic_s( 'contact_required_gender' );
	}

	if ( ! $message ) {
		$errors['cf_message'] = tactic_s( 'contact_required_message' );
	}

	if ( $terms !== '1' ) {
		$errors['cf_terms'] = tactic_s( 'contact_required_terms' );
	}

	if ( ! empty( $errors ) ) {
		wp_send_json_error(
			[
				'message' => reset( $errors ),
				'errors'  => $errors,
			],
			422
		);
	}

	$admin_email = get_option( 'admin_email' );
	$to          = $admin_email;
	$subject     = sprintf( '[TACTIC Contact] %s — %s', $name, $company ?: $country );
	$body        = implode( "\n", array_filter( [
		"Name: $name",
		$company  ? "Company: $company"   : '',
		$country  ? "Country: $country"   : '',
		$gender   ? "Gender: $gender"     : '',
		"Email: $email",
		$mobile   ? "Mobile: $mobile"      : '',
		$phone    ? "Phone: $phone"        : '',
		$address  ? "Address: $address"    : '',
		$industry ? "Industry: $industry"  : '',
		"Terms accepted: " . ( $terms === '1' ? 'yes' : 'no' ),
		"",
		"Message:",
		$message,
	] ) );
	$headers = [
		'Content-Type: text/plain; charset=UTF-8',
		"Reply-To: $name <$email>",
	];

	// Сохраняем сообщение в базу данных (CPT tactic_message)
	$post_id = wp_insert_post( [
		'post_type'   => 'tactic_message',
		'post_status' => 'publish',
		'post_title'  => $name,
		'post_author' => 1,
	] );

	if ( $post_id && ! is_wp_error( $post_id ) ) {
		update_post_meta( $post_id, '_msg_email',    $email );
		update_post_meta( $post_id, '_msg_company',  $company );
		update_post_meta( $post_id, '_msg_country',  $country );
		update_post_meta( $post_id, '_msg_mobile',   $mobile );
		update_post_meta( $post_id, '_msg_phone',    $phone );
		update_post_meta( $post_id, '_msg_address',  $address );
		update_post_meta( $post_id, '_msg_gender',   $gender );
		update_post_meta( $post_id, '_msg_industry', $industry );
		update_post_meta( $post_id, '_msg_terms',    $terms );
		update_post_meta( $post_id, '_msg_text',     $message );
		update_post_meta( $post_id, '_msg_read',     '0' );
	}
	// Отправляем письмо на admin email (дополнительно)
	wp_mail( $to, $subject, $body, $headers );

	wp_send_json_success( [ 'message' => 'ok' ] );
}
add_action( 'wp_ajax_tactic_contact',        'tactic_handle_contact_form' );
add_action( 'wp_ajax_nopriv_tactic_contact', 'tactic_handle_contact_form' );

/* ═══════════════════════════════════════════
   Дополнительные модули
═══════════════════════════════════════════ */
require_once TACTIC_DIR . '/inc/custom-post-types.php';
require_once TACTIC_DIR . '/inc/meta-boxes.php';
require_once TACTIC_DIR . '/inc/customizer.php';
require_once TACTIC_DIR . '/inc/admin-translations.php';
require_once TACTIC_DIR . '/inc/admin-contact-settings.php';