Bearsampp 2026.5.5
Loading...
Searching...
No Matches
class.symlinks.php
Go to the documentation of this file.
1<?php
2/*
3 *
4 * * Copyright (c) 2021-2024 Bearsampp
5 * * License: GNU General Public License version 3 or later; see LICENSE.txt
6 * * Website: https://bearsampp.com
7 * * Github: https://github.com/Bearsampp
8 *
9 */
10
15{
16 const PHPMYADMIN_SYMLINK = 'phpmyadmin';
17 const PHPPGADMIN_SYMLINK = 'phppgadmin';
18 const APACHE_SYMLINK = 'apache';
19 const MARIADB_SYMLINK = 'mariadb';
20 const MEMCACHED_SYMLINK = 'memcached';
21 const MYSQL_SYMLINK = 'mysql';
22 const NODEJS_SYMLINK = 'nodejs';
23 const PHP_SYMLINK = 'php';
24 const POSTGRESQL_SYMLINK = 'postgresql';
25 const COMPOSER_SYMLINK = 'composer';
26 const POWERSHELL_SYMLINK = 'powershell';
27 const GHOSTSCRIPT_SYMLINK = 'ghostscript';
28 const GIT_SYMLINK = 'git';
29 const NGROK_SYMLINK = 'ngrok';
30 const PERL_SYMLINK = 'perl';
31 const PYTHON_SYMLINK = 'python';
32 const RUBY_SYMLINK = 'ruby';
33 const XLIGHT_SYMLINK = 'xlight';
34 const MAILPIT_SYMLINK = 'mailpit';
35 const BRUNO_SYMLINK = 'bruno';
36
40 private $root;
41
47 public function __construct($root)
48 {
49 $this->root = $root;
50 $this->initializePaths();
51 }
52
60 private static function isPathWithinAllowedBase($path)
61 {
62 global $bearsamppRoot;
63
64 // Normalize paths for comparison
65 $normalizedPath = realpath($path);
66 if ($normalizedPath === false) {
67 Log::error('Failed to resolve path: ' . $path);
68 return false;
69 }
70
71 $allowedBases = [
72 realpath($bearsamppRoot->getAppsPath()),
73 realpath($bearsamppRoot->getBinPath()),
74 realpath($bearsamppRoot->getToolsPath())
75 ];
76
77 foreach ($allowedBases as $base) {
78 if ($base === false) {
79 continue;
80 }
81
82 // Ensure path starts with allowed base (with directory separator to prevent substring matches)
83 if (strpos($normalizedPath, $base . DIRECTORY_SEPARATOR) === 0 ||
84 $normalizedPath === $base) {
85 return true;
86 }
87 }
88
89 Log::error('Path is outside allowed symlink directories: ' . $path);
90 return false;
91 }
92
100 private static function isSymlink($path)
101 {
102 // Use is_link to check without following symlinks
103 return is_link($path);
104 }
105
121 private static function safeRemoveSymlink($path)
122 {
123 // Validate path is within allowed directories
124 if (!self::isPathWithinAllowedBase($path)) {
125 Log::error('Symlink removal blocked - path not in allowed directories: ' . $path);
126 return false;
127 }
128
129 // Check if path exists
130 if (!file_exists($path) && !is_link($path)) {
131 Log::debug('Symlink does not exist: ' . $path);
132 return false;
133 }
134
135 // If it's a symlink, remove it appropriately.
136 // On Windows, directory junctions require rmdir(), not unlink(). For broken junctions,
137 // is_dir() returns false even though the link exists, so we try both methods.
138 if (self::isSymlink($path)) {
139 $removed = @unlink($path) || @rmdir($path);
140 if ($removed) {
141 Log::debug('Safely removed symlink: ' . $path);
142 return true;
143 } else {
144 Log::error('Failed to remove symlink: ' . $path);
145 return false;
146 }
147 }
148
149 // If it's a directory, only remove if empty (rmdir fails on non-empty)
150 if (is_dir($path)) {
151 // Double-check: ensure we're not attempting recursive deletion
152 $items = @scandir($path);
153 if ($items === false) {
154 Log::error('Cannot read directory contents: ' . $path);
155 return false;
156 }
157
158 // Count real items (exclude . and ..)
159 $realItems = array_diff($items, ['.', '..']);
160
161 if (!empty($realItems)) {
162 Log::warning('Directory is not empty - refusing to delete: ' . $path .
163 ' (contains ' . count($realItems) . ' items)');
164 return false;
165 }
166
167 // Directory is empty, safe to remove
168 if (@rmdir($path)) {
169 Log::debug('Safely removed empty directory: ' . $path);
170 return true;
171 } else {
172 Log::error('Failed to remove empty directory: ' . $path);
173 return false;
174 }
175 }
176
177 // Regular files should not be deleted here
178 Log::warning('Path is a regular file, not a symlink - refusing deletion: ' . $path);
179 return false;
180 }
181
196 public static function deleteCurrentSymlinks()
197 {
199
200 // Check to see if purging is necessary
201 $appsPath = $bearsamppRoot->getAppsPath();
202 $binPath = $bearsamppRoot->getBinPath();
203 $toolsPath = $bearsamppRoot->getToolsPath();
204
205 $array = [
206 self::PHPMYADMIN_SYMLINK => $appsPath . '/phpmyadmin/current',
207 self::PHPPGADMIN_SYMLINK => $appsPath . '/phppgadmin/current',
208 self::APACHE_SYMLINK => $binPath . '/apache/current',
209 self::MARIADB_SYMLINK => $binPath . '/mariadb/current',
210 self::MEMCACHED_SYMLINK => $binPath . '/memcached/current',
211 self::MYSQL_SYMLINK => $binPath . '/mysql/current',
212 self::NODEJS_SYMLINK => $binPath . '/nodejs/current',
213 self::PHP_SYMLINK => $binPath . '/php/current',
214 self::POSTGRESQL_SYMLINK => $binPath . '/postgresql/current',
215 self::COMPOSER_SYMLINK => $toolsPath . '/composer/current',
216 self::POWERSHELL_SYMLINK => $toolsPath . '/powershell/current',
217 self::GHOSTSCRIPT_SYMLINK => $toolsPath . '/ghostscript/current',
218 self::GIT_SYMLINK => $toolsPath . '/git/current',
219 self::NGROK_SYMLINK => $toolsPath . '/ngrok/current',
220 self::PERL_SYMLINK => $toolsPath . '/perl/current',
221 self::PYTHON_SYMLINK => $toolsPath . '/python/current',
222 self::RUBY_SYMLINK => $toolsPath . '/ruby/current',
223 self::XLIGHT_SYMLINK => $binPath . '/xlight/current',
224 self::MAILPIT_SYMLINK => $binPath . '/mailpit/current',
225 self::BRUNO_SYMLINK => $toolsPath . '/bruno/current'
226 ];
227
228 // Fix for PHP 8.2: Add null checks before accessing array elements
229 if (!is_array($array) || empty($array)) {
230 Log::error('Current symlinks array is not initialized or empty.');
231 return;
232 }
233
234 // Purge "current" symlinks with safety checks
235 foreach ($array as $name => $path) {
236 // Skip if path is null
237 if (empty($path)) {
238 continue;
239 }
240
241 if (!file_exists($path) && !is_link($path)) {
242 // Skip if the symlink doesn't exist - no need to log an error
243 continue;
244 }
245
246 // Use safe removal method with path validation and non-recursive guarantees
248 }
249 }
250}
global $bearsamppRoot
global $bearsamppCore
static debug($data, $file=null)
static warning($data, $file=null)
static error($data, $file=null)