WebOrbiton
v1.3.0

AtheraStudio SyncDrop

590 lines · 19.1 KB
  1. <?php
  2. declare(strict_types=1);
  3. ​
  4. defined('SD_BOOT') || exit;
  5. ​
  6. const SD_NAME = 'AtheraStudio SyncDrop';
  7. define('SD_VERSION', (static function (): string {
  8. $version = trim((string)@file_get_contents(dirname(__DIR__) . '/version.txt'));
  9. return preg_match('/^\d{1,4}(\.\d{1,4}){1,3}$/', $version) ? $version : '0.0.0';
  10. })());
  11. const SD_IDLE = 900;
  12. const SD_ABSOLUTE = 28800;
  13. const SD_CHUNK = 1048576;
  14. const SD_MAX_FILE = 268435456;
  15. const SD_MAX_FILES = 20000;
  16. const SD_MAX_BATCH = 1000;
  17. const SD_KEEP_BACKUPS = 10;
  18. const SD_PREFIX = '.syncdrop';
  19. const SD_ID_FILE = '.syncdrop-id';
  20. const SD_WEBROOTS = ['public_html', 'private_html', 'www', 'htdocs', 'httpdocs', 'html', 'web', 'wwwroot', 'public'];
  21. const SD_STRONG_WEBROOTS = ['public_html', 'private_html', 'htdocs', 'httpdocs', 'wwwroot'];
  22. ​
  23. define('SD_ROOT', dirname(__DIR__));
  24. ​
  25. function h(string $value): string
  26. {
  27. return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
  28. }
  29. ​
  30. function sd_config(): array
  31. {
  32. static $config = null;
  33. if ($config !== null) return $config;
  34. $config = [];
  35. $file = SD_ROOT . '/config.php';
  36. if (is_file($file)) {
  37. $loaded = (static fn(string $f): mixed => require $f)($file);
  38. if (is_array($loaded)) $config = $loaded;
  39. }
  40. return $config;
  41. }
  42. ​
  43. function sd_https(): bool
  44. {
  45. $https = strtolower((string)($_SERVER['HTTPS'] ?? ''));
  46. if ($https !== '' && $https !== 'off') return true;
  47. if ((string)($_SERVER['SERVER_PORT'] ?? '') === '443') return true;
  48. if (!sd_via_proxy()) return false;
  49. return strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https';
  50. }
  51. ​
  52. function sd_local(): bool
  53. {
  54. return in_array((string)($_SERVER['REMOTE_ADDR'] ?? ''), ['127.0.0.1', '::1'], true);
  55. }
  56. ​
  57. function sd_via_proxy(): bool
  58. {
  59. static $via = null;
  60. if ($via !== null) return $via;
  61. $remote = (string)($_SERVER['REMOTE_ADDR'] ?? '');
  62. $via = false;
  63. foreach ((array)(sd_config()['proxies'] ?? []) as $cidr) {
  64. if (is_string($cidr) && sd_cidr_contains($cidr, $remote)) return $via = true;
  65. }
  66. return $via;
  67. }
  68. ​
  69. function sd_ip(): string
  70. {
  71. static $ip = null;
  72. if ($ip !== null) return $ip;
  73. $ip = (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
  74. $header = (string)(sd_config()['ip_header'] ?? '');
  75. if ($header !== '' && preg_match('/^HTTP_[A-Z0-9_]{1,60}$/', $header) && sd_via_proxy()) {
  76. $parts = array_map('trim', explode(',', (string)($_SERVER[$header] ?? '')));
  77. $candidate = (string)end($parts);
  78. if (filter_var($candidate, FILTER_VALIDATE_IP)) $ip = $candidate;
  79. }
  80. if (!filter_var($ip, FILTER_VALIDATE_IP)) $ip = '0.0.0.0';
  81. if (str_starts_with(strtolower($ip), '::ffff:') && filter_var(substr($ip, 7), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) $ip = substr($ip, 7);
  82. return $ip;
  83. }
  84. ​
  85. function sd_cidr_parse(string $value): ?array
  86. {
  87. $value = trim($value);
  88. $bits = null;
  89. if (str_contains($value, '/')) {
  90. [$value, $suffix] = explode('/', $value, 2);
  91. if (!preg_match('/^\d{1,3}$/', $suffix)) return null;
  92. $bits = (int)$suffix;
  93. }
  94. if (!filter_var($value, FILTER_VALIDATE_IP)) return null;
  95. $packed = @inet_pton($value);
  96. if ($packed === false) return null;
  97. $max = strlen($packed) * 8;
  98. $bits ??= $max;
  99. if ($bits < 0 || $bits > $max) return null;
  100. $net = '';
  101. for ($i = 0; $i < strlen($packed); $i++) {
  102. $keep = max(0, min(8, $bits - $i * 8));
  103. $net .= chr(ord($packed[$i]) & ((0xFF << (8 - $keep)) & 0xFF));
  104. }
  105. $text = (string)inet_ntop($net);
  106. return ['net' => $net, 'bits' => $bits, 'max' => $max, 'text' => $bits === $max ? $text : $text . '/' . $bits];
  107. }
  108. ​
  109. function sd_cidr_contains(string $cidr, string $ip): bool
  110. {
  111. $range = sd_cidr_parse($cidr);
  112. $addr = sd_cidr_parse($ip);
  113. if ($range === null || $addr === null || $range['max'] !== $addr['max']) return false;
  114. $full = intdiv($range['bits'], 8);
  115. if (strncmp($range['net'], $addr['net'], $full) !== 0) return false;
  116. $rest = $range['bits'] % 8;
  117. if ($rest === 0) return true;
  118. $mask = (0xFF << (8 - $rest)) & 0xFF;
  119. return (ord($range['net'][$full]) & $mask) === (ord($addr['net'][$full]) & $mask);
  120. }
  121. ​
  122. function sd_ua(): string
  123. {
  124. return mb_substr(preg_replace('/[\x00-\x1F\x7F]/u', '', (string)($_SERVER['HTTP_USER_AGENT'] ?? '')) ?? '', 0, 300);
  125. }
  126. ​
  127. function sd_host(): string
  128. {
  129. $host = strtolower((string)($_SERVER['HTTP_HOST'] ?? ''));
  130. return preg_match('/^(\[[0-9a-f:.]{2,45}\]|[a-z0-9.-]{1,253})(:\d{1,5})?$/', $host) ? $host : '';
  131. }
  132. ​
  133. function sd_norm(string $path): string
  134. {
  135. $path = rtrim(str_replace('\\', '/', $path), '/');
  136. return $path === '' ? '/' : $path;
  137. }
  138. ​
  139. function sd_app(): string
  140. {
  141. return realpath(SD_ROOT) ?: SD_ROOT;
  142. }
  143. ​
  144. function sd_docroot(): string
  145. {
  146. static $doc = null;
  147. if ($doc !== null) return $doc;
  148. $raw = trim((string)($_SERVER['DOCUMENT_ROOT'] ?? ''));
  149. $doc = $raw === '' ? false : realpath($raw);
  150. if ($doc === false || $doc === '' || !sd_inside(sd_app(), $doc)) $doc = sd_app();
  151. return $doc;
  152. }
  153. ​
  154. function sd_inside(string $path, string $root): bool
  155. {
  156. $path = rtrim(sd_norm($path), '/') . '/';
  157. $root = rtrim(sd_norm($root), '/') . '/';
  158. if (DIRECTORY_SEPARATOR === '\\') {
  159. $path = strtolower($path);
  160. $root = strtolower($root);
  161. }
  162. return strncmp($path, $root, strlen($root)) === 0;
  163. }
  164. ​
  165. function sd_webroot_named(string $dir, array $names): bool
  166. {
  167. return in_array(strtolower(basename(sd_norm($dir))), $names, true);
  168. }
  169. ​
  170. function sd_under_strong_webroot(string $dir): bool
  171. {
  172. $dir = sd_norm($dir);
  173. while (true) {
  174. if (sd_webroot_named($dir, SD_STRONG_WEBROOTS)) return true;
  175. $parent = sd_norm(dirname($dir));
  176. if ($parent === $dir) return false;
  177. $dir = $parent;
  178. }
  179. }
  180. ​
  181. function sd_base_candidates(): array
  182. {
  183. $doc = sd_docroot();
  184. $list = [];
  185. $dir = $doc;
  186. while (true) {
  187. $parent = dirname($dir);
  188. if ($parent === $dir) break;
  189. if (sd_webroot_named($dir, SD_WEBROOTS)) $list[] = $parent;
  190. $dir = $parent;
  191. }
  192. $list[] = dirname($doc);
  193. return array_values(array_unique($list));
  194. }
  195. ​
  196. function sd_base_ok(string $dir): bool
  197. {
  198. if ($dir === '' || !is_dir($dir) || !is_writable($dir) || dirname($dir) === $dir) return false;
  199. return !sd_inside($dir, sd_docroot()) && !sd_inside($dir, sd_app());
  200. }
  201. ​
  202. function sd_base(): ?string
  203. {
  204. static $base = false;
  205. if ($base !== false) return $base;
  206. $base = null;
  207. $custom = trim((string)(sd_config()['base'] ?? ''));
  208. if ($custom !== '') {
  209. $real = realpath($custom);
  210. if ($real !== false && sd_base_ok($real)) $base = $real;
  211. return $base;
  212. }
  213. foreach (sd_base_candidates() as $dir) {
  214. $real = realpath($dir);
  215. if ($real !== false && !sd_under_strong_webroot($real) && sd_base_ok($real)) return $base = $real;
  216. }
  217. return $base;
  218. }
  219. ​
  220. function sd_instance(): string
  221. {
  222. static $id = null;
  223. if ($id !== null) return $id;
  224. $file = SD_ROOT . '/' . SD_ID_FILE;
  225. $raw = is_file($file) ? trim((string)@file_get_contents($file)) : '';
  226. if (preg_match('/^[a-f0-9]{16}$/', $raw)) return $id = $raw;
  227. $new = bin2hex(random_bytes(8));
  228. if (@file_put_contents($file, $new . "\n", LOCK_EX) !== false) {
  229. @chmod($file, 0600);
  230. return $id = $new;
  231. }
  232. return $id = substr(hash('sha256', sd_app()), 0, 16);
  233. }
  234. ​
  235. function sd_slot(): string
  236. {
  237. static $slot = null;
  238. if ($slot !== null) return $slot;
  239. $base = sd_base();
  240. if ($base === null) return $slot = '';
  241. $id = sd_instance();
  242. if (is_dir($base . '/' . SD_PREFIX . '-' . $id)) return $slot = '-' . $id;
  243. $primary = $base . '/' . SD_PREFIX;
  244. $ownerFile = $primary . '/owner';
  245. if (!is_dir($primary) && @mkdir($primary, 0700)) {
  246. @file_put_contents($primary . '/.htaccess', "Require all denied\n");
  247. @file_put_contents($ownerFile, $id, LOCK_EX);
  248. return $slot = '';
  249. }
  250. $owner = is_file($ownerFile) ? trim((string)@file_get_contents($ownerFile)) : '';
  251. if ($owner === '' && is_dir($primary) && @file_put_contents($ownerFile, $id, LOCK_EX) !== false) return $slot = '';
  252. return $slot = $owner === $id ? '' : '-' . $id;
  253. }
  254. ​
  255. function sd_name(string $kind): string
  256. {
  257. $name = SD_PREFIX . sd_slot();
  258. return match ($kind) {
  259. 'env' => $name . '.env',
  260. 'setup' => $name . '.setup',
  261. default => $name,
  262. };
  263. }
  264. ​
  265. function sd_path(string $kind): string
  266. {
  267. return sd_base() . DIRECTORY_SEPARATOR . sd_name($kind);
  268. }
  269. ​
  270. function sd_data(string $sub = ''): string
  271. {
  272. $dir = sd_path('data');
  273. if (!is_dir($dir)) @mkdir($dir, 0700, true);
  274. if (is_dir($dir) && !is_file($dir . '/.htaccess')) @file_put_contents($dir . '/.htaccess', "Require all denied\n");
  275. if ($sub === '') return $dir;
  276. $path = $dir . '/' . $sub;
  277. if (!is_dir($path)) @mkdir($path, 0700, true);
  278. return $path;
  279. }
  280. ​
  281. function sd_url_base(): string
  282. {
  283. static $base = null;
  284. if ($base !== null) return $base;
  285. $script = str_replace('\\', '/', (string)($_SERVER['SCRIPT_NAME'] ?? ''));
  286. if ($script === '' || $script[0] !== '/' || basename($script) !== 'index.php') return $base = './';
  287. $dir = trim(str_replace('\\', '/', dirname($script)), '/');
  288. if ($dir === '' || $dir === '.') return $base = '/';
  289. return $base = '/' . implode('/', array_map('rawurlencode', explode('/', $dir))) . '/';
  290. }
  291. ​
  292. function sd_url(string $page = '', array $query = []): string
  293. {
  294. $params = $page !== '' ? ['p' => $page] + $query : $query;
  295. return sd_url_base() . ($params ? '?' . http_build_query($params) : '');
  296. }
  297. ​
  298. function sd_request_path_ok(): bool
  299. {
  300. $base = sd_url_base();
  301. if ($base === './') return true;
  302. $path = (string)parse_url('http://x' . (string)($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH);
  303. $path = (string)preg_replace('#/{2,}#', '/', rawurldecode($path));
  304. $dir = rawurldecode($base);
  305. return $path === $dir || $path === $dir . 'index.php';
  306. }
  307. ​
  308. function sd_env(bool $reload = false): ?array
  309. {
  310. static $env = false;
  311. if ($env !== false && !$reload) return $env;
  312. $env = null;
  313. if (sd_base() === null) return null;
  314. $file = sd_path('env');
  315. if (!is_file($file)) return null;
  316. $raw = @file_get_contents($file);
  317. if ($raw === false) return null;
  318. $env = [];
  319. foreach (preg_split('/\R/', $raw) as $line) {
  320. if (!preg_match('/^([A-Z][A-Z0-9_]{1,40})=(.*)$/', $line, $m)) continue;
  321. $value = json_decode($m[2], true);
  322. if ($value !== null || $m[2] === 'null') $env[$m[1]] = $value;
  323. }
  324. if (!isset($env['SD_USER'], $env['SD_PASS'], $env['SD_SECRET'])) $env = null;
  325. return $env;
  326. }
  327. ​
  328. function sd_env_save(array $env): void
  329. {
  330. $lines = [];
  331. foreach ($env as $key => $value) {
  332. if (!preg_match('/^[A-Z][A-Z0-9_]{1,40}$/', $key)) continue;
  333. $lines[] = $key . '=' . json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  334. }
  335. $file = sd_path('env');
  336. $tmp = $file . '.' . bin2hex(random_bytes(6)) . '.tmp';
  337. if (@file_put_contents($tmp, implode("\n", $lines) . "\n", LOCK_EX) === false) throw new RuntimeException('env_write');
  338. @chmod($tmp, 0600);
  339. if (!@rename($tmp, $file)) {
  340. @unlink($tmp);
  341. throw new RuntimeException('env_write');
  342. }
  343. sd_env(true);
  344. }
  345. ​
  346. function sd_env_set(array $changes): void
  347. {
  348. $env = sd_env(true) ?? [];
  349. foreach ($changes as $k => $v) $env[$k] = $v;
  350. sd_env_save($env);
  351. }
  352. ​
  353. function sd_state(string $name, callable $fn): mixed
  354. {
  355. $file = sd_data('state') . '/' . preg_replace('/[^a-z0-9_-]/', '', $name) . '.json';
  356. $fp = @fopen($file, 'c+');
  357. if ($fp === false) throw new RuntimeException('state_open');
  358. try {
  359. flock($fp, LOCK_EX);
  360. $raw = stream_get_contents($fp);
  361. $data = $raw ? json_decode($raw, true) : [];
  362. if (!is_array($data)) $data = [];
  363. $result = $fn($data);
  364. ftruncate($fp, 0);
  365. rewind($fp);
  366. fwrite($fp, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
  367. fflush($fp);
  368. flock($fp, LOCK_UN);
  369. return $result;
  370. } finally {
  371. fclose($fp);
  372. }
  373. }
  374. ​
  375. function sd_read_state(string $name): array
  376. {
  377. $file = sd_data('state') . '/' . preg_replace('/[^a-z0-9_-]/', '', $name) . '.json';
  378. if (!is_file($file)) return [];
  379. $data = json_decode((string)@file_get_contents($file), true);
  380. return is_array($data) ? $data : [];
  381. }
  382. ​
  383. function sd_audit(string $event, array $ctx = []): void
  384. {
  385. $line = json_encode([
  386. 't' => gmdate('c'),
  387. 'e' => $event,
  388. 'ip' => sd_ip(),
  389. 'ua' => mb_substr(sd_ua(), 0, 160),
  390. 'c' => $ctx,
  391. ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  392. $file = sd_data('logs') . '/audit.log';
  393. @file_put_contents($file, $line . "\n", FILE_APPEND | LOCK_EX);
  394. if (@filesize($file) > 5242880) @rename($file, $file . '.1');
  395. }
  396. ​
  397. function sd_audit_tail(int $limit = 100): array
  398. {
  399. $file = sd_data('logs') . '/audit.log';
  400. if (!is_file($file)) return [];
  401. $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
  402. $out = [];
  403. foreach (array_slice(array_reverse($lines), 0, $limit) as $line) {
  404. $row = json_decode($line, true);
  405. if (is_array($row)) $out[] = $row;
  406. }
  407. return $out;
  408. }
  409. ​
  410. function sd_headers(): void
  411. {
  412. header_remove('X-Powered-By');
  413. header("Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'; manifest-src 'self'");
  414. header('X-Content-Type-Options: nosniff');
  415. header('X-Frame-Options: DENY');
  416. header('Referrer-Policy: no-referrer');
  417. header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()');
  418. header('Cross-Origin-Opener-Policy: same-origin');
  419. header('Cross-Origin-Resource-Policy: same-origin');
  420. header('X-Robots-Tag: noindex, nofollow, noarchive');
  421. header('Cache-Control: no-store, max-age=0');
  422. if (sd_https()) header('Strict-Transport-Security: max-age=31536000');
  423. }
  424. ​
  425. function sd_cookie_name(): string
  426. {
  427. $name = 'syncdrop' . (sd_slot() === '' ? '' : '-' . substr(sd_instance(), 0, 8));
  428. return sd_https() ? '__Host-' . $name : $name;
  429. }
  430. ​
  431. function sd_session_start(): void
  432. {
  433. if (session_status() === PHP_SESSION_ACTIVE) return;
  434. ini_set('session.use_strict_mode', '1');
  435. ini_set('session.use_only_cookies', '1');
  436. ini_set('session.use_trans_sid', '0');
  437. ini_set('session.cookie_httponly', '1');
  438. ini_set('session.gc_maxlifetime', (string)SD_ABSOLUTE);
  439. session_save_path(sd_data('sessions'));
  440. session_name(sd_cookie_name());
  441. session_set_cookie_params([
  442. 'lifetime' => 0,
  443. 'path' => '/',
  444. 'secure' => sd_https(),
  445. 'httponly' => true,
  446. 'samesite' => 'Strict',
  447. ]);
  448. session_start();
  449. $now = time();
  450. $fingerprint = hash('sha256', sd_ua());
  451. if (isset($_SESSION['t0'])) {
  452. $expired = $now - (int)($_SESSION['last'] ?? 0) > SD_IDLE || $now - (int)$_SESSION['t0'] > SD_ABSOLUTE;
  453. if ($expired || !hash_equals((string)($_SESSION['fp'] ?? ''), $fingerprint)) {
  454. sd_session_reset();
  455. }
  456. }
  457. if (!isset($_SESSION['t0'])) {
  458. $_SESSION['t0'] = $now;
  459. $_SESSION['fp'] = $fingerprint;
  460. }
  461. $_SESSION['last'] = $now;
  462. if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32));
  463. sd_shield_inspect();
  464. }
  465. ​
  466. function sd_session_reset(): void
  467. {
  468. $_SESSION = [];
  469. session_regenerate_id(true);
  470. $_SESSION['t0'] = time();
  471. $_SESSION['fp'] = hash('sha256', sd_ua());
  472. $_SESSION['csrf'] = bin2hex(random_bytes(32));
  473. }
  474. ​
  475. function sd_session_elevate(string $stage): void
  476. {
  477. $keep = ['lang' => $_SESSION['lang'] ?? null];
  478. $_SESSION = [];
  479. session_regenerate_id(true);
  480. $_SESSION['t0'] = time();
  481. $_SESSION['last'] = time();
  482. $_SESSION['fp'] = hash('sha256', sd_ua());
  483. $_SESSION['csrf'] = bin2hex(random_bytes(32));
  484. $_SESSION['stage'] = $stage;
  485. $_SESSION['epoch'] = (int)(sd_env()['SD_EPOCH'] ?? 1);
  486. if ($keep['lang']) $_SESSION['lang'] = $keep['lang'];
  487. }
  488. ​
  489. function sd_stage(): string
  490. {
  491. $stage = (string)($_SESSION['stage'] ?? '');
  492. if ($stage === '') return '';
  493. $env = sd_env();
  494. if ($env === null || (int)($_SESSION['epoch'] ?? -1) !== (int)($env['SD_EPOCH'] ?? 1)) {
  495. sd_session_reset();
  496. return '';
  497. }
  498. return $stage;
  499. }
  500. ​
  501. function sd_csrf(): string
  502. {
  503. return (string)($_SESSION['csrf'] ?? '');
  504. }
  505. ​
  506. function sd_csrf_ok(): bool
  507. {
  508. $site = strtolower((string)($_SERVER['HTTP_SEC_FETCH_SITE'] ?? ''));
  509. if ($site !== '' && !in_array($site, ['same-origin', 'none'], true)) return false;
  510. $origin = (string)($_SERVER['HTTP_ORIGIN'] ?? '');
  511. if ($origin !== '' && $origin !== 'null') {
  512. $host = parse_url($origin, PHP_URL_HOST);
  513. $port = parse_url($origin, PHP_URL_PORT);
  514. $origHost = strtolower((string)$host) . ($port ? ':' . $port : '');
  515. if ($origHost !== sd_host()) return false;
  516. }
  517. $token = (string)($_POST['_csrf'] ?? ($_SERVER['HTTP_X_SYNCDROP_CSRF'] ?? ''));
  518. $expected = sd_csrf();
  519. return $expected !== '' && $token !== '' && hash_equals($expected, $token);
  520. }
  521. ​
  522. function sd_redirect(string $page = '', array $query = []): never
  523. {
  524. header('Location: ' . sd_url($page, $query), true, 303);
  525. exit;
  526. }
  527. ​
  528. function sd_json(array $data, int $status = 200): never
  529. {
  530. http_response_code($status);
  531. header('Content-Type: application/json; charset=utf-8');
  532. echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  533. exit;
  534. }
  535. ​
  536. function sd_flash(?string $type = null, ?string $message = null): ?array
  537. {
  538. if ($type !== null) {
  539. $_SESSION['flash'] = ['type' => $type, 'msg' => (string)$message];
  540. return null;
  541. }
  542. $flash = $_SESSION['flash'] ?? null;
  543. unset($_SESSION['flash']);
  544. return is_array($flash) ? $flash : null;
  545. }
  546. ​
  547. function sd_password_hash(string $password): string
  548. {
  549. if (defined('PASSWORD_ARGON2ID')) {
  550. return password_hash($password, PASSWORD_ARGON2ID, ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1]);
  551. }
  552. return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
  553. }
  554. ​
  555. function sd_password_strong(string $password, string $user): bool
  556. {
  557. if (mb_strlen($password) < 12 || mb_strlen($password) > 256) return false;
  558. if (stripos($password, $user) !== false) return false;
  559. $classes = 0;
  560. foreach (['/[a-z]/', '/[A-Z]/', '/\d/', '/[^a-zA-Z\d]/'] as $re) {
  561. if (preg_match($re, $password)) $classes++;
  562. }
  563. return $classes >= 3;
  564. }
  565. ​
  566. function sd_format_bytes(int $bytes): string
  567. {
  568. $units = ['B', 'KB', 'MB', 'GB'];
  569. $i = 0;
  570. $n = (float)$bytes;
  571. while ($n >= 1024 && $i < count($units) - 1) {
  572. $n /= 1024;
  573. $i++;
  574. }
  575. return ($i === 0 ? (string)$bytes : number_format($n, 1)) . ' ' . $units[$i];
  576. }
  577. ​
  578. function sd_rrmdir(string $dir, string $mustBeInside): void
  579. {
  580. $real = realpath($dir);
  581. $root = realpath($mustBeInside);
  582. if ($real === false || $root === false || !sd_inside($real, $root) || $real === $root) return;
  583. $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($real, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
  584. foreach ($it as $item) {
  585. if ($item->isDir() && !$item->isLink()) @rmdir($item->getPathname());
  586. else @unlink($item->getPathname());
  587. }
  588. @rmdir($real);
  589. }
  590. ​