403Webshell
Server IP : 208.122.213.31  /  Your IP : 216.73.216.185
Web Server : Apache
System : Linux msd6191.mjhst.com 4.18.0-553.137.1.el8_10.x86_64 #1 SMP Wed Jun 24 11:40:24 UTC 2026 x86_64
User : WHMCS_MIA_382 ( 1001)
PHP Version : 7.4.33
Disable Function : NONE
MySQL : ON  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /home/httpd/html/hotbabes4k.com/public_html/EmblemAVS/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/httpd/html/hotbabes4k.com/public_html/EmblemAVS/EmblemAVS.Class.php
<?php
/**
 * EmblemAVS Class
 *
 * @package EmblemAVS
 * @version 1.0.0
 * @link https://ghost.army/
 *
 **/

class EmblemAVS {
	public $config;
	public $debugMode;
	private $siteId;
	private $subsiteId;

	/**
	 * Resolve site/subsite IDs (args > globals > flags), load config, then init session and handle requests.
	 * Defaults: SITE_ID → 0, SUBSITE_ID → null. Empty-string subsite is treated as null.
	 * @param int|null $siteId     Optional override for site id.
	 * @param int|null $subsiteId  Optional override for sub-site id.
	 */
	public function __construct(?int $siteId = null, ?int $subsiteId = null)
	{
		// Avoid notices; treat missing flags as []
		$flags = $GLOBALS['loadvars']['pini']['flags'] ?? [];

		// siteId: args > globals > flags > 0
		$resolvedSiteId = $siteId ?? ($GLOBALS['SITE_ID'] ?? ($flags['SITE_ID'] ?? 0));
		$this->siteId   = max(0, (int)$resolvedSiteId);

		// subsiteId: args > globals > flags > null ('' => null)
		$resolvedSubsiteId = $subsiteId ?? ($GLOBALS['SUBSITE_ID'] ?? ($flags['SUBSITE_ID'] ?? null));
		$this->subsiteId   = (isset($resolvedSubsiteId) && trim((string)$resolvedSubsiteId) !== '')
			? (int)$resolvedSubsiteId
			: null;

		// Load configuration for this site
		$this->config = $this->readConfig($this->siteId, $this->subsiteId);

		// Initialize session and handle special request actions (e.g., flush)
		$this->initializeSession();
		$this->handleRequests();
	}

	/**
	 * Load config and compose final settings for a site/subsite.
	 * Merge order (last wins): global default < site default < subsite override.
	 * Throws when the file/shape is invalid or the merge yields an empty result.
	 *
	 * @param int      $siteId
	 * @param int|null $subsiteId
	 * @return array
	 * @throws \RuntimeException
	 */
	private function readConfig(int $siteId, ?int $subsiteId): array
	{
		static $rawConfig = null;           // cache parsed file for this process
		$configFile = __DIR__ . '/EmblemAVS.Config.php';

		if (!is_file($configFile) || !is_readable($configFile)) {
			throw new \RuntimeException("Configuration file {$configFile} not found or unreadable.");
		}

		// Support either: (a) file returns array, or (b) defines $emblem_config array.
		if ($rawConfig === null) {
			$emblem_config = null;
			$loaded = require $configFile;
			if (is_array($loaded)) {
				$rawConfig = $loaded;
			} elseif (is_array($emblem_config)) {
				$rawConfig = $emblem_config;
			} else {
				throw new \RuntimeException("Config must return an array or define \$emblem_config array in {$configFile}.");
			}
		}

		if (!is_array($rawConfig)) {
			throw new \RuntimeException("Configuration is not an array after loading {$configFile}.");
		}

		// 1) Global default
		$globalDefault = [];
		if (array_key_exists('default', $rawConfig)) {
			if (!is_array($rawConfig['default'])) {
				throw new \RuntimeException("'default' in {$configFile} must be an array.");
			}
			$globalDefault = $rawConfig['default'];
		}

		// 2) Site block (flat site default OR ['default'=>[], 'subsites'=>[]])
		$siteBlock = $rawConfig[$siteId] ?? [];
		if (!is_array($siteBlock)) {
			$siteBlock = [];
		}

		// Validate 'subsites' shape if present
		if (isset($siteBlock['subsites']) && !is_array($siteBlock['subsites'])) {
			throw new \RuntimeException("Site {$siteId} 'subsites' must be an array in {$configFile}.");
		}

		// Site default: prefer nested 'default', else flat block (when no 'subsites')
		if (isset($siteBlock['default'])) {
			if (!is_array($siteBlock['default'])) {
				throw new \RuntimeException("Site {$siteId} 'default' must be an array in {$configFile}.");
			}
			$siteDefault = $siteBlock['default'];
		} else {
			$siteDefault = isset($siteBlock['subsites']) ? [] : $siteBlock;
		}

		// 3) Subsite override
		$subOverride = [];
		if ($subsiteId !== null && isset($siteBlock['subsites'])) {
			if (array_key_exists($subsiteId, $siteBlock['subsites'])) {
				if (!is_array($siteBlock['subsites'][$subsiteId])) {
					throw new \RuntimeException("Site {$siteId} subsite {$subsiteId} must be an array in {$configFile}.");
				}
				$subOverride = $siteBlock['subsites'][$subsiteId];
			}
		}

		$final = array_replace_recursive($globalDefault, $siteDefault, $subOverride);

		if (empty($final)) {
			$available = implode(', ', array_map(
				static fn($k) => is_int($k) ? (string)$k : "'{$k}'",
				array_keys($rawConfig)
			));
			throw new \RuntimeException(
				"No effective configuration for siteId {$siteId}" .
				($subsiteId === null ? '' : " / subsite {$subsiteId}") .
				" from {$configFile}. Available top-level keys: {$available}."
			);
		}

		return $final;
	}

	/**
	 * Start a session if none is active.
	 * Skips CLI; avoids starting after headers are sent; hardens cookie flags (HttpOnly, Secure, SameSite=Lax).
	 * @return void
	 */
	private function initializeSession(): void
	{
		// Don’t start sessions for CLI scripts
		if (PHP_SAPI === 'cli') {
			return;
		}

		// Already active? done.
		if (session_status() === PHP_SESSION_ACTIVE) {
			return;
		}

		// If headers already went out, don’t attempt to start (prevents warnings)
		if (headers_sent()) {
			return;
		}

		// Secure cookie params (respect HTTPS if present)
		$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443);

		session_set_cookie_params([
			'lifetime' => 0,
			'path'     => ini_get('session.cookie_path')   ?: '/',
			'domain'   => ini_get('session.cookie_domain') ?: '',
			'secure'   => $secure,
			'httponly' => true,
			'samesite' => 'Lax',
		]);

		session_start();
	}

	/**
	 * Handle special query actions.
	 * Supports:
	 *  - emblem=flush        → clear session/cookies
	 *  - emblem_uuid=<uuid>  → set Emblem session (only if UUID is valid)
	 *  - emblemDebug=1|true  → enable debug mode
	 * @return void
	 */
	private function handleRequests(): void
	{
		// 1) Flush (consider requiring POST or a nonce in production)
		$action = filter_input(INPUT_GET, 'emblem', FILTER_UNSAFE_RAW);
		if ($action === 'flush') {
			$this->clearSessionAndCookie();
		}

		// 2) Adopt Emblem UUID (strict v4 format)
		$uuid = filter_input(INPUT_GET, 'emblem_uuid', FILTER_UNSAFE_RAW);
		if ($uuid !== null) {
			if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $uuid)) {
				$this->setEmblemSession($uuid);
			} else {
				// Optional: log invalid UUID attempt (don’t echo)
				// $this->logDebug('Invalid emblem_uuid provided');
			}
		}

		// 3) Debug mode flag
		$dbg = filter_input(INPUT_GET, 'emblemDebug', FILTER_UNSAFE_RAW);
		if ($dbg !== null) {
			$v = strtolower(trim((string)$dbg));
			if ($v === '1' || $v === 'true' || $v === 'yes' || $v === 'on') {
				$this->debugMode = true;
			}
		}
	}

	/**
	 * Build the AVS call-to-action for a user.
	 * Normalizes input (ID, username/email/IP), calls GhostArmy API, and returns the proper button/alert HTML.
	 * Returns '' when required info is missing or the API yields no actionable response.
	 *
	 * @param array $userInfo {user_id, username, user_ip_address?, user_email?, property_uuid?, property_group_id?, success_redirect_url?, enforce_geo?}
	 * @return string
	 */
	public function displayAVSButton(array $userInfo = []): string
	{
		// --- Requireds ----------------------------------------------------------
		$userId   = (int)($userInfo['user_id'] ?? 0);
		$username = trim((string)($userInfo['username'] ?? ''));
		if ($userId <= 0 || $username === '') {
			return '';
		}
		$username = function_exists('mb_strtolower') ? mb_strtolower($username, 'UTF-8') : strtolower($username);

		// --- Optionals / defaults ----------------------------------------------
		$email = isset($userInfo['user_email']) && filter_var($userInfo['user_email'], FILTER_VALIDATE_EMAIL) ? $userInfo['user_email'] : null;

		// If caller didn’t pass IP, fall back to REMOTE_ADDR (or your getClientIP() if you have it)
		$ip = $userInfo['user_ip_address'] ?? ($_SERVER['REMOTE_ADDR'] ?? null);

		$redirectUrl = (string)($userInfo['success_redirect_url'] ?? ($this->config['success_redirect_url'] ?? ''));

		$postData = [
			'whitelabel_uuid'     => $userInfo['property_uuid']     ?? ($this->config['property_uuid']     ?? null),
			'whitelabel_group_id' => $userInfo['property_group_id'] ?? ($this->config['property_group_id'] ?? null),
			'username'            => $username,
			'user_email'          => $email,
			'user_id'             => $userId,
			'user_ip_address'     => $ip,
			'success_redirect_url'=> $redirectUrl,
			'enforce_geo'         => (bool)($userInfo['enforce_geo'] ?? ($this->config['enforce_geo'] ?? false)),
		];

		// --- API call -----------------------------------------------------------
		$apiResponse = $this->ghostArmyAPI($postData, 'post', 'emblem_avs_auth');
		if (empty($apiResponse) || !is_array($apiResponse)) {
			return '';
		}

		// --- HTML output --------------------------------------------------------
		$html = '';

		// Case 1: we got a redirect URL to start verification
		if (!empty($apiResponse['redirectUrl'])) {
			$html .= '<div class="alert alert-warning text-center">'
				.  htmlspecialchars((string)($this->config['avs_message'] ?? 'Verification required'), ENT_QUOTES, 'UTF-8')
				.  '</div>';

			$html .= '<a href="' . htmlspecialchars((string)$apiResponse['redirectUrl'], ENT_QUOTES, 'UTF-8')
				.  '" target="_blank" rel="noopener noreferrer" id="emblem-avs-button" class="'
				.  htmlspecialchars((string)($this->config['avs_button_class'] ?? 'btn btn-primary'), ENT_QUOTES, 'UTF-8')
				.  '">'
				.  htmlspecialchars((string)($this->config['avs_button'] ?? 'Verify Age'), ENT_QUOTES, 'UTF-8')
				.  '</a>';

			return $html;
		}

		// Case 2: already verified (auth_success === 1)
		$auth = $apiResponse['auth'] ?? null;
		if (is_array($auth) && !empty($auth['auth_success']) && (int)$auth['auth_success'] === 1 && !empty($auth['uuid'])) {
			// Append emblem_uuid safely
			$successUrl = $redirectUrl ?: '';
			if ($successUrl !== '') {
				$sep        = (strpos($successUrl, '?') !== false) ? '&' : '?';
				$successUrl = $successUrl . $sep . 'emblem_uuid=' . rawurlencode((string)$auth['uuid']);
			}

			$html .= '<div class="alert alert-success text-center">'
				.  htmlspecialchars((string)($this->config['avs_complete'] ?? 'Verification complete'), ENT_QUOTES, 'UTF-8')
				.  '</div>';

			$html .= '<a href="' . htmlspecialchars($successUrl, ENT_QUOTES, 'UTF-8')
				.  '" id="emblem-avs-button" class="'
				.  htmlspecialchars((string)($this->config['avs_button_class_success'] ?? 'btn btn-success'), ENT_QUOTES, 'UTF-8')
				.  '">'
				.  htmlspecialchars((string)($this->config['avs_button_success'] ?? 'Continue'), ENT_QUOTES, 'UTF-8')
				.  '</a>';

			return $html;
		}

		// No actionable content
		return '';
	}

	/**
	 * Render the AVS template with provided $userInfo.
	 * Validates template path, isolates scope, and returns or echoes the output.
	 *
	 * @param array       $userInfo Data exposed to the template as $EmblemClientAVS_userInfo.
	 * @param string|null $template Optional template filename (overrides config).
	 * @param bool        $echo     Echo output (true) or return as string (false).
	 * @return string|null          Rendered HTML when $echo=false; otherwise null.
	 * @throws \RuntimeException    When the template is missing or outside the base path.
	 */
	public function includeAVSTemplate(array $userInfo = [], ?string $template = null, bool $echo = true)
	{
		$base = rtrim((string)($this->config['emblem_absolute_path'] ?? ''), '/');
		$file = (string)($template ?? ($this->config['emblem_template'] ?? ''));

		if ($base === '' || $file === '') {
			throw new \RuntimeException('Template base path or filename not configured.');
		}

		// Build and validate full path (prevents traversal)
		$full = $base . '/' . ltrim($file, '/');
		$baseReal = realpath($base);
		$fullReal = $full && is_file($full) ? realpath($full) : false;

		if ($baseReal === false || $fullReal === false || strpos($fullReal, $baseReal . DIRECTORY_SEPARATOR) !== 0) {
			throw new \RuntimeException("Template not found or invalid path: {$full}");
		}
		if (!is_readable($fullReal)) {
			throw new \RuntimeException("Template not readable: {$full}");
		}

		// Scoped render (no globals). Expose $EmblemClientAVS_userInfo inside the template.
		$render = static function (string $templateFile, array $data): string {
			$EmblemClientAVS_userInfo = $data; // variable available to the template
			ob_start();
			include $templateFile;
			return (string)ob_get_clean();
		};

		$html = $render($fullReal, $userInfo);

		if ($echo) {
			echo $html;
			return null;
		}
		return $html;
	}

	/**
	 * Resolve and return the absolute path to the AVS template.
	 * No side effects (does not set globals). Validates that the file exists, is readable,
	 * and resides under the configured base path.
	 *
	 * @param array $userInfo Ignored; kept for BC (template rendering should receive data at include time).
	 * @return string Absolute, validated template path.
	 * @throws \RuntimeException When misconfigured or file is missing/unreadable/outside base.
	 */
	public function getAVSTemplate(array $userInfo = []): string
	{
		$base = rtrim((string)($this->config['emblem_absolute_path'] ?? ''), '/');
		$file = (string)($this->config['emblem_template'] ?? '');

		if ($base === '' || $file === '') {
			throw new \RuntimeException('Template base path or filename not configured.');
		}

		// Build and validate full path (prevents traversal)
		$full     = $base . '/' . ltrim($file, '/');
		$baseReal = realpath($base);
		$fullReal = is_file($full) ? realpath($full) : false;

		if ($baseReal === false || $fullReal === false || strpos($fullReal, $baseReal . DIRECTORY_SEPARATOR) !== 0) {
			throw new \RuntimeException("Template not found or invalid path: {$full}");
		}
		if (!is_readable($fullReal)) {
			throw new \RuntimeException("Template not readable: {$full}");
		}

		return $fullReal;
	}

	/**
	 * Store the user's Emblem UUID in the session.
	 * Validates RFC4122 UUID (v1–v5). Optionally mirrors into $_SERVER if configured.
	 *
	 * @param string|null $uuid
	 * @return void
	 */
	private function setEmblemSession(?string $uuid = null): void
	{
		// Nothing to do
		$uuid = trim((string)$uuid);
		if ($uuid === '') {
			return;
		}

		// Strict UUID (v1–v5) validation
		if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $uuid)) {
			// Optional: $this->logDebug('Invalid UUID supplied to setEmblemSession');
			return;
		}

		// Ensure session is available (your initializeSession() already skips CLI and checks headers)
		$this->initializeSession();

		// Session key: fallback to a sensible default if not configured
		$name = (string)($this->config['session_name'] ?? 'emblem_uuid');

		// Write only if changed
		if (!isset($_SESSION[$name]) || $_SESSION[$name] !== $uuid) {
			$_SESSION[$name] = $uuid;
		}

		// Optional mirroring to $_SERVER for downstream consumers (opt-in)
		if (!empty($this->config['mirror_uuid_to_server'])) {
			$_SERVER[strtoupper(preg_replace('/[^a-z0-9_]/i', '_', $name))] = $uuid;
		}
	}

	/**
	 * Get the URL the user should follow for AVS.
	 * Calls GhostArmy (`emblem_avs_auth`) and returns either the verification start URL
	 * or, if already verified, the success URL with `emblem_uuid`. Returns null if inputs
	 * are insufficient or the API has no actionable response.
	 *
	 * @param array $userInfo {user_id, username, user_ip_address?, user_email?, property_uuid?, property_group_id?, success_redirect_url?, enforce_geo?}
	 * @return string|null
	 */
	public function getAVSRedirect(array $userInfo = []): ?string
	{
		// --- Requireds ----------------------------------------------------------
		$userId   = (int)($userInfo['user_id'] ?? 0);
		$username = trim((string)($userInfo['username'] ?? ''));
		if ($userId <= 0 || $username === '') {
			return null;
		}
		$username = function_exists('mb_strtolower') ? mb_strtolower($username, 'UTF-8') : strtolower($username);

		// --- Optionals / defaults ----------------------------------------------
		$email = isset($userInfo['user_email']) && filter_var($userInfo['user_email'], FILTER_VALIDATE_EMAIL)
			? $userInfo['user_email']
			: null;

		$ip = $userInfo['user_ip_address']
			?? ($_SERVER['REMOTE_ADDR'] ?? null); // or swap in $this->getClientIP() if you have it

		$redirectUrl = (string)($userInfo['success_redirect_url'] ?? ($this->config['success_redirect_url'] ?? ''));

		$postData = [
			'whitelabel_uuid'      => $userInfo['property_uuid']     ?? ($this->config['property_uuid']     ?? null),
			'whitelabel_group_id'  => $userInfo['property_group_id'] ?? ($this->config['property_group_id'] ?? null),
			'username'             => $username,
			'user_email'           => $email,
			'user_id'              => $userId,
			'user_ip_address'      => $ip,
			'success_redirect_url' => $redirectUrl,
			'enforce_geo'          => (bool)($userInfo['enforce_geo'] ?? ($this->config['enforce_geo'] ?? false)),
		];

		// --- API call -----------------------------------------------------------
		$apiResponse = $this->ghostArmyAPI($postData, 'post', 'emblem_avs_auth');

		if ($this->debugMode ?? false) {
			// Don’t echo from libraries; log instead
			error_log('[EmblemAVS] getAVSRedirect response: ' . json_encode($apiResponse));
		}

		if (empty($apiResponse) || !is_array($apiResponse)) {
			return null;
		}

		// Case 1: verification needs to start
		if (!empty($apiResponse['redirectUrl'])) {
			return (string)$apiResponse['redirectUrl'];
		}

		// Case 2: already verified -> send to success URL with emblem_uuid
		$auth = $apiResponse['auth'] ?? null;
		if (is_array($auth) && !empty($auth['auth_success']) && (int)$auth['auth_success'] === 1 && !empty($auth['uuid'])) {
			if ($redirectUrl === '') {
				return null; // nowhere meaningful to send them
			}
			$sep = (strpos($redirectUrl, '?') !== false) ? '&' : '?';
			return $redirectUrl . $sep . 'emblem_uuid=' . rawurlencode((string)$auth['uuid']);
		}

		return null;
	}

	/**
	 * Determine if the current user is AVS-verified.
	 * Uses session short-circuit; otherwise calls GhostArmy `emblem_avs_auth/verify`.
	 * May set a session UUID (API UUID or generated) when not blocked. Returns true when usable.
	 *
	 * @param array $userInfo {user_id, username, user_ip_address?, user_email?, property_uuid?, property_group_id?, success_redirect_url?, enforce_geo?}
	 * @return bool
	 */
	public function isUserAVS(array $userInfo = []): bool
	{
		$this->initializeSession();

		$sessionKey = (string)($this->config['session_name'] ?? 'emblem_uuid');

		// If we already have a UUID in session, consider user verified.
		if (!empty($_SESSION[$sessionKey])) {
			return true;
		}

		// --- Requireds to verify ------------------------------------------------
		$userId   = (int)($userInfo['user_id'] ?? 0);
		$username = trim((string)($userInfo['username'] ?? ''));
		if ($userId <= 0 || $username === '') {
			return false;
		}
		$username = function_exists('mb_strtolower') ? mb_strtolower($username, 'UTF-8') : strtolower($username);

		// --- Optionals / defaults ----------------------------------------------
		$email = isset($userInfo['user_email']) && filter_var($userInfo['user_email'], FILTER_VALIDATE_EMAIL)
			? $userInfo['user_email']
			: null;

		$ip = $userInfo['user_ip_address']
			?? ($_SERVER['REMOTE_ADDR'] ?? null); // or swap with your getClientIP()

		$redirectUrl = (string)($userInfo['success_redirect_url'] ?? ($this->config['success_redirect_url'] ?? ''));

		$wlUuid   = $userInfo['property_uuid']     ?? ($this->config['property_uuid']     ?? null);
		$wlGroup  = $userInfo['property_group_id'] ?? ($this->config['property_group_id'] ?? null);
		$enfGeo   = (bool)($userInfo['enforce_geo'] ?? ($this->config['enforce_geo'] ?? false));

		$postData = [
			'whitelabel_uuid'      => $wlUuid,
			'whitelabel_group_id'  => $wlGroup,
			'username'             => $username,
			'user_email'           => $email,
			'user_id'              => $userId,
			'user_ip_address'      => $ip,
			'success_redirect_url' => $redirectUrl,
			'enforce_geo'          => $enfGeo,
		];

		if ($this->debugMode ?? false) {
			error_log('[EmblemAVS] isUserAVS postData: ' . json_encode($postData));
		}

		// --- API: verify --------------------------------------------------------
		$apiResponse = $this->ghostArmyAPI($postData, 'post', 'emblem_avs_auth/verify');

		if ($this->debugMode ?? false) {
			error_log('[EmblemAVS] isUserAVS response: ' . json_encode($apiResponse));
		}

		if (empty($apiResponse) || !is_array($apiResponse)) {
			return false;
		}

		// Blocked? (explicit trigger)
		if (!empty($apiResponse['trigger_block'])) {
			return false;
		}

		// Already verified (auth_success === 1): store API UUID and succeed.
		if (!empty($apiResponse['auth'])
			&& is_array($apiResponse['auth'])
			&& !empty($apiResponse['auth']['uuid'])
			&& (int)($apiResponse['auth']['auth_success'] ?? 0) === 1
		) {
			$this->setEmblemSession((string)$apiResponse['auth']['uuid']);
			return true;
		}

		// Not blocked and no redirect/uuid/auth: treat as allowed; set a deterministic UUID.
		if (empty($apiResponse['uuid']) && empty($apiResponse['auth']) && empty($apiResponse['redirectUrl'])) {
			if (method_exists($this, 'generateUserUUID')) {
				$this->setEmblemSession($this->generateUserUUID($userId, $username, (string)$wlUuid));
			}
			return true;
		}

		// Any other case (e.g., needs redirect) is not "verified" yet.
		return false;
	}

	/**
	 * Append a JSON log line to logs/emblemAVS_YYYYMM.log.
	 * Ensures the logs directory exists; writes atomically; enriches entry with ISO8601 UTC timestamp and level.
	 *
	 * @param array  $logData Arbitrary structured data to log.
	 * @param string $level   Log level hint (e.g., info, warn, error). Default: 'info'.
	 * @return void
	 * @throws \RuntimeException When misconfigured or write fails.
	 */
	public function logIt(array $logData, string $level = 'info'): void
	{
		$base = rtrim((string)($this->config['emblem_absolute_path'] ?? ''), DIRECTORY_SEPARATOR);
		if ($base === '') {
			throw new \RuntimeException('emblem_absolute_path not configured.');
		}

		$logDir = $base . DIRECTORY_SEPARATOR . 'logs';
		if (!is_dir($logDir)) {
			// Create recursively; respect process umask
			if (!@mkdir($logDir, 0775, true) && !is_dir($logDir)) {
				throw new \RuntimeException("Failed to create log directory: {$logDir}");
			}
		}
		if (!is_writable($logDir)) {
			throw new \RuntimeException("Log directory is not writable: {$logDir}");
		}

		$logFile = $logDir . DIRECTORY_SEPARATOR . 'emblemAVS_' . date('Ym') . '.log';

		// Compose entry (ISO8601 UTC timestamp + level + payload)
		$entry = [
			'ts'    => gmdate('c'),
			'level' => $level,
		] + $logData;

		$json = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
		if ($json === false) {
			// Fallback: stringify on encoding failure
			$json = '{"ts":"' . gmdate('c') . '","level":"' . $level . '","_raw":' . json_encode((string)print_r($logData, true)) . '}';
		}
		$json .= PHP_EOL;

		// Atomic append with lock
		$ok = @file_put_contents($logFile, $json, FILE_APPEND | LOCK_EX);
		if ($ok === false) {
			throw new \RuntimeException("Failed writing to log file: {$logFile}");
		}
	}

	/**
	 * Call GhostArmy API and return decoded JSON (plus http_code/headers).
	 * Supports GET/POST (JSON body for POST). Adds UA/Auth/Origin headers, verifies SSL,
	 * enforces timeouts, and logs failures. Returns [] on failure or non-JSON responses.
	 *
	 * @param array       $post_data Payload or query params.
	 * @param string      $method    'post'|'get' (case-insensitive). Default: 'post'.
	 * @param string      $endpoint  Path relative to config api_url (no leading slash needed).
	 * @return array                 Decoded JSON with 'http_code' and 'headers' when available; [] on failure.
	 */
	private function ghostArmyAPI(array $post_data = [], string $method = 'post', string $endpoint = 'emblem_avs_auth'): array
	{
		$response_data = [];

		$base = rtrim((string)($this->config['api_url'] ?? ''), '/');
		$key  = (string)($this->config['api_key'] ?? '');
		$orig = (string)($this->config['origin_domain'] ?? '');
		$ep   = ltrim((string)$endpoint, '/');

		if ($base === '' || $key === '') {
			$this->logIt([
				'label'  => 'api_misconfig',
				'status' => 'error',
				'api_url'=> $base,
				'has_key'=> $key !== '',
			], 'error');
			return [];
		}

		// Build URL
		$api_url = $base . '/' . $ep;
		$method  = strtolower($method) === 'get' ? 'get' : 'post';
		if ($method === 'get' && !empty($post_data)) {
			$qs = http_build_query($post_data, '', '&', PHP_QUERY_RFC3986);
			$api_url .= (str_contains($api_url, '?') ? '&' : '?') . $qs;
		}

		// Init cURL
		$ch = curl_init($api_url);
		if ($ch === false) {
			$this->logIt(['label' => 'curl_init_failed', 'status' => 'error', 'url' => $api_url], 'error');
			return [];
		}

		// Headers
		$headers = [
			'Content-Type: application/json',
			'Accept: application/json',
			'Origin: ' . $orig,
			'Authorization: Bearer ' . $key,
		];

		// POST payload
		if ($method === 'post') {
			curl_setopt($ch, CURLOPT_POST, 1);
			// Safer JSON flags; handle potential encoding errors
			$json = json_encode($post_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
			if ($json === false) {
				$json = '{}';
			}
			curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
		}

		// Core options
		curl_setopt_array($ch, [
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_FOLLOWLOCATION => false,  // avoid auth/header leaks on redirects
			CURLOPT_USERAGENT      => 'EmblemClientAVS/1.0',
			CURLOPT_HTTPHEADER     => $headers,
			CURLOPT_HEADER         => true,   // capture headers + body
			CURLOPT_SSL_VERIFYPEER => true,
			CURLOPT_SSL_VERIFYHOST => 2,
			CURLOPT_CONNECTTIMEOUT => 5,
			CURLOPT_TIMEOUT        => 10,
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
		]);

		$response = curl_exec($ch);
		$errno    = curl_errno($ch);
		$err      = $errno ? curl_error($ch) : null;
		$http     = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
		$hdrSize  = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE);

		// Split headers/body
		$rawHeaders = $hdrSize > 0 ? substr((string)$response, 0, $hdrSize) : '';
		$body       = $hdrSize > 0 ? substr((string)$response, $hdrSize) : (string)$response;

		curl_close($ch);

		// Parse headers into assoc (last value wins)
		$headerLines = preg_split("/\r\n|\n|\r/", (string)$rawHeaders, -1, PREG_SPLIT_NO_EMPTY) ?: [];
		$parsedHeaders = [];
		foreach ($headerLines as $line) {
			if (stripos($line, 'HTTP/') === 0) {
				$parsedHeaders[':status-line'] = $line;
				continue;
			}
			$pos = strpos($line, ':');
			if ($pos !== false) {
				$name  = trim(substr($line, 0, $pos));
				$value = trim(substr($line, $pos + 1));
				$parsedHeaders[$name] = $value;
			}
		}

		if ($this->debugMode ?? false) {
			// Redact auth
			$dbgHeaders = $parsedHeaders;
			if (isset($dbgHeaders['Authorization'])) {
				$dbgHeaders['Authorization'] = 'Bearer ****';
			}
			$logData = [
				'label'   => 'api_debug',
				'origin'				=> $orig,
				'property_uuid'			=> $this->config['property_uuid'] ?? null,
				'property_group_id'		=> $this->config['property_group_id'] ?? null,
				'status'  => $errno ? 'error' : 'ok',
				'method'  => strtoupper($method),
				'url'     => $api_url,
				'http'    => $http,
				'errno'   => $errno,
				'headers' => $dbgHeaders,
				'body_len'=> strlen($body),
			];

			$this->logIt($logData, $errno ? 'warn' : 'info');

			// notify Ghost Army of the failure
			$this->notifyGhostArmy($logData);
		}

		// Handle transport errors or non-2xx
		if ($errno !== 0 || $http < 200 || $http >= 300) {
			$logData = [
				'label'					=> 'api_failed',
				'origin'				=> $orig,
				'property_uuid'			=> $this->config['property_uuid'] ?? null,
				'property_group_id'		=> $this->config['property_group_id'] ?? null,
				'status'				=> 'error',
				'method'				=> strtoupper($method),
				'url'					=> $api_url,
				'http'					=> $http,
				'errno'					=> $errno,
				'error'					=> $err,
				'resp_snip'				=> substr($body, 0, 500),
			];

			$this->logIt($logData, 'error');

			// notify Ghost Army of the failure
			$this->notifyGhostArmy($logData);

			return [];
		}

		// Decode JSON body
		$decoded = json_decode($body, true);
		if (!is_array($decoded)) {
			// Not JSON; log and return []
			$logData = [
				'label'    => 'api_non_json',
				'origin'				=> $orig,
				'property_uuid'			=> $this->config['property_uuid'] ?? null,
				'property_group_id'		=> $this->config['property_group_id'] ?? null,
				'status'   => 'warn',
				'method'   => strtoupper($method),
				'url'      => $api_url,
				'http'     => $http,
				'resp_snip'=> substr($body, 0, 500),
			];

			$this->logIt($logData, 'warn');

			// notify Ghost Army of the failure
			$this->notifyGhostArmy($logData);

			return [];
		}

		// Attach meta
		$decoded['http_code'] = $http;
		$decoded['headers']   = $parsedHeaders;

		return $decoded;
	}

	/**
	 * Notify Ghost Army of API issues via a dedicated endpoint.
	 *
	 * @param array $logData
	 * @return void
	 */
	private function notifyGhostArmy(array $logData): void
	{
		$encodedData = base64_encode(json_encode($logData));
		$bugsnagUrl = 'https://support.ghost.army/EmblemBugsnag/?data=' . rawurlencode($encodedData);
		$chBugsnag = curl_init($bugsnagUrl);
		if ($chBugsnag !== false) {
			curl_setopt_array($chBugsnag, [
				CURLOPT_RETURNTRANSFER => true,
				CURLOPT_FOLLOWLOCATION => false,
				CURLOPT_USERAGENT      => 'EmblemClientAVS/1.0',
				CURLOPT_TIMEOUT        => 5,
			]);
			curl_exec($chBugsnag);
			curl_close($chBugsnag);
		}
	}

	/**
	 * Clear session and related cookies.
	 * Empties $_SESSION, expires the PHP session cookie (respecting SameSite/flags),
	 * destroys the session, and removes the app cookie (config session_name) on host
	 * and dot-domain variants. No output; safe when headers already sent (best-effort).
	 *
	 * @return void
	 */
	private function clearSessionAndCookie(): void
	{
		// Ensure session exists unless headers already sent (can’t set cookies then)
		if (!headers_sent()) {
			$this->initializeSession();
		}

		// --- Clear session data -------------------------------------------------
		if (session_status() === PHP_SESSION_ACTIVE) {
			$_SESSION = [];
			// Expire the session cookie using current params
			$params = session_get_cookie_params();
			$cookieName = session_name();

			// Build deletion options (PHP 7.3+ array form keeps SameSite/flags)
			$opts = [
				'expires'  => time() - 3600,
				'path'     => $params['path']   ?: '/',
				'domain'   => $params['domain'] ?? '',
				'secure'   => (bool)($params['secure'] ?? false),
				'httponly' => true,
			];
			if (isset($params['samesite'])) {
				$opts['samesite'] = $params['samesite']; // usually 'Lax' or 'Strict'
			}

			if (!headers_sent()) {
				@setcookie($cookieName, '', $opts);
			}

			// Regenerate then destroy to prevent fixation
			@session_regenerate_id(true);
			@session_destroy();
			@session_write_close();
		}

		// --- App-specific cookie(s) --------------------------------------------
		$appCookie = (string)($this->config['session_name'] ?? 'emblem_uuid');
		if ($appCookie !== '') {
			// Remove from superglobal regardless
			unset($_COOKIE[$appCookie]);

			if (!headers_sent()) {
				// Try expiring on both host and dot-domain (covers subdomains)
				$host = $_SERVER['HTTP_HOST'] ?? ($_SERVER['SERVER_NAME'] ?? '');
				$host = preg_replace('~:\d+$~', '', (string)$host); // strip port if present
				$dotDomain = $host !== '' && strpos($host, '.') !== false ? '.' . ltrim($host, '.') : '';

				$cookieOpts = [
					'expires'  => time() - 3600,
					'path'     => '/',
					'secure'   => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
								|| ((int)($_SERVER['SERVER_PORT'] ?? 0) === 443),
					'httponly' => true,
					'samesite' => 'Lax',
				];

				// Current host
				@setcookie($appCookie, '', $cookieOpts + ['domain' => '']);
				if ($host !== '') {
					@setcookie($appCookie, '', $cookieOpts + ['domain' => $host]);
				}
				// Dot-domain (parent scope)
				if ($dotDomain !== '') {
					@setcookie($appCookie, '', $cookieOpts + ['domain' => $dotDomain]);
				}
			}
		}

		// Optional: clear any server mirror
		if (!empty($this->config['mirror_uuid_to_server'])) {
			$k = strtoupper(preg_replace('/[^a-z0-9_]/i', '_', (string)($this->config['session_name'] ?? 'emblem_uuid')));
			unset($_SERVER[$k]);
		}
	}

	/**
	 * Resolve the client's public IP.
	 * Prefers trusted proxy headers (CF/XFF/True-Client-IP) when enabled; else uses REMOTE_ADDR.
	 * Localhost (::1/127.0.0.1) → "127.0.0.1". Returns '' if no public IP found.
	 *
	 * Config (optional):
	 *   - trust_proxy_headers (bool)              default: false
	 *   - trusted_proxy_cidrs (string[] CIDRs)    e.g., Cloudflare ranges
	 *
	 * @return string
	 */
	public function getUserIP(): string
	{
		// Helper: CIDR match
		$ipInCidrs = static function (string $ip, array $cidrs): bool {
			if ($ip === '' || empty($cidrs)) return false;
			$ipBin = @inet_pton($ip);
			if ($ipBin === false) return false;
			foreach ($cidrs as $cidr) {
				if (!preg_match('~^(.+?)/(\\d{1,3})$~', $cidr, $m)) continue;
				[$full, $net, $bits] = $m;
				$netBin = @inet_pton($net);
				if ($netBin === false || strlen($netBin) !== strlen($ipBin)) continue;
				$bytes = (int) floor($bits / 8);
				$rem   = (int) ($bits % 8);
				if ($bytes && substr($ipBin, 0, $bytes) !== substr($netBin, 0, $bytes)) continue;
				if ($rem) {
					$mask = chr(0xFF << (8 - $rem) & 0xFF);
					if ((ord($ipBin[$bytes]) & ord($mask)) !== (ord($netBin[$bytes]) & ord($mask))) continue;
				}
				return true;
			}
			return false;
		};

		// Helper: validate public IP; normalize localhost
		$normalize = static function (string $ip): string {
			if ($ip === '::1' || $ip === '127.0.0.1') return '127.0.0.1';
			return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) ? $ip : '';
		};

		// Decide if we should trust proxy headers
		$trustProxy = (bool)($this->config['trust_proxy_headers'] ?? false);
		$trustedCidrs = (array)($this->config['trusted_proxy_cidrs'] ?? []); // e.g., Cloudflare IPv4/IPv6 ranges

		$remoteAddr = (string)($_SERVER['REMOTE_ADDR'] ?? '');
		$remoteIsTrusted = $trustProxy && ($remoteAddr !== '') && ($ipInCidrs($remoteAddr, $trustedCidrs) || empty($trustedCidrs));

		// Candidate sources, in order, when proxy is trusted
		if ($remoteIsTrusted) {
			$candidates = [
				(string)($_SERVER['HTTP_CF_CONNECTING_IP'] ?? ''),   // Cloudflare
				(string)($_SERVER['HTTP_TRUE_CLIENT_IP']   ?? ''),   // Some CDNs
				(string)($_SERVER['HTTP_X_FORWARDED_FOR']  ?? ''),   // May be a list
				(string)($_SERVER['HTTP_X_REAL_IP']        ?? ''),   // Nginx/Ingress
			];

			foreach ($candidates as $val) {
				if ($val === '') continue;

				// XFF can be a comma-separated list (left-most is original client)
				foreach (explode(',', $val) as $ip) {
					$ip = trim($ip);
					if ($ip === '') continue;
					$pub = $normalize($ip);
					if ($pub !== '') return $pub;
					// allow localhost normalization for direct CF dev envs
					if ($ip === '::1' || $ip === '127.0.0.1') return '127.0.0.1';
				}
			}
		}

		// Fallback: REMOTE_ADDR (direct client or untrusted proxy)
		if ($remoteAddr !== '') {
			$pub = $normalize($remoteAddr);
			if ($pub !== '') return $pub;
			if ($remoteAddr === '::1' || $remoteAddr === '127.0.0.1') return '127.0.0.1';
		}

		return '';
	}

	/**
	 * Generate a deterministic RFC-4122 UUIDv5 for a user.
	 * Uses property UUID as the namespace and "user_id:username" as the name.
	 * Falls back to DNS namespace if $property_uuid is invalid.
	 *
	 * @param mixed  $user_id
	 * @param string $username
	 * @param string $property_uuid Namespace UUID (e.g., per-property/brand)
	 * @return string UUIDv5 string (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
	 */
	public function generateUserUUID($user_id, string $username, string $property_uuid): string
	{
		// Normalize inputs
		$id  = (string)(int)$user_id;
		$usr = function_exists('mb_strtolower') ? mb_strtolower(trim($username), 'UTF-8') : strtolower(trim($username));

		// Validate namespace UUID (v1–v5)
		$isUuid = static function (string $u): bool {
			return (bool)preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $u);
		};

		// Use provided namespace or fallback to DNS namespace (RFC 4122)
		$ns = $isUuid($property_uuid) ? $property_uuid : '6ba7b810-9dad-11d1-80b4-00c04fd430c8';

		// Convert namespace UUID to 16 raw bytes
		$nsBin = hex2bin(str_replace('-', '', strtolower($ns)));
		if ($nsBin === false || strlen($nsBin) !== 16) {
			// Shouldn't happen, but guard anyway
			$nsBin = hex2bin(str_replace('-', '', '6ba7b810-9dad-11d1-80b4-00c04fd430c8'));
		}

		// Name = "id:username"
		$name = $id . ':' . $usr;

		// SHA-1 over namespace bytes + name (binary output)
		$hash = sha1($nsBin . $name, true); // 20 bytes

		// Set version (5) and variant (RFC 4122)
		$bytes = substr($hash, 0, 16);
		$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x50); // version 5
		$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); // variant 10xxxxxx

		// Format as UUID string
		$hex = bin2hex($bytes);
		return sprintf(
			'%s-%s-%s-%s-%s',
			substr($hex, 0, 8),
			substr($hex, 8, 4),
			substr($hex, 12, 4),
			substr($hex, 16, 4),
			substr($hex, 20, 12)
		);
	}

}

Youez - 2016 - github.com/yon3zu
LinuXploit