30 'Apache' => [
'type' =>
'binary'],
31 'Bruno' => [
'type' =>
'tools'],
32 'Composer' => [
'type' =>
'tools'],
33 'Ghostscript' => [
'type' =>
'tools'],
34 'Git' => [
'type' =>
'tools'],
35 'Mailpit' => [
'type' =>
'binary'],
36 'MariaDB' => [
'type' =>
'binary'],
37 'Memcached' => [
'type' =>
'binary'],
38 'MySQL' => [
'type' =>
'binary'],
39 'Ngrok' => [
'type' =>
'tools'],
40 'NodeJS' => [
'type' =>
'binary'],
41 'Perl' => [
'type' =>
'tools'],
42 'PHP' => [
'type' =>
'binary'],
43 'PhpMyAdmin' => [
'type' =>
'application'],
44 'PhpPgAdmin' => [
'type' =>
'application'],
45 'PostgreSQL' => [
'type' =>
'binary'],
46 'PowerShell' => [
'type' =>
'tools'],
47 'Python' => [
'type' =>
'tools'],
48 'Ruby' => [
'type' =>
'tools'],
49 'Xlight' => [
'type' =>
'binary']
95 if ($isPrerelease && $includePr == 1) {
96 return '<span class="text-danger">' . htmlspecialchars($version) .
' PR</span>';
99 return htmlspecialchars($version);
113 $moduleName = str_replace(
'module-',
'', $moduleName);
117 foreach ($this->modules as $key => $moduleInfo) {
118 if (strtolower($key) === strtolower($moduleName)) {
133 return array_keys($this->modules);
151 if (!$validation[
'valid']) {
193 $headers = array(
'HTTP/1.1 ' .
$result[
'status']);
194 foreach (
$result[
'headers'] as $name => $value) {
195 $headers[$name] = $value;
215 $remoteFileCreationTime = strtotime($remoteModTime);
216 if ($remoteFileCreationTime > $localFileCreationTime) {
231 if (!file_exists($this->jsonFilePath)) {
238 return filectime($this->jsonFilePath);
257 if ($headers ===
false ||
280 if (!is_array($headers)) {
283 foreach ($headers as $key => $value) {
284 if (is_string($key) && strcasecmp($key, $name) === 0) {
285 if (is_array($value)) {
286 $value = reset($value);
289 return is_string($value) ? $value :
null;
306 Log::debug(
'Headers: ' . print_r($headers,
true));
317 $content = @file_get_contents($this->jsonFilePath);
318 if ($content ===
false) {
319 Log::error(
'Error fetching content from JSON file: ' . $this->jsonFilePath);
321 return [
'error' =>
'Error fetching JSON file'];
324 $data = json_decode($content,
true);
325 if (json_last_error() !== JSON_ERROR_NONE) {
326 Log::error(
'Error decoding JSON content: ' . json_last_error_msg());
328 return [
'error' =>
'Error decoding JSON content'];
342 Log::debug(
'Fetching JSON file: ' . $this->jsonFilePath);
347 if ($jsonContent ===
'') {
349 throw new Exception(
'Failed to fetch JSON content from the URL.');
353 $result = file_put_contents($this->jsonFilePath, $jsonContent);
357 throw new Exception(
'Failed to save JSON content to the specified path.');
361 return [
'success' =>
'JSON content fetched and saved successfully'];
380 foreach ($jsonData as $entry) {
381 if (is_array($entry)) {
382 if (isset($entry[
'module']) && is_string($entry[
'module'])) {
383 if (isset($entry[
'versions']) && is_array($entry[
'versions'])) {
384 $moduleVersions = array_column($entry[
'versions'],
null,
'version');
385 uasort($moduleVersions,
function ($a, $b) {
386 return version_compare($b[
'version'], $a[
'version']);
388 $versions[$entry[
'module']] = $moduleVersions;
392 Log::error(
'Invalid entry format in JSON data');
399 return [
'error' =>
'No versions found'];
423 Log::debug(
'getModuleUrl called for module: ' . $module .
' version: ' . $version);
424 $moduleKey =
'module-' . strtolower($module);
425 if (!isset($this->versions[$moduleKey][$version][
'url'])) {
428 return [
'error' =>
'Version not found'];
430 $url = trim($this->versions[$moduleKey][$version][
'url']);
432 Log::debug(
'Found URL for version: ' . $version .
' URL: ' . $url);
438 return [
'error' =>
'Version not found'];
465 Log::error(
'Global configuration is not set.');
473 if (empty($DownloadId)) {
481 Log::debug(
'Validating download ID via QuickPick API.');
490 Log::error(
'Failed to validate QuickPick license - API server unavailable');
500 if (json_last_error() !== JSON_ERROR_NONE) {
501 Log::error(
'Error decoding JSON response: ' . json_last_error_msg());
507 if (isset($data[
'success']) && $data[
'success'] ===
true && isset($data[
'data']) && is_array($data[
'data']) && count($data[
'data']) > 0) {
508 Log::debug(
'License key valid: ' . $DownloadId);
513 Log::error(
'Invalid license key: ' . $DownloadId);
537 if (is_array($moduleUrl) && isset($moduleUrl[
'error'])) {
538 Log::error(
'Module URL not found for module: ' . $module .
' version: ' . $version);
540 return [
'error' =>
'Module URL not found'];
543 if (empty($moduleUrl)) {
544 Log::error(
'Module URL not found for module: ' . $module .
' version: ' . $version);
546 return [
'error' =>
'Module URL not found'];
558 Log::debug(
'Enhanced mode: ' . ($enhancedMode ?
'enabled' :
'disabled'));
561 if (isset(
$response[
'success']) && $enhancedMode == 1) {
563 Log::debug(
'Enhanced mode enabled - Updating config for module: ' . $module .
' version: ' . $version);
566 if ($configUpdated) {
573 Log::debug(
'Config updated successfully, launching reload to apply changes...');
576 if (ob_get_level() > 0) {
579 echo json_encode([
'phase' =>
'updating',
'message' =>
'Applying version changes...']) . PHP_EOL;
583 Log::debug(
'Clearing caches before reload...');
591 Log::debug(
'Launching reload command: ' . $reloadCmd);
596 Log::error(
'Config update failed for module: ' . $module);
600 if (isset(
$response[
'success']) && $enhancedMode == 0) {
601 Log::debug(
'Enhanced mode disabled - skipping config update');
604 Log::debug(
'Clearing caches after module installation (Standard mode)...');
611 Log::error(
'No internet connection available.');
613 return [
'error' =>
'No internet connection'];
631 Log::debug(
'Temporary Directory: ' . $tmpDir);
633 $fileName = basename($moduleUrl);
636 $tmpFilePath = $tmpDir .
'/' . $fileName;
641 if (!self::isAllowedArchive($fileName)) {
642 Log::error(
'Unsupported archive type rejected before download: ' . $fileName);
644 return [
'error' =>
'Unsupported archive type'];
647 $moduleName = str_replace(
'module-',
'', $module);
653 foreach ($this->modules as $key => $moduleInfo) {
654 if (strtolower($key) === strtolower($moduleName)) {
661 Log::error(
"Module not found in modules array: $moduleName");
663 return [
'error' =>
'Module configuration not found'];
666 $moduleType = $this->modules[$moduleKey][
'type'];
679 Log::error(
'Failed to retrieve file from URL: ' . $moduleUrl);
680 @unlink($tmpFilePath);
682 return [
'error' =>
'Failed to retrieve file from URL'];
688 if (!self::verifyModuleChecksum($moduleUrl, $tmpFilePath)) {
689 Log::error(
'SHA-256 checksum verification failed for module: ' . $module .
' (URL: ' . $moduleUrl .
')');
690 @unlink($tmpFilePath);
692 return [
'error' =>
'Checksum verification failed. Download aborted. File: ' . basename($moduleUrl)];
698 if (!self::isAllowedArchive($tmpFilePath)) {
699 Log::error(
'Unsupported archive extension after download: ' . $tmpFilePath);
700 @unlink($tmpFilePath);
702 return [
'error' =>
'Unsupported archive extension'];
705 $fileExtension = strtolower(pathinfo($tmpFilePath, PATHINFO_EXTENSION));
706 Log::debug(
'File extension: ' . $fileExtension);
708 if ($fileExtension ===
'7z' || $fileExtension ===
'zip') {
709 echo json_encode([
'phase' =>
'extracting']) . PHP_EOL;
710 if (ob_get_length()) {
715 $unzipResult =
$bearsamppCore->unzipFile($tmpFilePath, $destination,
function ($currentPercentage) {
716 $progressStr = is_numeric($currentPercentage) ?
"$currentPercentage%" : $currentPercentage;
717 echo json_encode([
'progress' => $progressStr]) . PHP_EOL;
718 if (ob_get_length()) {
724 if ($unzipResult ===
false) {
725 return [
'error' =>
'Failed to unzip file. File: ' . $tmpFilePath .
' could not be unzipped',
'Destination: ' . $destination];
728 Log::error(
'Unsupported file extension: ' . $fileExtension);
730 return [
'error' =>
'Unsupported file extension'];
733 return [
'success' =>
'Module installed successfully'];
750 if (!is_file($tmpFilePath)) {
751 Log::error(
'Checksum verify: downloaded file not found: ' . $tmpFilePath);
757 if ($expectedHash ===
null) {
758 Log::error(
'Checksum verify: could not retrieve SHA-256 sidecar for: ' . $moduleUrl);
763 $actualHash = @hash_file(
'sha256', $tmpFilePath);
764 if ($actualHash ===
false) {
765 Log::error(
'Checksum verify: failed to hash local file: ' . $tmpFilePath);
770 if (!hash_equals($expectedHash, strtolower($actualHash))) {
772 'Checksum verify: mismatch for ' . basename($moduleUrl) .
773 ' (expected ' . $expectedHash .
', got ' . $actualHash .
')'
779 Log::debug(
'Checksum verify: OK for ' . basename($moduleUrl));
797 $sidecarUrl = $moduleUrl .
'.sha256';
802 Log::trace(
'verifyModuleChecksum() fetching sidecar via GitHub proxy: ' . $sidecarUrl);
809 if ($content ===
false) {
810 Log::error(
'Checksum verify: sidecar fetch failed for: ' . $sidecarUrl);
815 if (preg_match(
'/\b([0-9a-f]{64})\b/i', $content, $match) !== 1) {
816 Log::error(
'Checksum verify: unexpected sidecar content for: ' . $sidecarUrl);
821 return strtolower($match[1]);
837 if ($fileName ===
'' || $fileName ===
false) {
841 $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
843 return in_array($extension, self::$allowedArchiveExtensions,
true);
861 if ($moduleType ===
'application') {
863 } elseif ($moduleType ===
'binary') {
865 } elseif ($moduleType ===
'tools') {
882 Log::debug(
'Regenerating menu (AJAX-safe mode)...');
885 $oldErrorReporting = error_reporting();
886 error_reporting($oldErrorReporting & ~E_WARNING);
893 error_reporting($oldErrorReporting);
898 }
catch (Exception $e) {
900 error_reporting($oldErrorReporting);
902 Log::warning(
'Error during menu regeneration: ' . $e->getMessage());
922 $moduleName = str_replace(
'module-',
'', $module);
927 foreach ($this->modules as $key => $moduleInfo) {
928 if (strtolower($key) === strtolower($moduleName)) {
935 Log::error(
"Module not found in modules array: $moduleName");
940 $moduleType = $this->modules[$moduleKey][
'type'];
945 if (preg_match(
'/^[0-9][0-9a-zA-Z.\-+]*$/', $version) !== 1) {
946 Log::error(
"Invalid version format for module: $module");
953 $configSection = strtolower($moduleKey);
955 Log::debug(
"Updating config for module: $module (key: $moduleKey, type: $moduleType) to version: $version");
960 $configKey = $configSection .
'Version';
963 Log::info(
"Successfully updated $configSection version to $version in bearsampp.conf");
966 }
catch (Exception $e) {
967 Log::error(
"Failed to update module config: " . $e->getMessage());
984 <div
id =
"configErrorContainer" class = "text-center mt-3 pe-3
">
985 <div class = "alert alert-danger d-inline-block
" role = "alert
" style = "max-width: 500px;">
986 <h4 class = "alert-heading">
987 <i class = "fas fa-exclamation-circle"></i> Configuration Error
992 echo htmlspecialchars($errorMessage); ?>
995 <small class = "text-muted">
996 Please add the missing parameter to the <code>bearsampp.conf</code> file in the
Bearsampp root directory.
1001 return ob_get_clean();
1018 public function getQuickpickMenu(array $modules, array $versions, string $imagesPath): string
1020 global $bearsamppConfig;
1021 $includePr = $bearsamppConfig->getIncludePr();
1025 if (HttpClient::checkInternetState()) {
1028 <div
class = "enhanced-mode-toggle
">
1029 <label class = "form-check-label me-2
" for = "enhancedQuickPickSwitch
">
1032 <div class = "form-check form-switch mb-0
">
1033 <input class = "form-check-input
" type = "checkbox
" role = "switch
" id = "enhancedQuickPickSwitch
"
1035 echo $enhancedMode == 1 ? 'checked' : ''; ?>
1036 data-bs-toggle = "tooltip
" data-bs-placement = "bottom
"
1037 title = "Toggle between enhanced (auto-config update) and standard
QuickPick mode
">
1040 <div id = 'quickPickContainer'>
1041 <div class = 'quickpick'>
1043 <div class = "custom-select
">
1044 <button class = "select-button
" role = "combobox
"
1045 aria-label = "select button
"
1046 aria-haspopup = "listbox
"
1047 aria-expanded = "false
"
1048 aria-controls = "select-dropdown
">
1049 <span class = "selected-value
">Select a module and version</span>
1050 <span class = "arrow
"></span>
1052 <ul class = "select-dropdown
" role = "listbox
" id = "select-dropdown
">
1055 foreach ($modules as $module): ?>
1057 if (is_string($module)): ?>
1058 <li role = "option
" class = "moduleheader
">
1060 echo htmlspecialchars($module); ?>
1064 foreach ($versions['module-' . strtolower($module)] as $version_array):
1065 // Skip prerelease versions if includePr is not enabled
1066 if (isset($version_array['prerelease']) && $version_array['prerelease'] === true && $includePr != 1) {
1070 <li role = "option
" class = "moduleoption
"
1072 echo htmlspecialchars($module); ?>-version-<?php
1073 echo htmlspecialchars($version_array[
'version']); ?>-li
"
1074 data-module = "<?php
1075 echo htmlspecialchars($module); ?>
"
1077 echo htmlspecialchars($version_array[
'version']); ?>
">
1078 <input type = "radio
"
1080 echo htmlspecialchars($module); ?>-version-<?php
1081 echo htmlspecialchars($version_array[
'version']); ?>
"
1082 name = "module
" data-module = "<?php
1083 echo htmlspecialchars($module); ?>
"
1085 echo htmlspecialchars($version_array[
'version']); ?>
">
1088 echo htmlspecialchars($module); ?>-version-<?php
1089 echo htmlspecialchars($version_array[
'version']); ?>
"><?php
1090 echo $this->formatVersionLabel(
1091 $version_array['version'],
1092 isset($version_array['prerelease']) && $version_array['prerelease'] === true
1104 <div class = "progress
" id = "progress
" tabindex = "-1
" style = "width:260px;display:none
"
1105 aria-labelledby = "progressbar
" aria-hidden = "true">
1106 <div class = "progress-bar progress-bar-striped progress-bar-animated
" id = "progress-bar
" role = "progressbar
" aria-valuenow = "0
" aria-valuemin = "0
"
1107 aria-valuemax = "100
" data-module = "Module"
1108 data-version = "0.0.0
">0%
1110 <div id = "download-module
" style = "display: none
">ModuleName</div>
1111 <div id = "download-version
" style = "display: none
">Version</div>
1116 <div id = "subscribeContainer
" class = "text-center
">
1120 echo
$imagesPath .
'subscribe.svg'; ?>
" alt = "Subscribe Icon
" class = "me-2
">
1121 Subscribe to QuickPick now
1128 <div id = "InternetState
" class = "text-center
">
1130 echo
$imagesPath .
'no-wifi-icon.svg'; ?>
" alt = "No Wifi Icon
" class = "me-2
">
1131 <span>No internet present</span>
1136 return ob_get_clean();
static background(string $command)
static proxyFetch($url, $method='GET', $verify=true)
static getApiJson($url, $verify=true)
static getSslStreamContext($verify=true, $url=null)
static getWebsiteUrl($path='', $fragment='', $utmSource=true)
static downloadFile(string $url, string $filePath, $progressBar=false, $verify=true)
static checkInternetState()
static isGithubHost($url)
static info($data, $file=null)
static debug($data, $file=null)
static warning($data, $file=null)
static trace($data, $file=null)
static error($data, $file=null)
static getCorePath($aetrayPath=false)
static getToolsPath($aetrayPath=false)
static getResourcesPath($aetrayPath=false)
static getPhpExe($aetrayPath=false)
static getBinPath($aetrayPath=false)
static getTmpPath($aetrayPath=false)
static getAppsPath($aetrayPath=false)
static verifyModuleChecksum(string $moduleUrl, string $tmpFilePath)
isValidHeaderResponse($headers)
fetchAndUnzipModule(string $moduleUrl, string $module)
getModuleUrl(string $module, string $version)
getLocalFileCreationTime()
normalizeModuleName(string $moduleName)
getErrorModal(string $errorMessage)
installModule(string $module, string $version)
static fetchChecksumFromSidecar(string $moduleUrl)
updateModuleConfig(string $module, string $version)
getModuleDestinationPath(string $moduleType, string $moduleName)
getHeaderValue($headers, string $name)
getQuickpickMenu(array $modules, array $versions, string $imagesPath)
formatVersionLabel($version, $isPrerelease=false)
logHeaders(array $headers)
loadQuickpick(string $imagesPath)
static isAllowedArchive(string $fileName)
static $allowedArchiveExtensions
if('@'. 'php_dir'.'@'=='@'. 'php_dir'.'@')