2727use phpMyFAQ \Setup ;
2828use phpMyFAQ \System ;
2929use phpMyFAQ \User ;
30+ use Random \RandomException ;
3031use RecursiveDirectoryIterator ;
3132use RecursiveIteratorIterator ;
33+ use SplFileInfo ;
3234use SplFileObject ;
3335use Symfony \Component \HttpFoundation \Request ;
3436use Tivie \HtaccessParser \Exception \SyntaxException ;
@@ -49,6 +51,8 @@ class Update extends Setup
4951 /** @var string[] */
5052 private array $ dryRunQueries = [];
5153
54+ private ?string $ backupFilename = null ;
55+
5256 public function __construct (protected System $ system , private readonly Configuration $ configuration )
5357 {
5458 parent ::__construct ($ this ->system );
@@ -72,6 +76,7 @@ public function isConfigTableNotAvailable(DatabaseDriver $databaseDriver): bool
7276 /**
7377 * Creates a backup of the current config files
7478 * @throws Exception
79+ * @throws RandomException
7580 */
7681 public function createConfigBackup (string $ configDir ): string
7782 {
@@ -84,19 +89,44 @@ public function createConfigBackup(string $configDir): string
8489
8590 $ files = new RecursiveIteratorIterator (
8691 new RecursiveDirectoryIterator ($ configDir ),
87- RecursiveIteratorIterator::SELF_FIRST
92+ RecursiveIteratorIterator::SELF_FIRST ,
8893 );
8994
90- foreach ($ files as $ file ) {
91- $ file = realpath ($ file );
92- if (str_contains ($ file , $ configDir . DIRECTORY_SEPARATOR )) {
93- if (is_dir ($ file )) {
94- $ zipArchive ->addEmptyDir (
95- str_replace ($ configDir . DIRECTORY_SEPARATOR , '' , $ file . DIRECTORY_SEPARATOR )
96- );
97- } elseif (is_file ($ file )) {
98- $ zipArchive ->addFile ($ file , str_replace ($ configDir . DIRECTORY_SEPARATOR , '' , $ file ));
99- }
95+ foreach ($ files as $ fileInfo ) {
96+ if ($ fileInfo instanceof SplFileInfo) {
97+ $ filePath = $ fileInfo ->getRealPath () ?: $ fileInfo ->getPathname ();
98+ $ isDir = $ fileInfo ->isDir ();
99+ $ isFile = $ fileInfo ->isFile ();
100+ } else {
101+ $ filePath = is_string ($ fileInfo ) ? $ fileInfo : (string ) $ fileInfo ;
102+ $ filePath = realpath ($ filePath ) ?: $ filePath ;
103+ $ isDir = is_dir ($ filePath );
104+ $ isFile = is_file ($ filePath );
105+ }
106+
107+ if ($ filePath === false || $ filePath === null || $ filePath === '' ) {
108+ continue ;
109+ }
110+
111+ // Exclude the zip we are currently writing
112+ if ($ filePath === $ outputZipFile ) {
113+ continue ;
114+ }
115+
116+ // Only include entries inside the config directory
117+ if (!str_contains ($ filePath , $ configDir . DIRECTORY_SEPARATOR ) && $ filePath !== $ configDir ) {
118+ continue ;
119+ }
120+
121+ // Compute a relative path inside the archive
122+ $ relativePath = str_replace ($ configDir . DIRECTORY_SEPARATOR , '' , $ filePath );
123+ $ relativePath = ltrim ($ relativePath , DIRECTORY_SEPARATOR );
124+
125+ if ($ isDir ) {
126+ // Ensure directory entries end with a slash
127+ $ zipArchive ->addEmptyDir (rtrim ($ relativePath , DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR );
128+ } elseif ($ isFile ) {
129+ $ zipArchive ->addFile ($ filePath , $ relativePath );
100130 }
101131 }
102132
@@ -1096,8 +1126,15 @@ private function updateVersion(): void
10961126 $ this ->configuration ->update (['main.currentVersion ' => System::getVersion ()]);
10971127 }
10981128
1129+ /**
1130+ * @throws RandomException
1131+ */
10991132 private function getBackupFilename (): string
11001133 {
1101- return sprintf ('phpmyfaq-config-backup.%s.zip ' , date ('Y-m-d ' ));
1134+ if ($ this ->backupFilename === null ) {
1135+ $ randomHash = bin2hex (random_bytes (4 )); // 8-character hex string
1136+ $ this ->backupFilename = sprintf ('phpmyfaq-config-backup.%s.%s.zip ' , date (format: 'Y-m-d ' ), $ randomHash );
1137+ }
1138+ return $ this ->backupFilename ;
11021139 }
11031140}
0 commit comments