| @copyright 2007-2026 (c) AVE.cms | @link https://ave-cms.ru | @version 3.3 */ namespace App\Frontend; defined('BASEPATH') || die('Direct access to this location is not allowed.'); use App\Common\Auth; use App\Common\ResponseSecurityHeaders; use App\Common\Settings; /** Blocks the public runtime during development while preserving privileged preview. */ class SiteAccess { const MODE_PUBLIC = 'public'; const MODE_DEVELOPMENT = 'development'; const PREVIEW_PERMISSION = 'view_development_site'; protected static $preview = false; public static function enabled() { Settings::init(); return (string) Settings::get('site_access_mode', self::MODE_PUBLIC) === self::MODE_DEVELOPMENT; } /** Returns false after emitting the temporary unavailable response. */ public static function enforce() { self::$preview = false; if (!self::enabled()) { return true; } self::disablePublicCache(); header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet', true); header('X-AVE-Site-Access: development', true); if (Auth::systemUserCan(self::PREVIEW_PERMISSION)) { self::$preview = true; return true; } self::respondUnavailable(); return false; } public static function isPreview() { return self::$preview; } /** Adds a small persistent marker without changing the site's document flow. */ public static function injectPreviewNotice($html) { $html = (string) $html; if (!self::$preview || $html === '') { return $html; } $notice = '
Режим разработки: посетителям сайт недоступен
'; if (stripos($html, '') !== false) { return preg_replace('/<\/body>/i', $notice . '', $html, 1); } return $html . $notice; } public static function unavailableDocument($title, $message, $siteName = '') { $title = self::escape($title !== '' ? $title : 'Сайт готовится к запуску'); $message = nl2br(self::escape($message !== '' ? $message : 'Мы завершаем настройку сайта. Пожалуйста, зайдите немного позже.')); $siteName = self::escape($siteName); return '' . '' . $title . '' . '' . '

' . $title . '

' . $message . '

' . ($siteName !== '' ? '' . $siteName . '' : '') . '
'; } protected static function respondUnavailable() { if (ob_get_level() > 0) { ob_clean(); } $title = trim((string) Settings::get('site_development_title', 'Сайт готовится к запуску')); $message = trim((string) Settings::get('site_development_message', 'Мы завершаем настройку сайта. Пожалуйста, зайдите немного позже.')); $siteName = trim((string) Settings::get('site_name', '')); $json = self::wantsJson(); $body = $json ? json_encode(array('success' => false, 'error' => 'site_development', 'message' => $title), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : self::unavailableDocument($title, $message, $siteName); http_response_code(503); ResponseSecurityHeaders::apply(); header('Retry-After: 3600'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0', true); header('Content-Type: ' . ($json ? 'application/json' : 'text/html') . '; charset=UTF-8'); header('Content-Length: ' . strlen($body)); if (!isset($_SERVER['REQUEST_METHOD']) || strtoupper((string) $_SERVER['REQUEST_METHOD']) !== 'HEAD') { echo $body; } } protected static function wantsJson() { $path = parse_url(isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '/', PHP_URL_PATH); $accept = isset($_SERVER['HTTP_ACCEPT']) ? strtolower((string) $_SERVER['HTTP_ACCEPT']) : ''; return strpos('/' . ltrim((string) $path, '/'), '/api/') === 0 || strpos($accept, 'application/json') !== false || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower((string) $_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); } protected static function disablePublicCache() { if (!defined('PUBLIC_RESPONSE_NO_CACHE')) { define('PUBLIC_RESPONSE_NO_CACHE', true); } if (function_exists('apache_setenv')) { @apache_setenv('PUBLIC_NO_CACHE', '1'); } $_SERVER['PUBLIC_NO_CACHE'] = '1'; header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0', true); } protected static function escape($value) { return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } }