Update Status.php

This commit is contained in:
gtbu 2025-04-05 14:59:54 +02:00
parent 70e9ecefbc
commit 8ce74665a1

View file

@ -1,28 +1,40 @@
<?php
declare(strict_types=1); // Enforce strict types for better code quality
namespace gp\admin\Tools;
// Ensure this script is run within the context of the application
defined('is_running') or die('Not an entry point...');
class Status extends \gp\special\Base {
protected $check_dir_len = 0;
protected $failed_count = 0;
protected $failed = [];
protected $passed_count = 0;
protected $show_failed_max = 50;
protected $deletable = [];
protected $euid;
public function __construct() {
class Status extends \gp\special\Base
{
protected int $check_dir_len = 0;
protected int $failed_count = 0;
/** @var list<string> */
protected array $failed = [];
protected int $passed_count = 0;
protected int $show_failed_max = 50;
/** @var list<string> */
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 '<h2>' . $langmessage['Site Status'] . '</h2>';
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 '<h2>' . htmlspecialchars($langmessage['Site Status'] ?? 'Site Status') . '</h2>';
$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 '<p class="gp_error">Configuration error: dataDir is not set.</p>';
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 '<p class="gp_error">Error: Data directory does not exist: ' . htmlspecialchars($check_dir) . '</p>';
$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 '<p class="gp_passed">';
echo sprintf($langmessage['data_check_passed'], $checked, $checked);
echo '</p>';
$this->ShowDeletable();
return;
}
echo '<p class="gp_notice">';
echo sprintf($langmessage['data_check_failed'], $this->failed_count, $checked);
echo '</p>';
// the /data directory isn't writable
if (count($this->failed) == 1 && in_array($check_dir, $this->failed)) {
echo '<p class="gp_notice">';
echo '<b>WARNING:</b> Your data directory at is no longer writable: ' . htmlspecialchars($check_dir, ENT_QUOTES, 'UTF-8'); // escape output
echo '</p>';
return;
}
if ($this->failed_count > $this->show_failed_max) {
echo '<p class="gp_notice">';
echo sprintf($langmessage['showing_max_failed'], $this->show_failed_max);
echo '</p>';
}
echo '<table class="bordered">';
echo '<tr><th>';
echo $langmessage['file_name'];
echo '</th><th>';
echo $langmessage['File Owner'];
echo '<br/>';
echo $langmessage['Current_Value'];
echo '</th><th>';
echo '<br/>';
echo $langmessage['Expected_Value'];
echo '</th><th> ';
echo '</th></tr>';
// 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 '<tr><td>';
echo htmlspecialchars($readable_path, ENT_QUOTES, 'UTF-8'); // escape output
echo '</td><td>';
echo $this->ShowUser($euid);
echo '</td><td>';
echo $this->ShowUser($this->euid);
echo '</td><td>';
// Use the sanitized path
echo \gp\tool::Link('Admin/Status', 'Fix', 'cmd=FixOwner&path=' . rawurlencode($safe_readable_path), 'data-cmd="cnreq"');
echo '</td></tr>';
}
echo '</table>';
$this->CheckPageFiles();
$this->ShowDeletable();
}
/**
* Show Deletable Files
*/
protected function ShowDeletable() {
if (empty($this->deletable)) {
if ($this->failed_count === 0) {
echo '<p class="gp_passed">';
echo sprintf(
htmlspecialchars($langmessage['data_check_passed'] ?? 'Passed %d checks out of %d.'),
$checked,
$checked
);
echo '</p>';
$this->ShowDeletable();
return;
}
echo '<p class="gp_notice">';
echo sprintf(
htmlspecialchars($langmessage['data_check_failed'] ?? 'Failed %d checks out of %d.'),
$this->failed_count,
$checked
);
echo '</p>';
// Special message if only the main /data directory isn't writable/accessible
if (count($this->failed) === 1 && $this->failed[0] === $check_dir) {
echo '<p class="gp_notice">';
echo '<b>WARNING:</b> Your data directory is not accessible or writable: ' . htmlspecialchars($check_dir);
echo '</p>';
// Don't show the table if only the root data dir failed
return;
}
if ($this->failed_count > $this->show_failed_max) {
echo '<p class="gp_notice">';
echo sprintf(
htmlspecialchars($langmessage['showing_max_failed'] ?? 'Showing first %d failed items.'),
$this->show_failed_max
);
echo '</p>';
}
echo '<table class="bordered">';
echo '<thead><tr><th>';
echo htmlspecialchars($langmessage['file_name'] ?? 'File/Folder');
echo '</th><th>';
echo htmlspecialchars($langmessage['File Owner'] ?? 'File Owner');
echo '<br/>';
echo htmlspecialchars($langmessage['Current_Value'] ?? 'Current');
echo '</th><th>';
echo '<br/>';
echo htmlspecialchars($langmessage['Expected_Value'] ?? 'Expected');
echo '</th><th> &nbsp;'; // Actions column
echo '</th></tr></thead>';
echo '<tbody>';
// 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 '<tr><td>';
echo htmlspecialchars($readable_path);
echo '</td><td>';
echo htmlspecialchars($this->ShowUser($file_euid));
echo '</td><td>';
echo htmlspecialchars($this->ShowUser($this->euid));
echo '</td><td>';
// 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 '&nbsp;'; // No fix action available
}
echo '</td></tr>';
$shown_count++;
}
echo '</tbody></table>';
// 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 '<h3>Deletable Files</h3>';
echo '<p>The following files or folders were marked as deletable during the scan:</p>';
echo '<ol>';
foreach ($this->deletable as $file) {
echo '<li>' . htmlspecialchars($file, ENT_QUOTES, 'UTF-8') . '</li>'; // escape output
echo '<li>' . htmlspecialchars($file) . '</li>';
}
echo '</ol>';
}
/**
* 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 '<p class="gp_notice">Cannot check for orphaned files: Files helper is unavailable.</p>';
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 '<p class="gp_notice">Cannot check for orphaned files: PageFile helper is unavailable.</p>';
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 '<h2>Orphaned Data Files</h2>';
echo '<p>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.</p>';
echo '<table class="bordered"><tr><th>File</th></tr>';
echo '<p>The following data files exist in the <code>_pages</code> 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.</p>';
echo '<table class="bordered"><thead><tr><th>File Path</th></tr></thead>';
echo '<tbody>';
foreach ($diff as $file) {
echo '<tr><td>'
. htmlspecialchars($file, ENT_QUOTES, 'UTF-8') // escape output
. '</td></tr>';
echo '<tr><td>' . htmlspecialchars($file) . '</td></tr>';
}
echo '</table>';
echo '</tbody></table>';
}
/**
* 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 '<h4>Attempting to fix: ' . htmlspecialchars($to_fix) . '</h4>';
echo '<ol>';
// 1. Copy original to new temporary location (should inherit correct owner/perms)
echo '<li>Copy: ' . htmlspecialchars($to_fix) . ' &rarr; ' . htmlspecialchars($new_file_rel) . '</li>';
if (!\gp\admin\Tools\Port::CopyAll($to_fix_full, $new_file_full)) {
echo '<li style="color: red;">Failed: Could not copy.</li>';
echo '</ol>';
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 '<li>Move (Original): ' . htmlspecialchars($to_fix) . ' &rarr; ' . htmlspecialchars($deletable_rel) . '</li>';
if (!@rename($to_fix_full, $deletable_full)) {
echo '<li style="color: red;">Failed: Could not move original to deletable location.</li>';
echo '</ol>';
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 '<li>Move (New Copy): ' . htmlspecialchars($new_file_rel) . ' &rarr; ' . htmlspecialchars($to_fix) . '</li>';
if (!@rename($new_file_full, $to_fix_full)) {
echo '<li style="color: red;">Failed: Could not move new copy into original place.</li>';
echo '</ol>';
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 '<li style="color: green;">Success: File/folder replaced.</li>';
// 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 '<li>Note: Deletable temporary file/folder ' . htmlspecialchars($deletable_rel) . ' could not be removed. It might require manual deletion.</li>';
} else {
echo '<li>Cleanup: Removed temporary original from ' . htmlspecialchars($deletable_rel) . '.</li>';
}
} else {
echo '<li>Note: Could not attempt cleanup of ' . htmlspecialchars($deletable_rel) . ' (RmAll helper missing).</li>';
}
echo '</ol>';
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 '<ol>';
echo '<li>Copy: ' . htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8') . ' -> ' . htmlspecialchars($new_file, ENT_QUOTES, 'UTF-8') . '</li>';
if (!\gp\admin\Tools\Port::CopyAll($to_fix_full, $new_file_full)) {
echo '<li>Failed</li>';
echo '</ol>';
msg($langmessage['OOPS'] . ' Not Copied');
\gp\tool\Files::RmAll($new_file_full);
return;
}
// move old to deletable
echo '<li>Move: ' . htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8') . ' -> ' . htmlspecialchars($deletable, ENT_QUOTES, 'UTF-8') . '</li>';
if (!rename($to_fix_full, $deletable_full)) {
echo '<li>Failed</li>';
echo '</ol>';
msg($langmessage['OOPS'] . ' Rename to deletable failed');
\gp\tool\Files::RmAll($new_file_full);
return;
}
// move
echo '<li>Move: ' . htmlspecialchars($new_file, ENT_QUOTES, 'UTF-8') . ' -> ' . htmlspecialchars($to_fix, ENT_QUOTES, 'UTF-8') . '</li>';
if (!rename($new_file_full, $to_fix_full)) {
echo '<li>Failed</li>';
echo '</ol>';
msg($langmessage['OOPS'] . ' Rename to old failed');
return;
}
echo '<li>Success</li>';
// attempt to remove deletable
if (!\gp\tool\Files::RmAll($deletable_full)) {
echo '<li>Note: ' . htmlspecialchars($deletable, ENT_QUOTES, 'UTF-8') . ' was not deleted</li>';
}
echo '</ol>';
}
/**
* 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 '<div class="gp_' . ($isSuccess ? 'passed' : 'error') . '">' . htmlspecialchars($message) . '</div>';
}
}
// 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;
}
}
// 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.