Каталог, карточка товара, корзина, оформление заказа и блог на WooCommerce. Стили разложены по слоям: токены из Figma, база, компоненты, страницы, брейкпоинты. Цвета, типографика, радиусы и отступы взяты из Variables и текстовых стилей макета, ниже по слоям хардкода нет. Иконки — Phosphor, вставляются инлайном как SVG, символами ничего не рисуется. Стили WooCommerce отключены, вёрстка своя, !important не используется.
187 lines
5.7 KiB
PHP
187 lines
5.7 KiB
PHP
<?php
|
|
/**
|
|
* URL handling.
|
|
*
|
|
* @package VyatkaShop
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Build links against the host the visitor actually asked for.
|
|
*
|
|
* WordPress stores one canonical home URL, so a site reachable under more than
|
|
* one name (a bare host and a proxied domain, say) would otherwise send people
|
|
* back to the stored one mid-session. Only the request host is trusted here,
|
|
* and only when it matches a host WordPress already knows about, so this cannot
|
|
* be driven by a forged Host header.
|
|
*
|
|
* @param string $url Complete URL generated by WordPress.
|
|
* @return string
|
|
*/
|
|
function vyatka_shop_request_host_url( string $url ): string {
|
|
$request_host = vyatka_shop_request_host();
|
|
if ( '' === $request_host || ! vyatka_shop_host_is_known( $request_host ) ) {
|
|
return $url;
|
|
}
|
|
|
|
$parts = wp_parse_url( $url );
|
|
if ( ! is_array( $parts ) ) {
|
|
return $url;
|
|
}
|
|
|
|
$forwarded_proto = isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] )
|
|
? strtolower( trim( explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) )[0] ) )
|
|
: '';
|
|
$scheme = 'https' === $forwarded_proto || is_ssl() ? 'https' : 'http';
|
|
|
|
$rewritten = $scheme . '://' . $request_host;
|
|
$rewritten .= $parts['path'] ?? '';
|
|
$rewritten .= isset( $parts['query'] ) ? '?' . $parts['query'] : '';
|
|
$rewritten .= isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
|
|
return $rewritten;
|
|
}
|
|
add_filter( 'home_url', 'vyatka_shop_request_host_url' );
|
|
add_filter( 'site_url', 'vyatka_shop_request_host_url' );
|
|
|
|
/**
|
|
* The host from the current request, without its port.
|
|
*/
|
|
function vyatka_shop_request_host(): string {
|
|
if ( ! isset( $_SERVER['HTTP_HOST'] ) ) {
|
|
return '';
|
|
}
|
|
|
|
return strtolower( preg_replace( '/:\d+$/', '', sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) );
|
|
}
|
|
|
|
/**
|
|
* Hosts this installation answers to: whatever is stored in the options, plus
|
|
* anything a site owner adds through the filter.
|
|
*
|
|
* @return array<int,string>
|
|
*/
|
|
function vyatka_shop_known_hosts(): array {
|
|
$hosts = array();
|
|
|
|
foreach ( array( 'home', 'siteurl' ) as $option_name ) {
|
|
$host = wp_parse_url( (string) get_option( $option_name ), PHP_URL_HOST );
|
|
if ( is_string( $host ) && '' !== $host ) {
|
|
$hosts[] = strtolower( $host );
|
|
}
|
|
}
|
|
|
|
// Sites behind a proxy answer to a name the stored options do not know.
|
|
// Declare those in wp-config.php:
|
|
// define( 'VYATKA_SHOP_HOSTS', 'shop.example.com, www.example.com' );
|
|
if ( defined( 'VYATKA_SHOP_HOSTS' ) ) {
|
|
foreach ( explode( ',', (string) VYATKA_SHOP_HOSTS ) as $host ) {
|
|
$host = strtolower( trim( $host ) );
|
|
if ( '' !== $host ) {
|
|
$hosts[] = $host;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Additional hosts the site is served under.
|
|
*
|
|
* @param array<int,string> $hosts Known hostnames.
|
|
*/
|
|
return array_values( array_unique( (array) apply_filters( 'vyatka_shop_known_hosts', $hosts ) ) );
|
|
}
|
|
|
|
/**
|
|
* @param string $host Hostname to test.
|
|
*/
|
|
function vyatka_shop_host_is_known( string $host ): bool {
|
|
return in_array( strtolower( $host ), vyatka_shop_known_hosts(), true );
|
|
}
|
|
|
|
/**
|
|
* Strip the origin from internal links in the finished HTML.
|
|
*
|
|
* Rewriting only the rendered document leaves redirects, REST responses and
|
|
* WP-CLI output fully qualified, while the markup a browser receives stays
|
|
* usable under whichever host served it.
|
|
*
|
|
* @param string $html Completed response body.
|
|
* @return string
|
|
*/
|
|
function vyatka_shop_relative_frontend_urls( string $html ): string {
|
|
foreach ( headers_list() as $header ) {
|
|
if ( 0 === stripos( $header, 'Content-Type:' ) && false === stripos( $header, 'text/html' ) ) {
|
|
return $html;
|
|
}
|
|
}
|
|
|
|
$hosts = vyatka_shop_known_hosts();
|
|
$request_host = vyatka_shop_request_host();
|
|
if ( '' !== $request_host ) {
|
|
$hosts[] = $request_host;
|
|
}
|
|
|
|
$absolute_bases = array();
|
|
foreach ( array_unique( $hosts ) as $host ) {
|
|
$absolute_bases[] = 'https://' . $host;
|
|
$absolute_bases[] = 'http://' . $host;
|
|
$absolute_bases[] = '//' . $host;
|
|
}
|
|
// Longest first, so "//host" never truncates "https://host".
|
|
usort( $absolute_bases, static fn( string $left, string $right ): int => strlen( $right ) <=> strlen( $left ) );
|
|
|
|
return str_replace( $absolute_bases, '', $html );
|
|
}
|
|
|
|
/**
|
|
* Buffer ordinary frontend documents so their internal links can be made
|
|
* relative without changing the site's canonical configuration.
|
|
*/
|
|
function vyatka_shop_start_relative_url_buffer(): void {
|
|
if ( is_admin() || wp_doing_ajax() || wp_is_json_request() || is_feed() ) {
|
|
return;
|
|
}
|
|
|
|
ob_start( 'vyatka_shop_relative_frontend_urls' );
|
|
}
|
|
add_action( 'template_redirect', 'vyatka_shop_start_relative_url_buffer', 0 );
|
|
|
|
/**
|
|
* Recover stale filtered category URLs when the selected page no longer exists.
|
|
*
|
|
* Filter forms always target page one, but bookmarked or cached /page/N/ URLs
|
|
* can still reach WordPress directly and become a 404 after the result set is
|
|
* narrowed. Preserve their query string and retry the category from page one.
|
|
*/
|
|
function vyatka_shop_redirect_stale_filtered_category_page(): void {
|
|
if ( ! is_404() || empty( $_GET ) ) {
|
|
return;
|
|
}
|
|
|
|
$filter_keys = array(
|
|
'filter_min_price',
|
|
'filter_max_price',
|
|
'feature',
|
|
'collection',
|
|
'per_page',
|
|
'orderby',
|
|
);
|
|
|
|
if ( ! array_intersect( $filter_keys, array_keys( $_GET ) ) ) {
|
|
return;
|
|
}
|
|
|
|
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
|
$request_path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
|
if ( ! is_string( $request_path ) || ! preg_match( '#^(.*/product-category/[^/]+)/page/[1-9][0-9]*/?$#u', $request_path, $matches ) ) {
|
|
return;
|
|
}
|
|
|
|
$target = add_query_arg( wp_unslash( $_GET ), trailingslashit( $matches[1] ) );
|
|
wp_safe_redirect( $target, 302, 'Vyatka Shop' );
|
|
exit;
|
|
}
|
|
add_action( 'template_redirect', 'vyatka_shop_redirect_stale_filtered_category_page', -10 );
|