0 && $len <= 255; } /** realpath($p) and ensure it remains inside $base (defends symlink escape). */ public static function safeReal(string $baseReal, string $p): ?string { $rp = realpath($p); if ($rp === false) return null; $base = rtrim($baseReal, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; $rp2 = rtrim($rp, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if (strpos($rp2, $base) !== 0) return null; return rtrim($rp, DIRECTORY_SEPARATOR); } /** * Small bounded DFS to learn if an unreadable folder has any readable descendant (for “locked” rows). * $maxDepth intentionally small to avoid expensive scans. */ public static function hasReadableDescendant( string $baseReal, string $absPath, string $relPath, string $user, array $perms, int $maxDepth = 2 ): bool { if ($maxDepth <= 0 || !is_dir($absPath)) return false; $IGNORE = self::IGNORE(); $SKIP = self::SKIP(); $items = @scandir($absPath) ?: []; foreach ($items as $child) { if ($child === '.' || $child === '..') continue; if ($child[0] === '.') continue; if (in_array($child, $IGNORE, true)) continue; if (!self::isSafeSegment($child)) continue; $lower = strtolower($child); if (in_array($lower, $SKIP, true)) continue; $abs = $absPath . DIRECTORY_SEPARATOR . $child; if (!@is_dir($abs)) continue; // Resolve symlink safely if (@is_link($abs)) { $safe = self::safeReal($baseReal, $abs); if ($safe === null || !is_dir($safe)) continue; $abs = $safe; } $rel = ($relPath === 'root') ? $child : ($relPath . '/' . $child); if (ACL::canRead($user, $perms, $rel) || ACL::canReadOwn($user, $perms, $rel)) { return true; } if ($maxDepth > 1 && self::hasReadableDescendant($baseReal, $abs, $rel, $user, $perms, $maxDepth - 1)) { return true; } } return false; } }