* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE * * Resolves placeholders like [HOST], [DATE], [PROFILE_NAME] in backup * directory paths and archive filename formats. */ namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory; class PlaceholderResolver { /** * Supported placeholders and their descriptions (for documentation). */ public const PLACEHOLDERS = [ '[HOST]' => 'Server hostname', '[DATE]' => 'Date as Ymd (e.g. 20260604)', '[TIME]' => 'Time as His (e.g. 143025)', '[DATETIME]' => 'Date and time as Ymd_His', '[YEAR]' => 'Four-digit year', '[MONTH]' => 'Two-digit month', '[DAY]' => 'Two-digit day', '[HOUR]' => 'Two-digit hour (24h)', '[MINUTE]' => 'Two-digit minute', '[SECOND]' => 'Two-digit second', '[PROFILE_ID]' => 'Backup profile ID', '[PROFILE_NAME]' => 'Profile title (sanitized)', '[SITE_NAME]' => 'Joomla site name (sanitized)', '[TYPE]' => 'Backup type (full, database, files, differential)', '[RANDOM]' => 'Random 6-character hex string', '[DEFAULT_DIR]' => 'Default backup directory', '[HOME]' => 'Home directory of the PHP process owner', ]; private array $replacements; /** * @param object $profile The backup profile object */ public function __construct(object $profile) { $now = new \DateTimeImmutable('now'); /* Resolve hostname: prefer HTTP_HOST (web), then try Joomla config (CLI), then system hostname */ $rawHost = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? ''; if (empty($rawHost) || $rawHost === 'localhost') { try { $app = Factory::getApplication(); $liveSite = $app->get('live_site', ''); if (!empty($liveSite)) { $parsed = parse_url($liveSite, PHP_URL_HOST); if (!empty($parsed)) { $rawHost = $parsed; } } } catch (\Throwable $e) { /* fallback */ } } if (empty($rawHost)) { $rawHost = php_uname('n'); } $hostname = preg_replace('/[^a-zA-Z0-9._-]/', '', $rawHost); $siteName = ''; try { $siteName = Factory::getApplication()->get('sitename', ''); } catch (\Throwable $e) { // Fallback: not critical } $this->replacements = [ '[HOST]' => $hostname, '[DATE]' => $now->format('Ymd'), '[TIME]' => $now->format('His'), '[DATETIME]' => $now->format('Ymd_His'), '[YEAR]' => $now->format('Y'), '[MONTH]' => $now->format('m'), '[DAY]' => $now->format('d'), '[HOUR]' => $now->format('H'), '[MINUTE]' => $now->format('i'), '[SECOND]' => $now->format('s'), '[PROFILE_ID]' => (string) ($profile->id ?? '0'), '[PROFILE_NAME]' => $this->sanitize($profile->title ?? 'default'), '[SITE_NAME]' => $this->sanitize($siteName ?: 'joomla'), '[TYPE]' => $profile->backup_type ?? 'full', '[RANDOM]' => bin2hex(random_bytes(3)), '[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(), '[HOME]' => BackupDirectory::getHomeDirectory(), ]; } /** * Replace all placeholders in a string. * * @param string $template String containing [placeholder] tokens * * @return string Resolved string */ public function resolve(string $template): string { return str_replace( array_keys($this->replacements), array_values($this->replacements), $template ); } /** * Get the raw hostname value (for backward compatibility). */ public function getHostname(): string { return $this->replacements['[HOST]']; } /** * Get the datetime tag value (for backward compatibility). */ public function getTag(): string { return $this->replacements['[DATETIME]']; } /** * Sanitize a string for use in filenames/paths. * Keeps alphanumerics, dots, hyphens, underscores. Replaces spaces with hyphens. */ private function sanitize(string $value): string { $value = str_replace(' ', '-', trim($value)); return preg_replace('/[^a-zA-Z0-9._-]/', '', $value); } }