Bearsampp 2026.9.23
Loading...
Searching...
No Matches
QuickPick Class Reference

Public Member Functions

 __construct ()
 checkDownloadId ()
 checkQuickpickJson ()
 fetchAndUnzipModule (string $moduleUrl, string $module)
 getErrorModal (string $errorMessage)
 getModuleDestinationPath (string $moduleType, string $moduleName)
 getModules ()
 getModuleUrl (string $module, string $version)
 getQuickpickJson ()
 getQuickpickMenu (array $modules, array $versions, string $imagesPath)
 getVersions ()
 installModule (string $module, string $version)
 loadQuickpick (string $imagesPath)
 normalizeModuleName (string $moduleName)
 rebuildQuickpickJson ()

Data Fields

 $modules

Private Member Functions

 formatVersionLabel ($version, $isPrerelease=false)
 getHeaderValue ($headers, string $name)
 getLocalFileCreationTime ()
 isValidHeaderResponse ($headers)
 logHeaders (array $headers)
 regenerateMenuSafe ()
 updateModuleConfig (string $module, string $version)

Static Private Member Functions

static fetchChecksumFromSidecar (string $moduleUrl)
static isAllowedArchive (string $fileName)
static verifyModuleChecksum (string $moduleUrl, string $tmpFilePath)

Private Attributes

 $jsonFilePath
 $versions = []

Static Private Attributes

static $allowedArchiveExtensions = ['7z', 'zip']

Detailed Description

Class QuickPick

The QuickPick class provides functionalities for managing and installing various modules within the Bearsampp application. It includes methods for retrieving available modules, fetching module versions, parsing release properties, and validating license keys.

Definition at line 18 of file class.action.quickPick.php.

Constructor & Destructor Documentation

◆ __construct()

__construct ( )

Constructor to initialize the jsonFilePath.

Definition at line 76 of file class.action.quickPick.php.

77 {
78 global $bearsamppCore;
79 $this->jsonFilePath = Path::getResourcesPath() . '/quickpick-releases.json';
80 }
global $bearsamppCore
static getResourcesPath($aetrayPath=false)

References $bearsamppCore, and Path\getResourcesPath().

Here is the call graph for this function:

Member Function Documentation

◆ checkDownloadId()

checkDownloadId ( )

Validates the format of a given username key by checking it against an external API.

This method performs several checks to ensure the validity of the username key:

  1. Logs the method call.
  2. Ensures the global configuration is available.
  3. Retrieves the username key from the global configuration.
  4. Ensures the username key is not empty.
  5. Constructs the API URL using the username key.
  6. Fetches the API response.
  7. Decodes the JSON response.
  8. Validates the response data.
Returns
bool True if the username key is valid, false otherwise.

Definition at line 457 of file class.action.quickPick.php.

457 : bool
458 {
459 global $bearsamppConfig;
460
461 Log::debug('checkDownloadId method called.');
462
463 // Ensure the global config is available
464 if (!isset($bearsamppConfig)) {
465 Log::error('Global configuration is not set.');
466
467 return false;
468 }
469
470 $DownloadId = $bearsamppConfig->getDownloadId();
471
472 // Ensure the license key is not empty
473 if (empty($DownloadId)) {
474 Log::error('License key is empty.');
475
476 return false;
477 }
478
479 $url = QUICKPICK_API_URL . QUICKPICK_API_KEY . '&download_id=' . $DownloadId;
480 // Never log the raw URL: it embeds both the API key and the per-user download ID.
481 Log::debug('Validating download ID via QuickPick API.');
482
483 // Attempt to fetch the API response (verified TLS context)
484 // Note: If this fails, PHP will generate a warning which will be logged by the error handler
485 // This is expected behavior when the API server is unavailable
486 $response = file_get_contents($url, false, HttpClient::getSslStreamContext());
487
488 // Check if the response is false
489 if ($response === false) {
490 Log::error('Failed to validate QuickPick license - API server unavailable');
491
492 return false;
493 }
494
495 Log::debug('API response: ' . $response);
496
497 $data = json_decode($response, true);
498
499 // Check if the JSON decoding was successful
500 if (json_last_error() !== JSON_ERROR_NONE) {
501 Log::error('Error decoding JSON response: ' . json_last_error_msg());
502
503 return false;
504 }
505
506 // Validate the response data
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);
509
510 return true;
511 }
512
513 Log::error('Invalid license key: ' . $DownloadId);
514
515 return false;
516 }
static getSslStreamContext($verify=true, $url=null)
static debug($data, $file=null)
static error($data, $file=null)
global $bearsamppConfig
Definition homepage.php:46
const QUICKPICK_API_URL
Definition root.php:34
const QUICKPICK_API_KEY
Definition root.php:33

References $bearsamppConfig, $response, Log\debug(), Log\error(), HttpClient\getSslStreamContext(), QUICKPICK_API_KEY, and QUICKPICK_API_URL.

Referenced by getQuickpickMenu().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ checkQuickpickJson()

checkQuickpickJson ( )

Checks if the local quickpick-releases.json file is up-to-date with the remote version.

Compares the creation time of the local JSON file with the remote file's last modified time. If the remote file is newer or the local file does not exist, it fetches the latest JSON data by calling the rebuildQuickpickJson method.

Returns
array|false Returns the JSON data if the remote file is newer or the local file does not exist, otherwise returns false.
Exceptions
Exception

Definition at line 174 of file class.action.quickPick.php.

175 {
176 global $bearsamppConfig;
177
178 // Determine local file creation time or rebuild if missing
179 $localFileCreationTime = $this->getLocalFileCreationTime();
180
181 // Attempt to retrieve remote file headers. GitHub-hosted content is reached
182 // through the GitHub proxy (verified TLS context); otherwise fetch directly.
183 $headers = false;
185 // Rebuild a get_headers($url, 1)-compatible structure from the proxy
186 // response (status line at index 0 plus every forwarded header) so the
187 // downstream validation/comparison behaves identically to the direct
188 // fetch path. Only a successful (2xx) response is trusted; error pages
189 // carry headers but must not drive update decisions. Proxy header keys
190 // are lowercase, so lookups below are case-insensitive.
192 if ($result !== false && $result['status'] >= 200 && $result['status'] < 300) {
193 $headers = array('HTTP/1.1 ' . $result['status']);
194 foreach ($result['headers'] as $name => $value) {
195 $headers[$name] = $value;
196 }
197 }
198 } else {
200 }
201 if (!$this->isValidHeaderResponse($headers)) {
202 // If headers or Date/Last-Modified are invalid, assume no update needed
203 return false;
204 }
205
206 // Optionally log headers for verbose output
207 $this->logHeaders($headers);
208
209 // Compare the creation times (remote vs. local). Last-Modified reflects the
210 // actual file modification time; Date is only a fallback for servers (or
211 // proxies) that do not forward Last-Modified.
212 $remoteModTime = $this->getHeaderValue($headers, 'Last-Modified')
213 ?? $this->getHeaderValue($headers, 'Date')
214 ?? '';
215 $remoteFileCreationTime = strtotime($remoteModTime);
216 if ($remoteFileCreationTime > $localFileCreationTime) {
217 return $this->rebuildQuickpickJson();
218 }
219
220 // Return false if local file is already up-to-date
221 return false;
222 }
$result
static proxyFetch($url, $method='GET', $verify=true)
static isGithubHost($url)
isValidHeaderResponse($headers)
getHeaderValue($headers, string $name)
logHeaders(array $headers)
const QUICKPICK_JSON_URL
Definition root.php:37

References $bearsamppConfig, $result, getHeaderValue(), getLocalFileCreationTime(), HttpClient\getSslStreamContext(), HttpClient\isGithubHost(), isValidHeaderResponse(), logHeaders(), HttpClient\proxyFetch(), QUICKPICK_JSON_URL, and rebuildQuickpickJson().

Referenced by loadQuickpick().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ fetchAndUnzipModule()

fetchAndUnzipModule ( string $moduleUrl,
string $module )

Fetches the module URL and stores it in /tmp, then unzips the file based on its extension.

Parameters
string$moduleUrlThe URL of the module to fetch.
string$moduleThe name of the module.
Returns
array An array containing the status and message.

Definition at line 625 of file class.action.quickPick.php.

625 : array
626 {
627 Log::debug("$module is: " . $module);
628
630 $tmpDir = Path::getTmpPath();
631 Log::debug('Temporary Directory: ' . $tmpDir);
632
633 $fileName = basename($moduleUrl);
634 Log::debug('File Name: ' . $fileName);
635
636 $tmpFilePath = $tmpDir . '/' . $fileName;
637 Log::debug('File Path: ' . $tmpFilePath);
638
639 // Strictly validate the archive extension BEFORE downloading, so we never fetch
640 // or unpack anything other than an allowed 7z/zip archive regardless of the URL.
641 if (!self::isAllowedArchive($fileName)) {
642 Log::error('Unsupported archive type rejected before download: ' . $fileName);
643
644 return ['error' => 'Unsupported archive type'];
645 }
646
647 $moduleName = str_replace('module-', '', $module);
648 Log::debug('Module Name: ' . $moduleName);
649
650 // Find the correct module key by searching through the modules array
651 // This handles proper capitalization for all module types
652 $moduleKey = null;
653 foreach ($this->modules as $key => $moduleInfo) {
654 if (strtolower($key) === strtolower($moduleName)) {
655 $moduleKey = $key;
656 break;
657 }
658 }
659
660 if (!$moduleKey) {
661 Log::error("Module not found in modules array: $moduleName");
662
663 return ['error' => 'Module configuration not found'];
664 }
665
666 $moduleType = $this->modules[$moduleKey]['type'];
667 Log::debug('Module Type: ' . $moduleType);
668
669 // Get module type
670 $destination = $this->getModuleDestinationPath($moduleType, $moduleName);
671 Log::debug('Destination: ' . $destination);
672
673 // Retrieve the file path from the URL via HttpClient's streaming download,
674 // passing the module URL and temporary file path, with the use Progress Bar parameter set to true.
675 $result = HttpClient::downloadFile($moduleUrl, $tmpFilePath, true);
676
677 // Check if $result indicates an error (downloadFile returns ['error' => ...] on failure)
678 if (!is_array($result) || isset($result['error'])) {
679 Log::error('Failed to retrieve file from URL: ' . $moduleUrl);
680 @unlink($tmpFilePath);
681
682 return ['error' => 'Failed to retrieve file from URL'];
683 }
684
685 // Verify the downloaded archive against the SHA-256 sidecar published with the release.
686 // This guards against a tampered JSON, a bypassed/disabled TLS check, or a corrupted
687 // download, ensuring we never extract/modify an unverified archive.
688 if (!self::verifyModuleChecksum($moduleUrl, $tmpFilePath)) {
689 Log::error('SHA-256 checksum verification failed for module: ' . $module . ' (URL: ' . $moduleUrl . ')');
690 @unlink($tmpFilePath);
691
692 return ['error' => 'Checksum verification failed. Download aborted. File: ' . basename($moduleUrl)];
693 }
694
695 // Determine the file extension and call the appropriate unzipping function.
696 // Enforce the strict whitelist on the actual downloaded file as a second layer
697 // of defense, even though it was pre-validated before download.
698 if (!self::isAllowedArchive($tmpFilePath)) {
699 Log::error('Unsupported archive extension after download: ' . $tmpFilePath);
700 @unlink($tmpFilePath);
701
702 return ['error' => 'Unsupported archive extension'];
703 }
704
705 $fileExtension = strtolower(pathinfo($tmpFilePath, PATHINFO_EXTENSION));
706 Log::debug('File extension: ' . $fileExtension);
707
708 if ($fileExtension === '7z' || $fileExtension === 'zip') {
709 echo json_encode(['phase' => 'extracting']) . PHP_EOL;
710 if (ob_get_length()) {
711 ob_flush();
712 }
713 flush();
714
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()) {
719 ob_flush();
720 }
721 flush();
722 });
723
724 if ($unzipResult === false) {
725 return ['error' => 'Failed to unzip file. File: ' . $tmpFilePath . ' could not be unzipped', 'Destination: ' . $destination];
726 }
727 } else {
728 Log::error('Unsupported file extension: ' . $fileExtension);
729
730 return ['error' => 'Unsupported file extension'];
731 }
732
733 return ['success' => 'Module installed successfully'];
734 }
global $bearsamppRoot
static downloadFile(string $url, string $filePath, $progressBar=false, $verify=true)
static getTmpPath($aetrayPath=false)
getModuleDestinationPath(string $moduleType, string $moduleName)

References $bearsamppCore, $bearsamppRoot, $result, Log\debug(), HttpClient\downloadFile(), Log\error(), getModuleDestinationPath(), and Path\getTmpPath().

Referenced by installModule().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ fetchChecksumFromSidecar()

fetchChecksumFromSidecar ( string $moduleUrl)
staticprivate

Fetches and parses the expected SHA-256 digest from the asset sidecar file.

The sidecar URL is the module download URL with '.sha256' appended, and its contents follow the standard "<hex-hash> <filename>" format. Only a full 64-char lowercase hex digest is accepted.

Parameters
string$moduleUrlThe module download URL.
Returns
string|null The expected SHA-256 hex digest, or null on failure.

Definition at line 795 of file class.action.quickPick.php.

795 : ?string
796 {
797 $sidecarUrl = $moduleUrl . '.sha256';
798
799 // GitHub-hosted sidecars are fetched through the GitHub proxy; everything
800 // else uses the verified TLS stream context.
801 if (HttpClient::isGithubHost($sidecarUrl)) {
802 Log::trace('verifyModuleChecksum() fetching sidecar via GitHub proxy: ' . $sidecarUrl);
803 $result = HttpClient::proxyFetch($sidecarUrl, 'GET', true);
804 $content = ($result === false) ? false : $result['body'];
805 } else {
806 $content = @file_get_contents($sidecarUrl, false, HttpClient::getSslStreamContext(true, $sidecarUrl));
807 }
808
809 if ($content === false) {
810 Log::error('Checksum verify: sidecar fetch failed for: ' . $sidecarUrl);
811
812 return null;
813 }
814
815 if (preg_match('/\b([0-9a-f]{64})\b/i', $content, $match) !== 1) {
816 Log::error('Checksum verify: unexpected sidecar content for: ' . $sidecarUrl);
817
818 return null;
819 }
820
821 return strtolower($match[1]);
822 }
static trace($data, $file=null)

References $result, Log\error(), HttpClient\getSslStreamContext(), HttpClient\isGithubHost(), HttpClient\proxyFetch(), and Log\trace().

Referenced by verifyModuleChecksum().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ formatVersionLabel()

formatVersionLabel ( $version,
$isPrerelease = false )
private

Format version label with PR indicator if it's a prerelease

Parameters
string$versionThe version to format
bool$isPrereleaseWhether this version is a prerelease
Returns
string Formatted version string

Definition at line 90 of file class.action.quickPick.php.

91 {
92 global $bearsamppConfig;
93 $includePr = $bearsamppConfig->getIncludePr();
94
95 if ($isPrerelease && $includePr == 1) {
96 return '<span class="text-danger">' . htmlspecialchars($version) . ' PR</span>';
97 }
98
99 return htmlspecialchars($version);
100 }

References $bearsamppConfig.

◆ getErrorModal()

getErrorModal ( string $errorMessage)

Generates an error modal for configuration validation failures.

Parameters
string$errorMessageThe error message to display.
Returns
string The HTML content of the error modal.

Definition at line 980 of file class.action.quickPick.php.

980 : string
981 {
982 ob_start();
983 ?>
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
988 </h4>
989 <hr>
990 <p class = "mb-0">
991 <?php
992 echo htmlspecialchars($errorMessage); ?>
993 </p>
994 <hr>
995 <small class = "text-muted">
996 Please add the missing parameter to the <code>bearsampp.conf</code> file in the Bearsampp root directory.
997 </small>
998 </div>
999 </div>
1000 <?php
1001 return ob_get_clean();
1002 }

Referenced by loadQuickpick().

Here is the caller graph for this function:

◆ getHeaderValue()

getHeaderValue ( $headers,
string $name )
private

Retrieves a header value case-insensitively.

HTTP header names are case-insensitive. get_headers() may return keys in any case, while the GitHub proxy normalizes them to lowercase, so lookups must not rely on exact key casing.

Parameters
mixed$headersThe header map (or false on failure).
string$nameThe header name to look up.
Returns
string|null The header value, or null if absent.

Definition at line 278 of file class.action.quickPick.php.

278 : ?string
279 {
280 if (!is_array($headers)) {
281 return null;
282 }
283 foreach ($headers as $key => $value) {
284 if (is_string($key) && strcasecmp($key, $name) === 0) {
285 if (is_array($value)) {
286 $value = reset($value);
287 }
288
289 return is_string($value) ? $value : null;
290 }
291 }
292
293 return null;
294 }

Referenced by checkQuickpickJson(), and isValidHeaderResponse().

Here is the caller graph for this function:

◆ getLocalFileCreationTime()

getLocalFileCreationTime ( )
private

Returns the local file's creation time, or triggers and returns 0 if file does not exist.

Returns
int Local file's creation time or 0 if the file doesn't exist.

Definition at line 229 of file class.action.quickPick.php.

230 {
231 if (!file_exists($this->jsonFilePath)) {
232 // If local file is missing, rebuild it immediately
233 $this->rebuildQuickpickJson();
234
235 return 0;
236 }
237
238 return filectime($this->jsonFilePath);
239 }

References rebuildQuickpickJson().

Referenced by checkQuickpickJson().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getModuleDestinationPath()

getModuleDestinationPath ( string $moduleType,
string $moduleName )

Get the destination path for a given module type and name.

This method constructs the destination path based on the type of module (application, binary, or tools) and the module name. It utilizes the bearsamppRoot global object to retrieve the base paths for each module type.

Parameters
string$moduleTypeThe type of the module ('application', 'binary', or 'tools').
string$moduleNameThe name of the module.
Returns
string The constructed destination path for the module.

Definition at line 858 of file class.action.quickPick.php.

859 {
860 global $bearsamppRoot;
861 if ($moduleType === 'application') {
862 $destination = Path::getAppsPath() . '/' . strtolower($moduleName) . '/';
863 } elseif ($moduleType === 'binary') {
864 $destination = Path::getBinPath() . '/' . strtolower($moduleName) . '/';
865 } elseif ($moduleType === 'tools') {
866 $destination = Path::getToolsPath() . '/' . strtolower($moduleName) . '/';
867 } else {
868 $destination = '';
869 }
870
871 return $destination;
872 }
static getToolsPath($aetrayPath=false)
static getBinPath($aetrayPath=false)
static getAppsPath($aetrayPath=false)

References $bearsamppRoot, Path\getAppsPath(), Path\getBinPath(), and Path\getToolsPath().

Referenced by fetchAndUnzipModule().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getModules()

getModules ( )

Retrieves the list of available modules.

Returns
array An array of module names.

Definition at line 131 of file class.action.quickPick.php.

131 : array
132 {
133 return array_keys($this->modules);
134 }

Referenced by loadQuickpick().

Here is the caller graph for this function:

◆ getModuleUrl()

getModuleUrl ( string $module,
string $version )

Fetches the URL of a specified module version from the local quickpick-releases.json file.

This method reads the quickpick-releases.json file to find the URL associated with the given module and version. It logs the process and returns the URL if found, or an error message if not.

Parameters
string$moduleThe name of the module.
string$versionThe version of the module.
Returns
string|array The URL of the specified module version or an error message if the version is not found.

Definition at line 420 of file class.action.quickPick.php.

421 {
422 $this->getVersions();
423 Log::debug('getModuleUrl called for module: ' . $module . ' version: ' . $version);
424 $moduleKey = 'module-' . strtolower($module);
425 if (!isset($this->versions[$moduleKey][$version]['url'])) {
426 Log::error('Version not found: ' . $version);
427
428 return ['error' => 'Version not found'];
429 }
430 $url = trim($this->versions[$moduleKey][$version]['url']);
431 if ($url <> '') {
432 Log::debug('Found URL for version: ' . $version . ' URL: ' . $url);
433
434 return $url;
435 } else {
436 Log::error('Version not found: ' . $version);
437
438 return ['error' => 'Version not found'];
439 }
440 }

References Log\debug(), Log\error(), and getVersions().

Referenced by installModule().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getQuickpickJson()

getQuickpickJson ( )

Retrieves the QuickPick JSON data from the local file.

Returns
array The decoded JSON data, or an error message if the file cannot be fetched or decoded.

Definition at line 315 of file class.action.quickPick.php.

315 : array
316 {
317 $content = @file_get_contents($this->jsonFilePath);
318 if ($content === false) {
319 Log::error('Error fetching content from JSON file: ' . $this->jsonFilePath);
320
321 return ['error' => 'Error fetching JSON file'];
322 }
323
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());
327
328 return ['error' => 'Error decoding JSON content'];
329 }
330
331 return $data;
332 }

References Log\error().

Referenced by getVersions().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getQuickpickMenu()

getQuickpickMenu ( array $modules,
array $versions,
string $imagesPath )

Generates the HTML content for the QuickPick menu.

This method creates the HTML structure for the QuickPick interface, including a dropdown for selecting modules and their respective versions. It checks if the license key is valid before displaying the modules. If the license key is invalid, it displays a subscription prompt. If there is no internet connection, it displays a message indicating the lack of internet.

Parameters
array$modulesAn array of available modules.
array$versionsAn associative array where the key is the module name and the value is an array containing the module versions.
string$imagesPathThe path to the images directory.
Returns
string The HTML content of the QuickPick menu.

Definition at line 1018 of file class.action.quickPick.php.

1018 : string
1019 {
1020 global $bearsamppConfig;
1021 $includePr = $bearsamppConfig->getIncludePr();
1022 $enhancedMode = $bearsamppConfig->getEnhancedQuickPick();
1023
1024 ob_start();
1026 // Check if the license key is valid
1027 if ($this->checkDownloadId()): ?>
1028 <div class = "enhanced-mode-toggle">
1029 <label class = "form-check-label me-2" for = "enhancedQuickPickSwitch">
1030 Enhanced Mode
1031 </label>
1032 <div class = "form-check form-switch mb-0">
1033 <input class = "form-check-input" type = "checkbox" role = "switch" id = "enhancedQuickPickSwitch"
1034 <?php
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">
1038 </div>
1039 </div>
1040 <div id = 'quickPickContainer'>
1041 <div class = 'quickpick'>
1042
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>
1051 </button>
1052 <ul class = "select-dropdown" role = "listbox" id = "select-dropdown">
1053
1054 <?php
1055 foreach ($modules as $module): ?>
1056 <?php
1057 if (is_string($module)): ?>
1058 <li role = "option" class = "moduleheader">
1059 <?php
1060 echo htmlspecialchars($module); ?>
1061 </li>
1062
1063 <?php
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) {
1067 continue;
1068 }
1069 ?>
1070 <li role = "option" class = "moduleoption"
1071 id = "<?php
1072 echo htmlspecialchars($module); ?>-version-<?php
1073 echo htmlspecialchars($version_array['version']); ?>-li"
1074 data-module = "<?php
1075 echo htmlspecialchars($module); ?>"
1076 data-value = "<?php
1077 echo htmlspecialchars($version_array['version']); ?>">
1078 <input type = "radio"
1079 id = "<?php
1080 echo htmlspecialchars($module); ?>-version-<?php
1081 echo htmlspecialchars($version_array['version']); ?>"
1082 name = "module" data-module = "<?php
1083 echo htmlspecialchars($module); ?>"
1084 data-value = "<?php
1085 echo htmlspecialchars($version_array['version']); ?>">
1086 <label
1087 for = "<?php
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
1093 ); ?></label>
1094 </li>
1095 <?php
1096 endforeach; ?>
1097 <?php
1098 endif; ?>
1099 <?php
1100 endforeach; ?>
1101 </ul>
1102 </div>
1103 </div>
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%
1109 </div>
1110 <div id = "download-module" style = "display: none">ModuleName</div>
1111 <div id = "download-version" style = "display: none">Version</div>
1112 </div>
1113 </div>
1114 <?php
1115 else: ?>
1116 <div id = "subscribeContainer" class = "text-center">
1117 <a href = "<?php
1118 echo HttpClient::getWebsiteUrl('subscribe'); ?>" class = "btn btn-dark d-inline-flex align-items-center">
1119 <img src = "<?php
1120 echo $imagesPath . 'subscribe.svg'; ?>" alt = "Subscribe Icon" class = "me-2">
1121 Subscribe to QuickPick now
1122 </a>
1123 </div>
1124 <?php
1125 endif;
1126 } else {
1127 ?>
1128 <div id = "InternetState" class = "text-center">
1129 <img src = "<?php
1130 echo $imagesPath . 'no-wifi-icon.svg'; ?>" alt = "No Wifi Icon" class = "me-2">
1131 <span>No internet present</span>
1132 </div>
1133 <?php
1134 }
1135
1136 return ob_get_clean();
1137 }
static getWebsiteUrl($path='', $fragment='', $utmSource=true)
static checkInternetState()
$imagesPath
Definition homepage.php:57

References $bearsamppConfig, $imagesPath, checkDownloadId(), HttpClient\getWebsiteUrl(), and if.

Referenced by loadQuickpick().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ getVersions()

getVersions ( )

Retrieves the list of available versions for all modules.

This method fetches the QuickPick JSON data and returns an array of versions or If no versions are found, an error message is logged and returned.

Returns
array An array of version strings for the specified module, or an error message if no versions are found.

Definition at line 372 of file class.action.quickPick.php.

372 : array
373 {
374 Log::debug('Versions called');
375
376 $versions = [];
377
378 $jsonData = $this->getQuickpickJson();
379
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']);
387 });
388 $versions[$entry['module']] = $moduleVersions;
389 }
390 }
391 } else {
392 Log::error('Invalid entry format in JSON data');
393 }
394 }
395
396 if (empty($versions)) {
397 Log::error('No versions found');
398
399 return ['error' => 'No versions found'];
400 }
401
402 Log::debug('Found versions');
403
404 $this->versions = $versions;
405
406 return $versions;
407 }

References $versions, Log\debug(), Log\error(), and getQuickpickJson().

Referenced by getModuleUrl(), and loadQuickpick().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ installModule()

installModule ( string $module,
string $version )

Installs a specified module by fetching its URL and unzipping its contents.

This method retrieves the URL of the specified module and version from the QuickPick JSON data. If the URL is found, it fetches and unzips the module. If the URL is not found, it logs an error and returns an error message.

Parameters
string$moduleThe name of the module to install.
string$versionThe version of the module to install.
Returns
array An array containing the status and message of the installation process. If successful, it returns the response from the fetchAndUnzipModule method. If unsuccessful, it returns an error message indicating the issue.

Definition at line 532 of file class.action.quickPick.php.

532 : array
533 {
534 // Find the module URL and module name from the data
535 $moduleUrl = $this->getModuleUrl($module, $version);
536
537 if (is_array($moduleUrl) && isset($moduleUrl['error'])) {
538 Log::error('Module URL not found for module: ' . $module . ' version: ' . $version);
539
540 return ['error' => 'Module URL not found'];
541 }
542
543 if (empty($moduleUrl)) {
544 Log::error('Module URL not found for module: ' . $module . ' version: ' . $version);
545
546 return ['error' => 'Module URL not found'];
547 }
548
550 if ($state) {
551 $response = $this->fetchAndUnzipModule($moduleUrl, $module);
552 Log::debug('Response is: ' . print_r($response, true));
553
554 // Check if enhanced mode is enabled
555 global $bearsamppConfig;
556 $enhancedMode = $bearsamppConfig->getEnhancedQuickPick();
557
558 Log::debug('Enhanced mode: ' . ($enhancedMode ? 'enabled' : 'disabled'));
559
560 // If installation was successful and enhanced mode is enabled, update config
561 if (isset($response['success']) && $enhancedMode == 1) {
562 // Step 1: Update config FIRST (so reload can pick up the new version)
563 Log::debug('Enhanced mode enabled - Updating config for module: ' . $module . ' version: ' . $version);
564 $configUpdated = $this->updateModuleConfig($module, $version);
565
566 if ($configUpdated) {
567 // Step 2: Launch the reload action to apply the new version automatically.
568 // QuickPick runs in the AJAX/web context, where the winbinder GUI used by
569 // the reload action is unavailable, so we spawn it as a detached process
570 // (the same "php-win.exe root.php reload" command the tray menu runs). The
571 // reload restarts the database services so clients such as phpMyAdmin pick
572 // up the new version without the user having to reload manually.
573 Log::debug('Config updated successfully, launching reload to apply changes...');
574
575 // Send progress update to user - flush output
576 if (ob_get_level() > 0) {
577 ob_flush();
578 }
579 echo json_encode(['phase' => 'updating', 'message' => 'Applying version changes...']) . PHP_EOL;
580 flush();
581
582 // Clear caches before the reload runs so it reads fresh values from disk
583 Log::debug('Clearing caches before reload...');
585
586 // Build and launch the reload command detached from this request.
587 // Leading "" is the (empty) window title required by cmd.exe "start".
588 $reloadCmd = '"" "' . Path::getPhpExe() . '" "'
589 . Path::getCorePath() . '/' . Core::isRoot_FILE . '" '
591 Log::debug('Launching reload command: ' . $reloadCmd);
592 CommandRunner::background($reloadCmd);
593
594 $response['reload_triggered'] = true;
595 } else {
596 Log::error('Config update failed for module: ' . $module);
597 $response['reload_triggered'] = false;
598 }
599 } else {
600 if (isset($response['success']) && $enhancedMode == 0) {
601 Log::debug('Enhanced mode disabled - skipping config update');
602
603 // Even if not updating config, clear cache to be safe as new files were added
604 Log::debug('Clearing caches after module installation (Standard mode)...');
606 }
607 }
608
609 return $response;
610 } else {
611 Log::error('No internet connection available.');
612
613 return ['error' => 'No internet connection'];
614 }
615 }
const RELOAD
static background(string $command)
const isRoot_FILE
static getCorePath($aetrayPath=false)
static getPhpExe($aetrayPath=false)
fetchAndUnzipModule(string $moduleUrl, string $module)
getModuleUrl(string $module, string $version)
updateModuleConfig(string $module, string $version)

References $bearsamppConfig, $response, CommandRunner\background(), HttpClient\checkInternetState(), CacheManager\clearAll(), Log\debug(), Log\error(), fetchAndUnzipModule(), Path\getCorePath(), getModuleUrl(), Path\getPhpExe(), Core\isRoot_FILE, Action\RELOAD, and updateModuleConfig().

Here is the call graph for this function:

◆ isAllowedArchive()

isAllowedArchive ( string $fileName)
staticprivate

Checks whether a file path/name has an allowed archive extension.

Only lowercase '.7z' and '.zip' are accepted. This is used both before download (from the URL filename) and after download (from the actual file) to strictly reject anything else.

Parameters
string$fileNameFile name or path to validate.
Returns
bool True if the extension is allowed.

Definition at line 835 of file class.action.quickPick.php.

835 : bool
836 {
837 if ($fileName === '' || $fileName === false) {
838 return false;
839 }
840
841 $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
842
843 return in_array($extension, self::$allowedArchiveExtensions, true);
844 }

◆ isValidHeaderResponse()

isValidHeaderResponse ( $headers)
private

Determines whether the header response is valid and includes a 'Date' or 'Last-Modified' key.

Both direct (get_headers) and GitHub-proxy HEAD responses are accepted, so update checks keep working even if the proxy forwards Last-Modified but not Date (or vice versa). Header name matching is case-insensitive.

Parameters
mixed$headersHeaders retrieved from get_headers() or the GitHub proxy.
Returns
bool True if headers are valid and contain 'Date' or 'Last-Modified', false otherwise.

Definition at line 254 of file class.action.quickPick.php.

254 : bool
255 {
256 // If headers retrieval failed or neither Date nor Last-Modified is set, return false
257 if ($headers === false ||
258 ($this->getHeaderValue($headers, 'Date') === null &&
259 $this->getHeaderValue($headers, 'Last-Modified') === null)) {
260 return false;
261 }
262
263 return true;
264 }

References getHeaderValue().

Referenced by checkQuickpickJson().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ loadQuickpick()

loadQuickpick ( string $imagesPath)

Loads the QuickPick interface with the available modules and their versions.

Parameters
string$imagesPathThe path to the images directory.
Returns
string The HTML content of the QuickPick interface.
Exceptions
Exception

Definition at line 145 of file class.action.quickPick.php.

145 : string
146 {
147 global $bearsamppConfig;
148
149 // Validate EnhancedQuickPick parameter
150 $validation = $bearsamppConfig->validateEnhancedQuickPick();
151 if (!$validation['valid']) {
152 return $this->getErrorModal($validation['error']);
153 }
154
155 $this->checkQuickpickJson();
156
157 $modules = $this->getModules();
158 $versions = $this->getVersions();
159
161 }
getErrorModal(string $errorMessage)
getQuickpickMenu(array $modules, array $versions, string $imagesPath)

References $bearsamppConfig, $imagesPath, $modules, $versions, checkQuickpickJson(), getErrorModal(), getModules(), getQuickpickMenu(), and getVersions().

Here is the call graph for this function:

◆ logHeaders()

logHeaders ( array $headers)
private

Logs the headers in debug mode if logsVerbose is set to 2.

Parameters
array$headersThe headers returned by get_headers().

Definition at line 301 of file class.action.quickPick.php.

301 : void
302 {
303 global $bearsamppConfig;
304
305 if ($bearsamppConfig->getLogsVerbose() === 2) {
306 Log::debug('Headers: ' . print_r($headers, true));
307 }
308 }

References $bearsamppConfig, and Log\debug().

Referenced by checkQuickpickJson().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ normalizeModuleName()

normalizeModuleName ( string $moduleName)

Normalizes a module name to find the correct module key from the modules array. Handles case-insensitive matching for all module types.

Parameters
string$moduleNameThe module name to normalize (may include 'module-' prefix)
Returns
string|null The correctly capitalized module key, or null if not found

Definition at line 110 of file class.action.quickPick.php.

110 : ?string
111 {
112 // Remove 'module-' prefix if present
113 $moduleName = str_replace('module-', '', $moduleName);
114
115 // Find the correct module key by searching through the modules array
116 // This handles proper capitalization for all module types
117 foreach ($this->modules as $key => $moduleInfo) {
118 if (strtolower($key) === strtolower($moduleName)) {
119 return $key;
120 }
121 }
122
123 return null;
124 }

◆ rebuildQuickpickJson()

rebuildQuickpickJson ( )

Rebuilds the local quickpick-releases.json file by fetching the latest data from the remote URL.

Returns
array An array containing the status and message of the rebuild process.
Exceptions
ExceptionIf the JSON content cannot be fetched or saved.

Definition at line 340 of file class.action.quickPick.php.

340 : array
341 {
342 Log::debug('Fetching JSON file: ' . $this->jsonFilePath);
343
344 // Fetch the JSON content from the URL via cURL (verified TLS + bundled GitHub token)
346
347 if ($jsonContent === '') {
348 // Handle error if the file could not be fetched
349 throw new Exception('Failed to fetch JSON content from the URL.');
350 }
351
352 // Save the JSON content to the specified path
353 $result = file_put_contents($this->jsonFilePath, $jsonContent);
354
355 if ($result === false) {
356 // Handle error if the file could not be saved
357 throw new Exception('Failed to save JSON content to the specified path.');
358 }
359
360 // Return success message
361 return ['success' => 'JSON content fetched and saved successfully'];
362 }
static getApiJson($url, $verify=true)

References $result, Log\debug(), HttpClient\getApiJson(), and QUICKPICK_JSON_URL.

Referenced by checkQuickpickJson(), and getLocalFileCreationTime().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ regenerateMenuSafe()

regenerateMenuSafe ( )
private

Regenerates the bearsampp.ini menu file without VBS checks (AJAX-safe version). This is a simplified version of TplApp\process() that avoids VBS errors in web context.

Returns
string The generated INI content

Definition at line 880 of file class.action.quickPick.php.

880 : string
881 {
882 Log::debug('Regenerating menu (AJAX-safe mode)...');
883
884 // Suppress errors temporarily during menu generation
885 $oldErrorReporting = error_reporting();
886 error_reporting($oldErrorReporting & ~E_WARNING);
887
888 try {
889 // Generate the menu content
890 $menuContent = TplApp::process();
891
892 // Restore error reporting
893 error_reporting($oldErrorReporting);
894
895 Log::debug('Menu regenerated successfully');
896
897 return $menuContent;
898 } catch (Exception $e) {
899 // Restore error reporting
900 error_reporting($oldErrorReporting);
901
902 Log::warning('Error during menu regeneration: ' . $e->getMessage());
903 throw $e;
904 }
905 }
static warning($data, $file=null)
static process()

References Log\debug(), TplApp\process(), and Log\warning().

Here is the call graph for this function:

◆ updateModuleConfig()

updateModuleConfig ( string $module,
string $version )
private

Updates the bearsampp.conf configuration file with the new module version. This method handles all module types: binaries, apps, and tools.

Parameters
string$moduleThe name of the module (e.g., 'Apache', 'PhpMyAdmin', 'Git').
string$versionThe version to set in the configuration.
Returns
bool True if the configuration was updated successfully, false otherwise.

Definition at line 916 of file class.action.quickPick.php.

916 : bool
917 {
918 try {
919 $bearsamppConfig = new Config();
920
921 // Remove 'module-' prefix if present and normalize the module name
922 $moduleName = str_replace('module-', '', $module);
923
924 // Find the correct module key by searching through the modules array
925 // This handles proper capitalization for all module types
926 $moduleKey = null;
927 foreach ($this->modules as $key => $moduleInfo) {
928 if (strtolower($key) === strtolower($moduleName)) {
929 $moduleKey = $key;
930 break;
931 }
932 }
933
934 if (!$moduleKey) {
935 Log::error("Module not found in modules array: $moduleName");
936
937 return false;
938 }
939
940 $moduleType = $this->modules[$moduleKey]['type'];
941
942 // Reject malformed version strings before they reach the config
943 // file (they later drive generated shell commands). This guards
944 // against a compromised or tampered releases feed.
945 if (preg_match('/^[0-9][0-9a-zA-Z.\-+]*$/', $version) !== 1) {
946 Log::error("Invalid version format for module: $module");
947
948 return false;
949 }
950
951 // Map module names to their config section names
952 // For all types, use the lowercase name for the config key
953 $configSection = strtolower($moduleKey);
954
955 Log::debug("Updating config for module: $module (key: $moduleKey, type: $moduleType) to version: $version");
956 Log::debug("Config section: $configSection");
957
958 // Update the configuration file
959 // The Config class expects a flat key like "nodejsVersion" not a section
960 $configKey = $configSection . 'Version';
961 $bearsamppConfig->replace($configKey, $version);
962
963 Log::info("Successfully updated $configSection version to $version in bearsampp.conf");
964
965 return true;
966 } catch (Exception $e) {
967 Log::error("Failed to update module config: " . $e->getMessage());
968
969 return false;
970 }
971 }
static info($data, $file=null)

References $bearsamppConfig, Log\debug(), Log\error(), and Log\info().

Referenced by installModule().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ verifyModuleChecksum()

verifyModuleChecksum ( string $moduleUrl,
string $tmpFilePath )
staticprivate

Verifies the SHA-256 checksum of a downloaded module archive.

The expected hash is read from the .sha256 sidecar published alongside the release asset (e.g. <archive>.7z.sha256), fetched over a verified TLS context. The archive is only considered valid if its computed hash matches exactly.

Parameters
string$moduleUrlThe URL the archive was downloaded from.
string$tmpFilePathThe local path of the downloaded archive.
Returns
bool True if the hash matches, false otherwise.

Definition at line 748 of file class.action.quickPick.php.

748 : bool
749 {
750 if (!is_file($tmpFilePath)) {
751 Log::error('Checksum verify: downloaded file not found: ' . $tmpFilePath);
752
753 return false;
754 }
755
756 $expectedHash = self::fetchChecksumFromSidecar($moduleUrl);
757 if ($expectedHash === null) {
758 Log::error('Checksum verify: could not retrieve SHA-256 sidecar for: ' . $moduleUrl);
759
760 return false;
761 }
762
763 $actualHash = @hash_file('sha256', $tmpFilePath);
764 if ($actualHash === false) {
765 Log::error('Checksum verify: failed to hash local file: ' . $tmpFilePath);
766
767 return false;
768 }
769
770 if (!hash_equals($expectedHash, strtolower($actualHash))) {
772 'Checksum verify: mismatch for ' . basename($moduleUrl) .
773 ' (expected ' . $expectedHash . ', got ' . $actualHash . ')'
774 );
775
776 return false;
777 }
778
779 Log::debug('Checksum verify: OK for ' . basename($moduleUrl));
780
781 return true;
782 }
static fetchChecksumFromSidecar(string $moduleUrl)

References Log\debug(), Log\error(), and fetchChecksumFromSidecar().

Here is the call graph for this function:

Field Documentation

◆ $allowedArchiveExtensions

$allowedArchiveExtensions = ['7z', 'zip']
staticprivate

Whitelist of archive extensions that may be downloaded and extracted.

Definition at line 71 of file class.action.quickPick.php.

◆ $jsonFilePath

$jsonFilePath
private

The file path to the local quickpick-releases.json file.

Definition at line 64 of file class.action.quickPick.php.

◆ $modules

$modules
Initial value:
= [
'Apache' => ['type' => 'binary'],
'Bruno' => ['type' => 'tools'],
'Composer' => ['type' => 'tools'],
'Ghostscript' => ['type' => 'tools'],
'Git' => ['type' => 'tools'],
'Mailpit' => ['type' => 'binary'],
'MariaDB' => ['type' => 'binary'],
'Memcached' => ['type' => 'binary'],
'MySQL' => ['type' => 'binary'],
'Ngrok' => ['type' => 'tools'],
'NodeJS' => ['type' => 'binary'],
'Perl' => ['type' => 'tools'],
'PHP' => ['type' => 'binary'],
'PhpMyAdmin' => ['type' => 'application'],
'PhpPgAdmin' => ['type' => 'application'],
'PostgreSQL' => ['type' => 'binary'],
'PowerShell' => ['type' => 'tools'],
'Python' => ['type' => 'tools'],
'Ruby' => ['type' => 'tools'],
'Xlight' => ['type' => 'binary']
]

An associative array where the key is the module name and the value is an array containing the module type. The module type can be one of the following:

  • 'application'
  • 'binary'
  • 'tool'

Definition at line 29 of file class.action.quickPick.php.

Referenced by loadQuickpick().

◆ $versions

$versions = []
private

An associative array where the key is the module name and the value is an array containing the module versions.

Definition at line 57 of file class.action.quickPick.php.

Referenced by getVersions(), and loadQuickpick().


The documentation for this class was generated from the following file: