PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / trunk
JetBackup – Backup, Restore & Migrate vtrunk
3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / Entities / Util.php
backup / src / JetBackup / Entities Last commit date
.htaccess 1 year ago Util.php 1 day ago index.html 1 year ago web.config 1 year ago
Util.php
403 lines
1 <?php
2
3 namespace JetBackup\Entities;
4
5 use DateTime;
6 use DateTimeZone;
7 use Exception;
8 use JetBackup\Exception\IOException;
9 use JetBackup\Factory;
10 use JetBackup\Filesystem\File;
11 use JetBackup\JetBackup;
12
13 if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');
14
15 class Util {
16
17 public const WEB_CONFIG_FILE = "%sweb.config";
18 const HTACCESS_FILE = "%s.htaccess";
19 const INDEX_HTML_FILE = "%sindex.html";
20
21 private function __construct() {}
22
23 /**
24 * Recursively check if a directory only contains NFS temporary files
25 * @param string $dir
26 * @return bool
27 */
28 private static function isNFSOnlyDirectory(string $dir): bool {
29 if (!is_dir($dir)) return false;
30
31 $handle = @opendir($dir);
32 if (!$handle) return false;
33
34 while (($entry = readdir($handle)) !== false) {
35 if ($entry === '.' || $entry === '..') continue;
36
37 $path = $dir . DIRECTORY_SEPARATOR . $entry;
38
39 if (is_dir($path)) {
40 // Recursively check subdirectories
41 if (!self::isNFSOnlyDirectory($path)) {
42 closedir($handle);
43 return false;
44 }
45 } else {
46 // Check if file is an NFS temp file
47 if (!str_starts_with($entry, '.nfs')) {
48 closedir($handle);
49 return false;
50 }
51 }
52 }
53
54 closedir($handle);
55 return true;
56 }
57
58 /**
59 /**
60 * @param string $directory
61 * @param bool $remove_main
62 * @param bool $verify
63 *
64 * @return void
65 * @throws IOException
66 */
67 public static function rm(string $directory, bool $remove_main=true):void {
68
69 if(!$directory) return;
70
71 $file = new File($directory);
72 if(!$file->exists()) return;
73
74 if($file->isFile() || $file->isLink()) {
75 unlink($directory);
76 return;
77 }
78
79 $main_length = strlen($directory);
80 if (($dir = @dir($directory)) === false) throw new IOException("Cannot delete directory $directory");
81 $queue = [$dir];
82
83 while($queue) {
84 $obj = array_shift($queue);
85
86 while ($obj !== false && ($fileName = $obj->read()) !== false) {
87 if($fileName == '.' || $fileName == '..') continue;
88
89 $file = new File($obj->path . '/' . $fileName);
90
91 if($file->isLink() || !$file->isDir()) {
92 if(!@unlink($file->path())) {
93 // On NFS, files being deleted while still open get renamed to .nfs* (silly rename)
94 // These will be automatically removed when the file handle is closed
95 $filename = basename($file->path());
96 if(str_starts_with($filename, '.nfs')) {
97 // Log warning but don't fail - NFS will clean it up automatically
98 error_log("JetBackup: Skipping NFS temporary file (will be auto-cleaned): {$file->path()}");
99 continue;
100 }
101 throw new IOException("cannot remove file \"{$file->path()}\"");
102 }
103 continue;
104 }
105 array_unshift($queue, $obj);
106 $obj = dir($file->path());
107 }
108
109 if($obj !== false && ($remove_main || strlen($obj->path) != $main_length) && !@rmdir($obj->path)) {
110 // On NFS, if directory contains only .nfs* files (recursively), rmdir will fail
111 // Check if directory only contains NFS temp files (including subdirectories)
112 if (self::isNFSOnlyDirectory($obj->path)) {
113 // Directory only contains NFS temp files - log and continue
114 error_log("JetBackup: Skipping directory with only NFS temp files (will be auto-cleaned): {$obj->path}");
115 } else {
116 throw new IOException("cannot remove \"$obj->path\": Directory not empty");
117 }
118 }
119
120 }
121 }
122
123 public static function cp(string $source, string $destination, int $permissions=0777, array $excludes=[]):void {
124 if(!file_exists($source)) throw new IOException("Source dir does not exist");
125 if(is_file($source)) {
126 foreach($excludes as $exclude) if(preg_match("#$exclude#", $source)) return;
127 if(!copy($source, $destination))
128 throw new IOException("Failed copping file \"$source\"");
129 return;
130 }
131
132 if(!file_exists($destination) || !is_dir($destination)) throw new IOException("Destination dir does not exist");
133
134 $queue = [$source];
135
136 while($queue) {
137 $dirName = array_shift($queue);
138 $dirObj = dir($dirName);
139
140 while (($fileName = $dirObj->read()) !== false) {
141 if($fileName == '.' || $fileName == '..') continue;
142
143 $filePath = $dirObj->path . '/' . $fileName;
144 $destFile = trim(preg_replace('/^' . preg_quote($source, '/') . '/', '', $dirObj->path), '/');
145
146 @mkdir($destination . '/' . $destFile, $permissions);
147
148 if(is_dir($filePath)) {
149 $queue[] = $filePath;
150 continue;
151 }
152
153 foreach($excludes as $exclude) if(preg_match("#$exclude#", $filePath)) continue 2;
154 if(!copy($filePath, $destination . '/' . $destFile . '/' . $fileName))
155 throw new IOException("Failed copping file \"$filePath\"");
156 }
157 }
158 }
159
160 public static function generateRandomString(int $length = 12): string {
161 try {
162 return substr(bin2hex(random_bytes(ceil($length / 2))), 0, $length);
163 } catch (Exception $e) {
164 // Fallback in case /dev/urandom is not readable
165 return substr(base64_encode(uniqid(mt_rand(), true)), 0, $length);
166 }
167 }
168
169
170 /**
171 * @param string|int $time
172 *
173 * @return DateTime
174 * @throws Exception
175 */
176 public static function getDateTime($time='now'): DateTime {
177 if(is_int($time)) $time = '@' . $time;
178 return new DateTime($time, new DateTimeZone(Factory::getSettingsGeneral()->getTimeZone()));
179 }
180
181 /**
182 * @throws Exception
183 */
184 public static function date(string $format, $timestamp=0): string {
185 if (!$timestamp || $timestamp == 0) $timestamp = time();
186 return self::getDateTime($timestamp)->format($format);
187 }
188
189 public static function generateUniqueId(): string {
190 static $inc;
191 if(!$inc) $inc = 0;
192 return sprintf("%08x%08x%08x", time(), floatval(microtime())*1000000, $inc++);
193 }
194
195 public static function generatePassword(int $length = 20 ): string {
196 $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+';
197 $charLength = strlen($chars);
198 $password = '';
199
200 while (strlen($password) < $length) {
201 $rand = random_int(0, PHP_INT_MAX);
202 $password .= $chars[$rand % $charLength];
203 }
204
205 return $password;
206 }
207
208
209 public static function humanReadableToBytes($sSize) {
210 if (is_numeric($sSize)) return $sSize;
211
212 $iValue = intval($sSize);
213 $sSuffix = strtoupper(substr($sSize, strlen($iValue)));
214
215 switch ($sSuffix) {
216 case 'PB': case 'P': $iValue *= 1125899906842624; break;
217 case 'TB': case 'T': $iValue *= 1099511627776; break;
218 case 'GB': case 'G': $iValue *= 1073741824; break;
219 case 'MB': case 'M': $iValue *= 1048576; break;
220 case 'KB': case 'K': $iValue *= 1024; break;
221 }
222 return $iValue;
223 }
224
225 public static function bytesToHumanReadable($bytes, $precision = 2): string {
226 if (!is_numeric($bytes)) {
227 // Handle the error, return an error message, or throw an exception
228 return 'Invalid numeric value';
229 }
230
231 $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
232
233 $bytes = max($bytes, 0);
234 $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
235 $pow = min($pow, count($units) - 1);
236 $bytes /= (1 << (10 * $pow));
237
238 return round($bytes, $precision) . ' ' . $units[$pow];
239 }
240
241 /**
242 * @param string $path
243 *
244 * @return string
245 */
246 public static function mb_basename(string $path):string {
247 if(!$path) return '';
248 $path = preg_replace("#/+#", "/", $path);
249 if($path == '/') return '';
250 $path = preg_replace("#/$#", "", $path);
251 $pos = mb_strrpos(mb_convert_encoding($path, 'UTF-8'), "/");
252 return $pos !== false ? mb_substr($path, $pos+1) : $path;
253 }
254
255 /**
256 * @param string $path
257 *
258 * @return string
259 */
260 public static function mb_dirname(string $path):string {
261 if(!$path) return '.';
262 $path = preg_replace("#/+#", "/", $path);
263 if($path == '/') return '/';
264 $path = preg_replace("#/$#", "", $path);
265 $pos = mb_strrpos(mb_convert_encoding($path, 'UTF-8'), "/");
266 return $pos !== false ? mb_substr($path, 0, $pos) : $path;
267 }
268
269 public static function generateTimeZoneList(): array {
270 $timezones = DateTimeZone::listAbbreviations();
271 $timezone_readable = [];
272 foreach ($timezones as $timezone_area) {
273 foreach ($timezone_area as $timezone) {
274 // Ensure timezone_id is not null before trimming
275 if (is_null($timezone['timezone_id']) || trim($timezone['timezone_id']) == '') continue;
276 if (trim($timezone['timezone_id']) == 'GB') continue;
277 $hours = ($timezone['offset'] / 3600);
278 $offset = floor($hours); // Round the result to the nearest 0.5
279 $timezone_readable[$timezone['timezone_id']] = $offset;
280 }
281 }
282 ksort($timezone_readable);
283 return $timezone_readable;
284 }
285
286 public static function IISWebConfig(): string {
287
288 return <<<XML
289 <?xml version="1.0" encoding="UTF-8"?>
290 <configuration>
291 <system.webServer>
292 <security>
293 <authorization>
294 <remove users="*" roles="" verbs="" />
295 <add accessType="Deny" users="*" />
296 </authorization>
297 </security>
298 </system.webServer>
299 </configuration>
300 XML;
301
302 }
303
304 public static function has_posix_getpwuid () : bool {return function_exists('posix_getpwuid');}
305 public static function has_posix_getgrgid () : bool {return function_exists('posix_getgrgid');}
306 public static function has_posix_geteuid () : bool {return function_exists('posix_geteuid');}
307
308 /**
309 * @param string $arg The argument to escape
310 * @return string The escaped argument
311 */
312 public static function escapeshellarg(string $arg): string {
313 if (function_exists('escapeshellarg')) return escapeshellarg($arg);
314
315 // Manual fallback
316 $escaped = str_replace("'", "'\\''", $arg);
317 return "'{$escaped}'";
318 }
319
320 /**
321 * Escape only if path contains: spaces, quotes, special shell chars, etc.
322 * @param string $arg The argument to escape
323 * @return string The escaped argument (only if needed)
324 */
325 public static function escapeshellargCron(string $arg): string {
326 if (preg_match('/[\s\'\"\\$`!*?<>|&;(){}]/', $arg)) return self::escapeshellarg($arg);
327 return $arg;
328 }
329
330 public static function getpwuid($uid = null): ?array {
331 // allow UID 0; only null means "not provided"
332 if ($uid === null) return null;
333 if (!self::has_posix_getpwuid()) return null;
334
335 if (!is_int($uid)) {
336 if (!is_numeric($uid)) return null;
337 $uid = (int) $uid;
338 }
339
340 $info = @posix_getpwuid($uid);
341 return is_array($info) ? $info : null;
342 }
343
344
345 public static function getgrgid($gid = null): ?array {
346 if ($gid === null) return null;
347 if (!self::has_posix_getgrgid()) return null;
348
349 if (!is_int($gid)) {
350 if (!is_numeric($gid)) return null;
351 $gid = (int) $gid;
352 }
353
354 $info = @posix_getgrgid($gid);
355 return is_array($info) ? $info : null;
356 }
357
358
359 public static function geteuid(): ?int {
360 if (!self::has_posix_geteuid()) return null;
361 $id = @posix_geteuid();
362 return is_int($id) ? $id : null;
363 }
364
365
366
367
368
369 public static function secureFolder($folder): void {
370
371 $config_file = sprintf(self::WEB_CONFIG_FILE, $folder . JetBackup::SEP);
372 $htaccess_file = sprintf(self::HTACCESS_FILE, $folder . JetBackup::SEP);
373 $html_file = sprintf(self::INDEX_HTML_FILE, $folder . JetBackup::SEP);
374
375 if(!file_exists($folder)) mkdir($folder, 0700, true);
376 if(!file_exists($htaccess_file)) file_put_contents($htaccess_file, "Deny from all");
377 if(!file_exists($html_file)) file_put_contents($html_file, "");
378 if(!file_exists($config_file)) file_put_contents($config_file, self::IISWebConfig());
379
380 }
381
382 /**
383 * Normalizes a path format by replacing double forward slashes (//)
384 *
385 * @param string $path The path to be converted.
386 * @return string The converted Windows-style path.
387 */
388 public static function normalizePath(string $path): string {
389 return str_replace('\\', JetBackup::SEP, $path);
390 }
391
392 public static function resolveRelativePath(string $path): string {
393 $path = self::normalizePath($path);
394 $isAbsolute = strpos($path, JetBackup::SEP) === 0;
395 $resolved = [];
396 foreach (explode(JetBackup::SEP, $path) as $segment) {
397 if ($segment === '' || $segment === '.') continue;
398 if ($segment === '..') { array_pop($resolved); continue; }
399 $resolved[] = $segment;
400 }
401 return ($isAbsolute ? JetBackup::SEP : '') . implode(JetBackup::SEP, $resolved);
402 }
403 }