mirror of
https://github.com/gtbu/Typesetter-5.3-p8.git
synced 2025-04-18 20:23:14 +02:00
Update Status.php
This commit is contained in:
parent
70e9ecefbc
commit
8ce74665a1
1 changed files with 434 additions and 276 deletions
|
@ -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> '; // 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 ' '; // 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) . ' → ' . 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) . ' → ' . 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) . ' → ' . 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.
|
Loading…
Reference in a new issue