Bearsampp 2026.7.11
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 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
75 public static function setSkipSymlinkCreation(bool $skip): void
76 {
77 self::$skipSymlinkCreation = $skip;
78 }
79
85 public static function isSkippingSymlinkCreation(): bool
86 {
87 return self::$skipSymlinkCreation;
88 }
89
96 public static function createModuleSymlink($module)
97 {
98 $src = Path::formatWindowsPath($module->currentPath);
99 $dest = Path::formatWindowsPath($module->symlinkPath);
100
101 if (is_link($dest)) {
102 if (readlink($dest) === $src) {
103 return;
104 }
106 } elseif (file_exists($dest)) {
107 if (is_dir($dest)) {
108 Log::error('Cannot create symlink: a real directory exists at the destination: ' . $dest);
109 return;
110 }
111 Log::warning('Removing file at symlink location: ' . $dest);
112 if (!@unlink($dest)) {
113 Log::error('Failed to remove file at symlink location: ' . $dest);
114 return;
115 }
116 }
117
118 Batch::createSymlink($src, $dest);
119 }
120
128 private static function isPathWithinAllowedBase($path)
129 {
130 global $bearsamppRoot;
131
132 // Normalize paths for comparison
133 $normalizedPath = realpath($path);
134 if ($normalizedPath === false) {
135 Log::error('Failed to resolve path: ' . $path);
136 return false;
137 }
138
139 $allowedBases = [
140 realpath(Path::getAppsPath()),
141 realpath(Path::getBinPath()),
142 realpath(Path::getToolsPath())
143 ];
144
145 foreach ($allowedBases as $base) {
146 if ($base === false) {
147 continue;
148 }
149
150 // Ensure path starts with allowed base (with directory separator to prevent substring matches)
151 if (strpos($normalizedPath, $base . DIRECTORY_SEPARATOR) === 0 ||
152 $normalizedPath === $base) {
153 return true;
154 }
155 }
156
157 Log::error('Path is outside allowed symlink directories: ' . $path);
158 return false;
159 }
160
168 private static function isSymlink($path)
169 {
170 return is_link($path);
171 }
172
180 public static function safeRemoveSymlink($path)
181 {
182 // Validate path is within allowed directories
183 if (!self::isPathWithinAllowedBase($path)) {
184 Log::error('Symlink removal blocked - path not in allowed directories: ' . $path);
185 return false;
186 }
187
188 // Check if path exists
189 if (!file_exists($path) && !is_link($path)) {
190 Log::trace('Symlink or directory already deleted or missing: ' . $path);
191 return false;
192 }
193
194 // If it's a directory (including junctions/directory-symlinks), use rmdir
195 if (is_dir($path)) {
196 // Attempt to remove as a symlink/junction first (non-recursive)
197 if (@rmdir($path)) {
198 Log::debug('Safely removed directory symlink/junction: ' . $path);
199 return true;
200 }
201
202 // If it failed and it's NOT a link, it's a real directory with content.
203 // We MUST NOT recursively delete it to avoid data loss.
204 if (!is_link($path)) {
205 Log::error('Symlink removal blocked - path is a real directory with content: ' . $path);
206 return false;
207 } else {
208 // If it's a link but rmdir failed, try unlink (e.g. file symlink)
209 if (@unlink($path)) {
210 Log::debug('Safely removed symlink via unlink: ' . $path);
211 return true;
212 }
213 }
214 }
215
216 // If it's a symlink but not a directory (e.g. file symlink), or if rmdir failed
217 if (is_link($path)) {
218 if (@unlink($path) || @rmdir($path)) {
219 Log::debug('Safely removed symlink: ' . $path);
220 return true;
221 }
222 }
223
224 if (is_link($path)) {
225 Log::error('Failed to remove symlink: ' . $path);
226 return false;
227 }
228
229 // Regular files should not be deleted here
230 Log::warning('Path is a regular file, not a symlink - refusing deletion: ' . $path);
231 return false;
232 }
233
248 public static function deleteCurrentSymlinks()
249 {
251
252 // Check to see if purging is necessary
253 $appsPath = Path::getAppsPath();
254 $binPath = Path::getBinPath();
255 $toolsPath = Path::getToolsPath();
256
257 $array = [
258 self::APACHE_SYMLINK => $binPath . '/apache/current',
259 self::BRUNO_SYMLINK => $toolsPath . '/bruno/current',
260 self::COMPOSER_SYMLINK => $toolsPath . '/composer/current',
261 self::GHOSTSCRIPT_SYMLINK => $toolsPath . '/ghostscript/current',
262 self::GIT_SYMLINK => $toolsPath . '/git/current',
263 self::MAILPIT_SYMLINK => $binPath . '/mailpit/current',
264 self::MARIADB_SYMLINK => $binPath . '/mariadb/current',
265 self::MEMCACHED_SYMLINK => $binPath . '/memcached/current',
266 self::MYSQL_SYMLINK => $binPath . '/mysql/current',
267 self::NGROK_SYMLINK => $toolsPath . '/ngrok/current',
268 self::NODEJS_SYMLINK => $binPath . '/nodejs/current',
269 self::PERL_SYMLINK => $toolsPath . '/perl/current',
270 self::PHP_SYMLINK => $binPath . '/php/current',
271 self::PHPMYADMIN_SYMLINK => $appsPath . '/phpmyadmin/current',
272 self::PHPPGADMIN_SYMLINK => $appsPath . '/phppgadmin/current',
273 self::POSTGRESQL_SYMLINK => $binPath . '/postgresql/current',
274 self::POWERSHELL_SYMLINK => $toolsPath . '/powershell/current',
275 self::PYTHON_SYMLINK => $toolsPath . '/python/current',
276 self::RUBY_SYMLINK => $toolsPath . '/ruby/current',
277 self::XLIGHT_SYMLINK => $binPath . '/xlight/current',
278 ];
279
280 // Fix for PHP 8.2: Add null checks before accessing array elements
281 if (!is_array($array) || empty($array)) {
282 Log::error('Current symlinks array is not initialized or empty.');
283 return;
284 }
285
286 // Purge "current" symlinks with safety checks
287 foreach ($array as $name => $path) {
288 // Skip if path is null
289 if (empty($path)) {
290 continue;
291 }
292
293 if (!file_exists($path) && !is_link($path)) {
294 // Log that the symlink was already missing
295 Log::trace('Symlink already deleted or missing: ' . $path);
296 continue;
297 }
298
299 // Use safe removal method with path validation and non-recursive guarantees
301 }
302 }
303}
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)