From 8ce74665a1c943261b8d6ae684991114a2a80de9 Mon Sep 17 00:00:00 2001 From: gtbu Date: Sat, 5 Apr 2025 14:59:54 +0200 Subject: [PATCH] Update Status.php --- include/admin/Tools/Status.php | 710 ++++++++++++++++++++------------- 1 file changed, 434 insertions(+), 276 deletions(-) diff --git a/include/admin/Tools/Status.php b/include/admin/Tools/Status.php index 6c5a78f..ab9d576 100644 --- a/include/admin/Tools/Status.php +++ b/include/admin/Tools/Status.php @@ -1,28 +1,40 @@ */ + protected array $failed = []; + protected int $passed_count = 0; + protected int $show_failed_max = 50; + /** @var list */ + protected array $deletable = []; + protected string|int|null $euid = null; // Can be string '?', int from posix_geteuid, or null initially + public function __construct() + { + // Constructor remains empty if no initialization is needed here } - public function RunScript() { - global $langmessage; + public function RunScript(): void + { + global $langmessage; // Access global variable - echo '

' . $langmessage['Site Status'] . '

'; + if (!is_array($langmessage)) { + // Handle missing/invalid global gracefully + $langmessage = ['Site Status' => 'Site Status']; // Provide default + trigger_error('$langmessage global variable not available or not an array.', E_USER_WARNING); + } + + echo '

' . htmlspecialchars($langmessage['Site Status'] ?? 'Site Status') . '

'; $cmd = \gp\tool::GetCommand(); switch ($cmd) { @@ -30,187 +42,271 @@ class Status extends \gp\special\Base { $this->FixOwner(); break; } - $this->CheckDataDir(); $this->DefaultDisplay(); } - public function CheckDataDir() { - global $dataDir; + public function CheckDataDir(): void + { + global $dataDir; // Access global variable + + if (!is_string($dataDir) || $dataDir === '') { + // Handle missing/invalid global gracefully + trigger_error('$dataDir global variable not available or empty.', E_USER_ERROR); + echo '

Configuration error: dataDir is not set.

'; + return; + } $this->check_dir_len = 0; $this->failed_count = 0; $this->passed_count = 0; + $this->failed = []; + $this->deletable = []; $this->show_failed_max = 50; - $check_dir = $dataDir . '/data'; $this->check_dir_len = strlen($check_dir); - $this->euid = '?'; + $this->euid = '?'; // Default string value + // Check if POSIX functions are available and get effective user ID if (function_exists('posix_geteuid')) { - $this->euid = posix_geteuid(); + $euid = posix_geteuid(); + // posix_geteuid returns int, assign directly + $this->euid = $euid; } - - $this->CheckDir($check_dir); + if (is_dir($check_dir)) { + $this->CheckDir($check_dir); + } else { + echo '

Error: Data directory does not exist: ' . htmlspecialchars($check_dir) . '

'; + $this->failed_count = 1; // Mark as failed if base dir doesn't exist + $this->failed[] = $check_dir; + } } - public function DefaultDisplay() { - global $langmessage, $dataDir; + public function DefaultDisplay(): void + { + global $langmessage, $dataDir; // Access global variables - $check_dir = $dataDir . '/data'; - $checked = $this->passed_count + $this->failed_count; - - if ($this->failed_count === 0) { - echo '

'; - echo sprintf($langmessage['data_check_passed'], $checked, $checked); - echo '

'; - $this->ShowDeletable(); - - return; - } - - echo '

'; - echo sprintf($langmessage['data_check_failed'], $this->failed_count, $checked); - echo '

'; - - - // the /data directory isn't writable - if (count($this->failed) == 1 && in_array($check_dir, $this->failed)) { - echo '

'; - echo 'WARNING: Your data directory at is no longer writable: ' . htmlspecialchars($check_dir, ENT_QUOTES, 'UTF-8'); // escape output - echo '

'; - return; - } - - - if ($this->failed_count > $this->show_failed_max) { - echo '

'; - echo sprintf($langmessage['showing_max_failed'], $this->show_failed_max); - echo '

'; - } - - - echo ''; - echo ''; - - // sort by strlen to get directories first - usort($this->failed, function ($a, $b) { - return strlen($a) - strlen($b); - }); - - foreach ($this->failed as $i => $path) { - - if ($i > $this->show_failed_max) { - break; + if (!is_array($langmessage)) { + $langmessage = []; // Prevent errors if not set + } + if (!is_string($dataDir)) { + $dataDir = ''; // Prevent errors if not set } - $readable_path = substr($path, $this->check_dir_len); + $check_dir = $dataDir . '/data'; + $checked = $this->passed_count + $this->failed_count; - // Sanitize the readable_path BEFORE passing it to rawurlencode and \gp\tool::Link - $safe_readable_path = $this->sanitizePath($readable_path); - - $euid = \gp\install\FilePermissions::file_uid($path); - - echo ''; - } - - echo '
'; - echo $langmessage['file_name']; - echo ''; - echo $langmessage['File Owner']; - echo '
'; - echo $langmessage['Current_Value']; - echo '
'; - echo '
'; - echo $langmessage['Expected_Value']; - echo '
'; - echo '
'; - echo htmlspecialchars($readable_path, ENT_QUOTES, 'UTF-8'); // escape output - echo ''; - - echo $this->ShowUser($euid); - echo ''; - echo $this->ShowUser($this->euid); - echo ''; - // Use the sanitized path - echo \gp\tool::Link('Admin/Status', 'Fix', 'cmd=FixOwner&path=' . rawurlencode($safe_readable_path), 'data-cmd="cnreq"'); - echo '
'; - - $this->CheckPageFiles(); - $this->ShowDeletable(); -} - - - /** - * Show Deletable Files - */ - protected function ShowDeletable() { - if (empty($this->deletable)) { + if ($this->failed_count === 0) { + echo '

'; + echo sprintf( + htmlspecialchars($langmessage['data_check_passed'] ?? 'Passed %d checks out of %d.'), + $checked, + $checked + ); + echo '

'; + $this->ShowDeletable(); return; } + echo '

'; + echo sprintf( + htmlspecialchars($langmessage['data_check_failed'] ?? 'Failed %d checks out of %d.'), + $this->failed_count, + $checked + ); + echo '

'; + + // Special message if only the main /data directory isn't writable/accessible + if (count($this->failed) === 1 && $this->failed[0] === $check_dir) { + echo '

'; + echo 'WARNING: Your data directory is not accessible or writable: ' . htmlspecialchars($check_dir); + echo '

'; + // Don't show the table if only the root data dir failed + return; + } + + if ($this->failed_count > $this->show_failed_max) { + echo '

'; + echo sprintf( + htmlspecialchars($langmessage['showing_max_failed'] ?? 'Showing first %d failed items.'), + $this->show_failed_max + ); + echo '

'; + } + + echo ''; + echo ''; + echo ''; + + // Sort by path length (directories often shorter, appear first) + usort($this->failed, fn (string $a, string $b): int => strlen($a) <=> strlen($b)); + + $shown_count = 0; + foreach ($this->failed as $path) { + if ($shown_count >= $this->show_failed_max) { + break; + } + + $readable_path = substr($path, $this->check_dir_len); + if ($readable_path === '') { + $readable_path = '/'; // Represent the base data dir + } + + // Attempt to get file UID if function exists + $file_euid = '?'; // Default if cannot determine + if (class_exists('\gp\install\FilePermissions') && method_exists('\gp\install\FilePermissions', 'file_uid')) { + $file_euid = \gp\install\FilePermissions::file_uid($path); + } + + echo ''; + $shown_count++; + } + + echo '
'; + echo htmlspecialchars($langmessage['file_name'] ?? 'File/Folder'); + echo ''; + echo htmlspecialchars($langmessage['File Owner'] ?? 'File Owner'); + echo '
'; + echo htmlspecialchars($langmessage['Current_Value'] ?? 'Current'); + echo '
'; + echo '
'; + echo htmlspecialchars($langmessage['Expected_Value'] ?? 'Expected'); + echo '
 '; // Actions column + echo '
'; + echo htmlspecialchars($readable_path); + echo ''; + echo htmlspecialchars($this->ShowUser($file_euid)); + echo ''; + echo htmlspecialchars($this->ShowUser($this->euid)); + echo ''; + // Only show Fix link if POSIX functions likely available (used by ShowUser) + if (function_exists('posix_geteuid')) { + echo \gp\tool::Link( + 'Admin/Status', + 'Fix', // $langmessage['Fix'] ?? 'Fix' + 'cmd=FixOwner&path=' . rawurlencode($readable_path), + ['data-cmd' => 'cnreq', 'title' => 'Attempt to fix ownership/permissions'] + ); + } else { + echo ' '; // No fix action available + } + echo '
'; + + // Only check for orphans if the primary checks didn't completely fail + if (!(count($this->failed) === 1 && $this->failed[0] === $check_dir)) { + $this->CheckPageFiles(); + } + + $this->ShowDeletable(); + } + + /** + * Show Deletable Files found during scan + */ + protected function ShowDeletable(): void + { + if (empty($this->deletable)) { + return; + } echo '

Deletable Files

'; + echo '

The following files or folders were marked as deletable during the scan:

'; echo '
    '; foreach ($this->deletable as $file) { - echo '
  1. ' . htmlspecialchars($file, ENT_QUOTES, 'UTF-8') . '
  2. '; // escape output + echo '
  3. ' . htmlspecialchars($file) . '
  4. '; } echo '
'; } - - /** * Check page files for orphaned data files - * */ - protected function CheckPageFiles() { - global $dataDir, $gp_index; + protected function CheckPageFiles(): void + { + global $dataDir, $gp_index; // Access global variables + + if (!is_string($dataDir) || $dataDir === '') { + trigger_error('$dataDir global variable not available or empty for CheckPageFiles.', E_USER_WARNING); + return; + } + if (!is_array($gp_index)) { + trigger_error('$gp_index global variable not available or not an array for CheckPageFiles.', E_USER_WARNING); + $gp_index = []; // Prevent errors + } $pages_dir = $dataDir . '/data/_pages'; - $all_files = \gp\tool\Files::ReadDir($pages_dir, 'php'); - foreach ($all_files as $key => $file) { - $all_files[$key] = $pages_dir . '/' . $file . '.php'; + if (!is_dir($pages_dir)) { + // If _pages dir doesn't exist, nothing to check + return; } - $page_files = array(); - foreach ($gp_index as $slug => $index) { - $page_files[] = \gp\tool\Files::PageFile($slug); + // Use Files helper if available, otherwise fallback or skip + if (!class_exists('\gp\tool\Files') || !method_exists('\gp\tool\Files', 'ReadDir')) { + echo '

Cannot check for orphaned files: Files helper is unavailable.

'; + return; } - $diff = array_diff($all_files, $page_files); + $all_files_in_dir = \gp\tool\Files::ReadDir($pages_dir, 'php'); + $all_page_data_files = []; + foreach ($all_files_in_dir as $file) { + // Assuming ReadDir returns filenames without extension + $all_page_data_files[] = $pages_dir . '/' . $file . '.php'; + } - if (!count($diff)) { + $active_page_files = []; + if (method_exists('\gp\tool\Files', 'PageFile')) { + foreach (array_keys($gp_index) as $slug) { + if(is_string($slug)){ // Ensure slug is a string + $active_page_files[] = \gp\tool\Files::PageFile($slug); + } + } + } else { + echo '

Cannot check for orphaned files: PageFile helper is unavailable.

'; + return; + } + + + // Find files present in the directory but not in the active page index + $diff = array_diff($all_page_data_files, $active_page_files); + + if (empty($diff)) { return; } echo '

Orphaned Data Files

'; - echo '

The following data files appear to be orphaned and are most likely no longer needed. Before completely removing these files, we recommend backing them up first.

'; - echo ''; + echo '

The following data files exist in the _pages directory but do not seem to correspond to any currently active page. They might be remnants of deleted pages. Before removing them, consider backing them up.

'; + echo '
File
'; + echo ''; foreach ($diff as $file) { - echo ''; + echo ''; } - echo '
File Path
' - . htmlspecialchars($file, ENT_QUOTES, 'UTF-8') // escape output - . '
' . htmlspecialchars($file) . '
'; + echo ''; } /** - * Check the ownership of the directory and files within it - * @param string $dir - * + * Check the ownership/permissions of a directory and recursively its contents. */ - protected function CheckDir($dir) { - - if (!$this->CheckFile($dir)) { - return; + protected function CheckDir(string $dir): void + { + // First, check the directory itself + if (!$this->CheckFile($dir, 'dir')) { + // If the directory check failed (e.g., permissions), maybe don't recurse? + // Original logic continued, so we keep that. If dir isn't readable, opendir will fail. + // $this->failed[] is already populated by CheckFile if it fails. } + // Attempt to open the directory + // Use error suppression carefully, consider try-catch for FilesystemIterator later if refactoring $dh = @opendir($dir); if ($dh === false) { - $this->failed_count++; - $this->failed[] = $dir; + // If opendir failed after CheckFile potentially passed (e.g., race condition or complex ACLs) + // ensure it's marked as failed if not already. + if (!in_array($dir, $this->failed, true)) { + $this->failed_count++; + $this->failed[] = $dir; + } + // Cannot proceed further into this directory return; } @@ -220,178 +316,240 @@ class Status extends \gp\special\Base { } $full_path = $dir . '/' . $file; + + // Skip symbolic links to avoid issues, unless specifically needed if (is_link($full_path)) { continue; } - if (preg_match('#x-deletable-[0-9]+#', $full_path)) { - $this->deletable[] = $full_path; - continue; + // Check for specially named deletable files/folders + if (preg_match('#/x-deletable-[0-9]+$#', $full_path)) { + if (!in_array($full_path, $this->deletable, true)) { + $this->deletable[] = $full_path; + } + continue; // Don't check permissions on deletable items } if (is_dir($full_path)) { - $this->CheckDir($full_path); + $this->CheckDir($full_path); // Recurse into subdirectory } else { - $this->CheckFile($full_path, 'file'); + $this->CheckFile($full_path, 'file'); // Check file } } + + closedir($dh); // Always close the directory handle } - protected function CheckFile($path, $type = 'dir') { + /** + * Check the ownership/permissions of a single file or directory. + * Returns true if checks pass, false otherwise. + */ + protected function CheckFile(string $path, string $type = 'dir'): bool + { + // Use FilePermissions helper if available + if (class_exists('\gp\install\FilePermissions') && method_exists('\gp\install\FilePermissions', 'HasFunctions')) { + if (\gp\install\FilePermissions::HasFunctions()) { + $perms = @fileperms($path); + if ($perms === false) { + // Cannot get permissions, count as failed + $this->failed_count++; + $this->failed[] = $path; + return false; + } + // Get last 3 digits of octal representation + $current_perms = substr(sprintf('%o', $perms), -3); - if (\gp\install\FilePermissions::HasFunctions()) { - $current = @substr(decoct(@fileperms($path)), -3); + if ($type === 'file') { + $expected_perms = \gp\install\FilePermissions::getExpectedPerms_file($path); + } else { + $expected_perms = \gp\install\FilePermissions::getExpectedPerms($path); + } - if ($type === 'file') { - $expected = \gp\install\FilePermissions::getExpectedPerms_file($path); - } else { - $expected = \gp\install\FilePermissions::getExpectedPerms($path); + // Compare permissions using helper + if (\gp\install\FilePermissions::perm_compare($expected_perms, $current_perms)) { + $this->passed_count++; + return true; + } else { + // Permission mismatch, count as failed + $this->failed_count++; + $this->failed[] = $path; + return false; + } } + } - if (\gp\install\FilePermissions::perm_compare($expected, $current)) { - $this->passed_count++; - return true; - } - } elseif (gp_is_writable($path)) { + // Fallback check: Check if writable (less precise but better than nothing) + // Use standard is_writable if gp_is_writable doesn't exist + $is_writable_func = function_exists('gp_is_writable') ? 'gp_is_writable' : 'is_writable'; + + if ($is_writable_func($path)) { $this->passed_count++; return true; } + // Fallback check failed $this->failed_count++; $this->failed[] = $path; - return false; } - /** - * Display a user name and uid - * @param int $uid + * Display a user name and UID if possible. */ - protected function ShowUser($uid) { - $user_info = posix_getpwuid($uid); - if ($user_info) { - return htmlspecialchars($user_info['name'], ENT_QUOTES, 'UTF-8') . ' (' . $uid . ')'; // escape output + protected function ShowUser(int|string|null $uid): string + { + // If UID is not an integer or posix functions are not available, just return the UID as string + if (!is_int($uid) || !function_exists('posix_getpwuid')) { + return (string) $uid; // Cast null or string '?' to string } - return $uid; - } + // Attempt to get user info only if we have an integer UID + $user_info = posix_getpwuid($uid); + if (is_array($user_info) && isset($user_info['name'])) { + // Found user info, return name and UID + return $user_info['name'] . ' (' . $uid . ')'; + } else { + // Function failed or user not found, return only UID + return (string) $uid; + } + } /** - * Attempt to fix the ownership issue of the posted file - * 1) create a copy of the file - * 2) move old to temp folder - * 3) move new into original place of old - * 4) attempt to delete temp folder - * + * Attempt to fix the ownership/permissions issue of the posted file/folder. + * This is a potentially risky operation. */ - - - public function FixOwner() { - global $dataDir, $langmessage; + public function FixOwner(): void + { + global $dataDir, $langmessage; // Access global variables - // Sanitize the path from user input - $unsafe_path = isset($_REQUEST['path']) ? $_REQUEST['path'] : ''; - $safe_path = $this->sanitizePath($unsafe_path); + if (!is_array($langmessage)) { + $langmessage = []; // Prevent errors + } + if (!is_string($dataDir) || $dataDir === '') { + trigger_error('$dataDir global variable not available or empty for FixOwner.', E_USER_ERROR); + msg($langmessage['OOPS'] ?? 'Oops!' . ' Configuration error.'); + return; + } - // Validate path AFTER sanitization - if (strpos($safe_path, 'javascript:') !== false) { - msg($langmessage['OOPS'] . ' Invalid Path: javascript: URI is not allowed.'); - return; + // Basic input check + if (!isset($_REQUEST['path']) || !is_string($_REQUEST['path']) || $_REQUEST['path'] === '') { + msg($langmessage['OOPS'] ?? 'Oops!' . ' Invalid request path.'); + return; + } + + // Path received from URL needs decoding; should correspond to $readable_path + $relative_path = $_REQUEST['path']; + $to_fix = '/data' . $relative_path; + $to_fix_full = $dataDir . $to_fix; + + // Use Files helper for path checking if available + if (class_exists('\gp\tool\Files') && method_exists('\gp\tool\Files', 'CheckPath')) { + if (!\gp\tool\Files::CheckPath($to_fix_full, false)) { // Check existence without is_executable + msg(($langmessage['OOPS'] ?? 'Oops!') . ' Invalid or non-existent path: ' . htmlspecialchars($to_fix)); + return; + } + } elseif (!file_exists($to_fix_full)) { + // Basic fallback check + msg(($langmessage['OOPS'] ?? 'Oops!') . ' Path does not exist: ' . htmlspecialchars($to_fix)); + return; + } + + // Check dependencies for FileSystem and Port helpers + if (!class_exists('\gp\tool\FileSystem') || !method_exists('\gp\tool\FileSystem', 'TempFile')) { + msg(($langmessage['OOPS'] ?? 'Oops!') . ' FileSystem helper unavailable.'); + return; + } + if (!class_exists('\gp\admin\Tools\Port') || !method_exists('\gp\admin\Tools\Port', 'CopyAll')) { + msg(($langmessage['OOPS'] ?? 'Oops!') . ' Port helper unavailable.'); + return; + } + if (!class_exists('\gp\tool\Files') || !method_exists('\gp\tool\Files', 'RmAll')) { + // RmAll is needed for cleanup + } + + + // Generate temporary paths using the helper + $new_file_rel = \gp\tool\FileSystem::TempFile($to_fix); // Relative path from dataDir + $new_file_full = $dataDir . $new_file_rel; + $deletable_rel = \gp\tool\FileSystem::TempFile(dirname($to_fix) . '/x-deletable'); // Relative path + $deletable_full = $dataDir . $deletable_rel; + + echo '

Attempting to fix: ' . htmlspecialchars($to_fix) . '

'; + echo '
    '; + + // 1. Copy original to new temporary location (should inherit correct owner/perms) + echo '
  1. Copy: ' . htmlspecialchars($to_fix) . ' → ' . htmlspecialchars($new_file_rel) . '
  2. '; + if (!\gp\admin\Tools\Port::CopyAll($to_fix_full, $new_file_full)) { + echo '
  3. Failed: Could not copy.
  4. '; + echo '
'; + msg(($langmessage['OOPS'] ?? 'Oops!') . ' Failed to create temporary copy.'); + // Attempt cleanup of potentially partially created new file + if (file_exists($new_file_full) && class_exists('\gp\tool\Files') && method_exists('\gp\tool\Files', 'RmAll')) { + \gp\tool\Files::RmAll($new_file_full); + } + return; + } + + // 2. Move original to a deletable location + echo '
  • Move (Original): ' . htmlspecialchars($to_fix) . ' → ' . htmlspecialchars($deletable_rel) . '
  • '; + if (!@rename($to_fix_full, $deletable_full)) { + echo '
  • Failed: Could not move original to deletable location.
  • '; + echo ''; + msg(($langmessage['OOPS'] ?? 'Oops!') . ' Rename to deletable location failed.'); + // Attempt cleanup of the temporary copy + if (class_exists('\gp\tool\Files') && method_exists('\gp\tool\Files', 'RmAll')) { + \gp\tool\Files::RmAll($new_file_full); + } + return; + } + + // 3. Move the new copy into the original's place + echo '
  • Move (New Copy): ' . htmlspecialchars($new_file_rel) . ' → ' . htmlspecialchars($to_fix) . '
  • '; + if (!@rename($new_file_full, $to_fix_full)) { + echo '
  • Failed: Could not move new copy into original place.
  • '; + echo ''; + msg(($langmessage['OOPS'] ?? 'Oops!') . ' Rename to original location failed.'); + // CRITICAL: Try to move the original back from deletable + if (@rename($deletable_full, $to_fix_full)) { + msg('Attempted to restore original file.'); + } else { + msg('CRITICAL ERROR: Could not restore original file. Manual intervention required.'); + } + // Cleanup the temporary copy if it still exists somehow + if (file_exists($new_file_full) && class_exists('\gp\tool\Files') && method_exists('\gp\tool\Files', 'RmAll')) { + \gp\tool\Files::RmAll($new_file_full); + } + return; + } + + echo '
  • Success: File/folder replaced.
  • '; + + // 4. Attempt to remove the old version from the deletable location + if (class_exists('\gp\tool\Files') && method_exists('\gp\tool\Files', 'RmAll')) { + if (!\gp\tool\Files::RmAll($deletable_full)) { + echo '
  • Note: Deletable temporary file/folder ' . htmlspecialchars($deletable_rel) . ' could not be removed. It might require manual deletion.
  • '; + } else { + echo '
  • Cleanup: Removed temporary original from ' . htmlspecialchars($deletable_rel) . '.
  • '; + } + } else { + echo '
  • Note: Could not attempt cleanup of ' . htmlspecialchars($deletable_rel) . ' (RmAll helper missing).
  • '; + } + + echo ''; + msg('Fix attempt completed. Please refresh the status page to verify.', true); // Success message } - if (strpos($safe_path, 'data:') !== false) { - msg($langmessage['OOPS'] . ' Invalid Path: data: URI is not allowed.'); - return; - } - - $to_fix = '/data' . $safe_path; - $to_fix_full = $dataDir . $to_fix; - - $new_file = \gp\tool\FileSystem::TempFile($to_fix); - $new_file_full = $dataDir . $new_file; - - $deletable = \gp\tool\FileSystem::TempFile(dirname($to_fix) . '/x-deletable'); - $deletable_full = $dataDir . $deletable; - - if (!\gp\tool\Files::CheckPath($to_fix_full)) { - msg($langmessage['OOPS'] . ' Invalid Path'); - return; - } - - echo '
      '; - echo '
    1. Copy: ' . htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8') . ' -> ' . htmlspecialchars($new_file, ENT_QUOTES, 'UTF-8') . '
    2. '; - - if (!\gp\admin\Tools\Port::CopyAll($to_fix_full, $new_file_full)) { - echo '
    3. Failed
    4. '; - echo '
    '; - msg($langmessage['OOPS'] . ' Not Copied'); - \gp\tool\Files::RmAll($new_file_full); - return; - } - - // move old to deletable - echo '
  • Move: ' . htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8') . ' -> ' . htmlspecialchars($deletable, ENT_QUOTES, 'UTF-8') . '
  • '; - if (!rename($to_fix_full, $deletable_full)) { - echo '
  • Failed
  • '; - echo ''; - msg($langmessage['OOPS'] . ' Rename to deletable failed'); - \gp\tool\Files::RmAll($new_file_full); - return; - } - - // move - echo '
  • Move: ' . htmlspecialchars($new_file, ENT_QUOTES, 'UTF-8') . ' -> ' . htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8') . '
  • '; - if (!rename($new_file_full, $to_fix_full)) { - echo '
  • Failed
  • '; - echo ''; - msg($langmessage['OOPS'] . ' Rename to old failed'); - return; - } - - echo '
  • Success
  • '; - - // attempt to remove deletable - if (!\gp\tool\Files::RmAll($deletable_full)) { - echo '
  • Note: ' . htmlspecialchars($deletable, ENT_QUOTES, 'UTF-8') . ' was not deleted
  • '; - } - - echo ''; } - /** - * Sanitizes a path to prevent directory traversal and XSS. - * - * @param string $path The path to sanitize. - * @return string The sanitized path. - */ - private function sanitizePath(string $path): string -{ - // Block Javascript and data URI - if (stripos($path, 'javascript:') !== false || stripos($path, 'data:') !== false) { - return ''; // or throw an exception, or return a safe default path + +// Helper function 'msg' needs to be defined globally or within the scope where this class is used. +// Example definition: +if (!function_exists('msg')) { + function msg(string $message, bool $isSuccess = false): void { + echo '
    ' . htmlspecialchars($message) . '
    '; } +} - // Remove any characters that aren't alphanumeric, underscores, dashes, or slashes - $sanitized = preg_replace('/[^a-zA-Z0-9_\-\/]/', '', $path); - - // Resolve directory traversal attempts (e.g., ../../) - $sanitized = str_replace('..', '', $sanitized); - - // Remove potentially harmful URL components - $sanitized = str_replace('href', '', strtolower($sanitized)); - - // Remove leading and trailing slashes - $sanitized = trim($sanitized, '/'); - - // Ensure no double slashes - $sanitized = preg_replace('/\/+/', '/', $sanitized); - - // Add a leading slash - if ($sanitized !== '') { - $sanitized = '/' . $sanitized; - } - - return $sanitized; - } - -} \ No newline at end of file +// gp\tool::GetCommand(), gp\tool::Link(), gp\tool\Files::*, gp\tool\FileSystem::*, +// gp\install\FilePermissions::*, gp\admin\Tools\Port::*, gp_is_writable() +// and globals $langmessage, $dataDir, $gp_index +// MUST all be defined and compatible with PHP 8.4 elsewhere in your application. \ No newline at end of file