Bearsampp 2026.9.23
Loading...
Searching...
No Matches
class.symlinks.php
Go to the documentation of this file.
1<?php
2/*
3 *
4 * * Copyright (c) 2021-2026 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 APACHE_SYMLINK = 'apache';
17 const BRUNO_SYMLINK = 'bruno';
18 const COMPOSER_SYMLINK = 'composer';
19 const GHOSTSCRIPT_SYMLINK = 'ghostscript';
20 const GIT_SYMLINK = 'git';
21 const MAILPIT_SYMLINK = 'mailpit';
22 const MARIADB_SYMLINK = 'mariadb';
23 const MEMCACHED_SYMLINK = 'memcached';
24 const MYSQL_SYMLINK = 'mysql';
25 const NGROK_SYMLINK = 'ngrok';
26 const NODEJS_SYMLINK = 'nodejs';
27 const PERL_SYMLINK = 'perl';
28 const PHP_SYMLINK = 'php';
29 const PHPMYADMIN_SYMLINK = 'phpmyadmin';
30 const PHPPGADMIN_SYMLINK = 'phppgadmin';
31 const POSTGRESQL_SYMLINK = 'postgresql';
32 const POWERSHELL_SYMLINK = 'powershell';
33 const PYTHON_SYMLINK = 'python';
34 const RUBY_SYMLINK = 'ruby';
35 const XLIGHT_SYMLINK = 'xlight';
36
40 private static $skipSymlinkCreation = false;
41
45 private static $root;
46
52 public function __construct($root)
53 {
54 self::$root = $root;
56 }
57
62 public static function initializePaths()
63 {
64 // Path initialization logic can be added here if needed
65 // For now, it's a placeholder as requested by the issue to restore it.
66 }
67
76 public static function setSkipSymlinkCreation(bool $skip): void
77 {
78 self::$skipSymlinkCreation = $skip;
79 }
80
86 public static function isSkippingSymlinkCreation(): bool
87 {
88 return self::$skipSymlinkCreation;
89 }
90
97 public static function createModuleSymlink($module)
98 {
99 $src = Path::formatWindowsPath($module->currentPath);
100 $dest = Path::formatWindowsPath($module->symlinkPath);
101
102 if (is_link($dest)) {
103 if (readlink($dest) === $src) {
104 return;
105 }
107 } elseif (file_exists($dest)) {
108 if (is_dir($dest)) {
109 Log::error('Cannot create symlink: a real directory exists at the destination: ' . $dest);
110
111 return;
112 }
113 Log::warning('Removing file at symlink location: ' . $dest);
114 if (!@unlink($dest)) {
115 Log::error('Failed to remove file at symlink location: ' . $dest);
116
117 return;
118 }
119 }
120
121 Batch::createSymlink($src, $dest);
122 }
123
132 private static function isPathWithinAllowedBase($path)
133 {
134 global $bearsamppRoot;
135
136 // Normalize paths for comparison
137 $normalizedPath = realpath($path);
138 if ($normalizedPath === false) {
139 Log::error('Failed to resolve path: ' . $path);
140
141 return false;
142 }
143
144 $allowedBases = [
145 realpath(Path::getAppsPath()),
146 realpath(Path::getBinPath()),
147 realpath(Path::getToolsPath())
148 ];
149
150 foreach ($allowedBases as $base) {
151 if ($base === false) {
152 continue;
153 }
154
155 // Ensure path starts with allowed base (with directory separator to prevent substring matches)
156 if (strpos($normalizedPath, $base . DIRECTORY_SEPARATOR) === 0 ||
157 $normalizedPath === $base) {
158 return true;
159 }
160 }
161
162 Log::error('Path is outside allowed symlink directories: ' . $path);
163
164 return false;
165 }
166
175 private static function isSymlink($path)
176 {
177 return is_link($path);
178 }
179
188 public static function safeRemoveSymlink($path)
189 {
190 // Validate path is within allowed directories
191 if (!self::isPathWithinAllowedBase($path)) {
192 Log::error('Symlink removal blocked - path not in allowed directories: ' . $path);
193
194 return false;
195 }
196
197 // Check if path exists
198 if (!file_exists($path) && !self::isSymlink($path)) {
199 Log::trace('Symlink or directory already deleted or missing: ' . $path);
200
201 return false;
202 }
203
204 // If it's a directory (including junctions/directory-symlinks), use rmdir
205 if (is_dir($path)) {
206 // Attempt to remove as a symlink/junction first (non-recursive)
207 if (@rmdir($path)) {
208 Log::debug('Safely removed directory symlink/junction: ' . $path);
209
210 return true;
211 }
212
213 // If it failed and it's NOT a link, it's a real directory with content.
214 // We MUST NOT recursively delete it to avoid data loss.
215 if (!self::isSymlink($path)) {
216 Log::error('Symlink removal blocked - path is a real directory with content: ' . $path);
217
218 return false;
219 } else {
220 // If it's a link but rmdir failed, try unlink (e.g. file symlink)
221 if (@unlink($path)) {
222 Log::debug('Safely removed symlink via unlink: ' . $path);
223
224 return true;
225 }
226 }
227 }
228
229 // If it's a symlink but not a directory (e.g. file symlink), or if rmdir failed
230 if (self::isSymlink($path)) {
231 if (@unlink($path) || @rmdir($path)) {
232 Log::debug('Safely removed symlink: ' . $path);
233
234 return true;
235 }
236 }
237
238 if (self::isSymlink($path)) {
239 Log::error('Failed to remove symlink: ' . $path);
240
241 return false;
242 }
243
244 // Regular files should not be deleted here
245 Log::warning('Path is a regular file, not a symlink - refusing deletion: ' . $path);
246
247 return false;
248 }
249
264 public static function deleteCurrentSymlinks()
265 {
267
268 // Check to see if purging is necessary
269 $appsPath = Path::getAppsPath();
270 $binPath = Path::getBinPath();
271 $toolsPath = Path::getToolsPath();
272
273 $array = [
274 self::APACHE_SYMLINK => $binPath . '/apache/current',
275 self::BRUNO_SYMLINK => $toolsPath . '/bruno/current',
276 self::COMPOSER_SYMLINK => $toolsPath . '/composer/current',
277 self::GHOSTSCRIPT_SYMLINK => $toolsPath . '/ghostscript/current',
278 self::GIT_SYMLINK => $toolsPath . '/git/current',
279 self::MAILPIT_SYMLINK => $binPath . '/mailpit/current',
280 self::MARIADB_SYMLINK => $binPath . '/mariadb/current',
281 self::MEMCACHED_SYMLINK => $binPath . '/memcached/current',
282 self::MYSQL_SYMLINK => $binPath . '/mysql/current',
283 self::NGROK_SYMLINK => $toolsPath . '/ngrok/current',
284 self::NODEJS_SYMLINK => $binPath . '/nodejs/current',
285 self::PERL_SYMLINK => $toolsPath . '/perl/current',
286 self::PHP_SYMLINK => $binPath . '/php/current',
287 self::PHPMYADMIN_SYMLINK => $appsPath . '/phpmyadmin/current',
288 self::PHPPGADMIN_SYMLINK => $appsPath . '/phppgadmin/current',
289 self::POSTGRESQL_SYMLINK => $binPath . '/postgresql/current',
290 self::POWERSHELL_SYMLINK => $toolsPath . '/powershell/current',
291 self::PYTHON_SYMLINK => $toolsPath . '/python/current',
292 self::RUBY_SYMLINK => $toolsPath . '/ruby/current',
293 self::XLIGHT_SYMLINK => $binPath . '/xlight/current',
294 ];
295
296 // Fix for PHP 8.2: Add null checks before accessing array elements
297 if (!is_array($array) || empty($array)) {
298 Log::error('Current symlinks array is not initialized or empty.');
299
300 return;
301 }
302
303 // Purge "current" symlinks with safety checks
304 foreach ($array as $name => $path) {
305 // Skip if path is null
306 if (empty($path)) {
307 continue;
308 }
309
310 if (!file_exists($path) && !is_link($path)) {
311 // Log that the symlink was already missing
312 Log::trace('Symlink already deleted or missing: ' . $path);
313 continue;
314 }
315
316 // Use safe removal method with path validation and non-recursive guarantees
318 }
319 }
320}
global $bearsamppRoot
global $bearsamppCore
static removeSymlink($link)
static createSymlink($src, $dest)
static debug($data, $file=null)
static warning($data, $file=null)
static trace($data, $file=null)
static error($data, $file=null)
static getToolsPath($aetrayPath=false)
static formatWindowsPath($path)
static getBinPath($aetrayPath=false)
static getAppsPath($aetrayPath=false)