getFilename()) { return true; } } } catch (\Throwable) { // A directory we cannot inspect may contain private data. Failing closed // is safer than treating a configuration change as a migration. return true; } return false; } /** * Records only a verified, plugin-created private root. If a site changes * its configured parent later, uninstall can discover and marker-validate * the old root instead of silently leaving encrypted review data behind. */ private static function remember_directory(string $directory): bool { $existing = get_option(self::ROOTS_OPTION, array()); $roots = array(); foreach (is_array($existing) ? $existing : array() as $root) { if (!is_string($root)) { continue; } $root = untrailingslashit(trim($root)); if ('' !== $root && strlen($root) <= 4096 && self::is_absolute_path($root) && basename($root) === self::directory_name()) { $roots[$root] = true; } } $roots[$directory] = true; $stored = array_keys($roots); sort($stored, SORT_STRING); if (!add_option(self::ROOTS_OPTION, $stored, '', false)) { update_option(self::ROOTS_OPTION, $stored, false); } $verified = get_option(self::ROOTS_OPTION, array()); return is_array($verified) && in_array($directory, $verified, true); } private static function directory_name(): string { return self::DIRECTORY_PREFIX . self::install_id(); } private static function marker_content(): string { $install_id = self::install_id(); return '' === $install_id ? '' : "CommentMate private storage\ninstall=" . $install_id . "\n"; } private static function install_id(): string { $stored = get_option(self::INSTALL_ID_OPTION, ''); if (is_string($stored) && preg_match('/\A[a-f0-9]{24}\z/', $stored)) { return $stored; } try { $generated = bin2hex(random_bytes(12)); } catch (\Throwable) { return ''; } if (!add_option(self::INSTALL_ID_OPTION, $generated, '', false)) { $stored = get_option(self::INSTALL_ID_OPTION, ''); if (is_string($stored) && preg_match('/\A[a-f0-9]{24}\z/', $stored)) { return $stored; } // Do not overwrite a malformed/concurrent value with an untracked path. // Failing closed here is safer than allowing two installations to share a // deterministic storage directory or marker. return ''; } return $generated; } private static function is_absolute_path(string $path): bool { return DIRECTORY_SEPARATOR === substr($path, 0, 1) || (bool) preg_match('/\A[A-Za-z]:[\\\\\/]/', $path); } /** * @return list */ public static function public_roots(): array { $filesystem = self::local_filesystem(); $uploads = wp_get_upload_dir(); $candidates = array( $filesystem->abspath(), $filesystem->wp_content_dir(), $filesystem->wp_plugins_dir(), $uploads['basedir'] ?? '', self::document_root() ?? '', ); $roots = array(); foreach ($candidates as $candidate) { if (!is_string($candidate) || '' === trim($candidate)) { continue; } $real = realpath($candidate); if (false !== $real && is_dir($real)) { $roots[] = untrailingslashit($real) ?: DIRECTORY_SEPARATOR; } } return array_values(array_unique($roots)); } /** Keep the existing install-root sibling default without guessing from URLs. */ public static function wordpress_root(): string { $root = self::local_filesystem()->abspath(); return is_string($root) ? $root : ''; } /** * Use core's directory helpers on the local filesystem explicitly. A global * FTP/SSH transport may describe remote paths; never use those to authorize * local storage or uninstall. Loading these classes makes no connection, * asks for no credentials and does not replace the global filesystem object. */ private static function local_filesystem(): \WP_Filesystem_Direct { if (!class_exists('WP_Filesystem_Base')) { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; } if (!class_exists('WP_Filesystem_Direct')) { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; } return new \WP_Filesystem_Direct(null); } private static function document_root(): ?string { if (!isset($_SERVER['DOCUMENT_ROOT']) || !is_string($_SERVER['DOCUMENT_ROOT']) || '' === trim(wp_unslash($_SERVER['DOCUMENT_ROOT']))) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Literal filesystem path is string-checked, unslashed and resolved with realpath before containment checks; text sanitization can change the protected path. return null; } $candidate = realpath((string) wp_unslash($_SERVER['DOCUMENT_ROOT'])); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Literal filesystem path is string-checked, unslashed and resolved with realpath before containment checks; text sanitization can change the protected path. return false !== $candidate && is_dir($candidate) ? untrailingslashit($candidate) : null; } }