Bearsampp 2026.9.23
Loading...
Searching...
No Matches
class.action.quickPick.php
Go to the documentation of this file.
1<?php
2/*
3 *
4 * * Copyright (c) 2022-2025 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
19{
29 public $modules = [
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']
50 ];
51
57 private $versions = [];
58
65
71 private static $allowedArchiveExtensions = ['7z', 'zip'];
72
76 public function __construct()
77 {
78 global $bearsamppCore;
79 $this->jsonFilePath = Path::getResourcesPath() . '/quickpick-releases.json';
80 }
81
90 private function formatVersionLabel($version, $isPrerelease = false)
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 }
101
110 public function normalizeModuleName(string $moduleName): ?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 }
125
131 public function getModules(): array
132 {
133 return array_keys($this->modules);
134 }
135
145 public function loadQuickpick(string $imagesPath): 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 }
162
174 public function checkQuickpickJson()
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 }
223
229 private function getLocalFileCreationTime()
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 }
240
254 private function isValidHeaderResponse($headers): 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 }
265
278 private function getHeaderValue($headers, string $name): ?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 }
295
301 private function logHeaders(array $headers): void
302 {
303 global $bearsamppConfig;
304
305 if ($bearsamppConfig->getLogsVerbose() === 2) {
306 Log::debug('Headers: ' . print_r($headers, true));
307 }
308 }
309
315 public function getQuickpickJson(): 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 }
333
340 public function rebuildQuickpickJson(): 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 }
363
372 public function getVersions(): 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 }
408
420 public function getModuleUrl(string $module, string $version)
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 }
441
457 public function checkDownloadId(): 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 }
517
532 public function installModule(string $module, string $version): 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 }
616
625 public function fetchAndUnzipModule(string $moduleUrl, string $module): 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 }
735
748 private static function verifyModuleChecksum(string $moduleUrl, string $tmpFilePath): 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 }
783
795 private static function fetchChecksumFromSidecar(string $moduleUrl): ?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 }
823
835 private static function isAllowedArchive(string $fileName): 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 }
845
858 public function getModuleDestinationPath(string $moduleType, string $moduleName)
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 }
873
880 private function regenerateMenuSafe(): 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 }
906
916 private function updateModuleConfig(string $module, string $version): 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 }
972
980 public function getErrorModal(string $errorMessage): 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 }
1003
1018 public function getQuickpickMenu(array $modules, array $versions, string $imagesPath): string
1019 {
1020 global $bearsamppConfig;
1021 $includePr = $bearsamppConfig->getIncludePr();
1022 $enhancedMode = $bearsamppConfig->getEnhancedQuickPick();
1023
1024 ob_start();
1025 if (HttpClient::checkInternetState()) {
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 }
1138}
$result
global $bearsamppRoot
global $bearsamppCore
const RELOAD
static background(string $command)
const isRoot_FILE
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)
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
static process()
if('@'. 'php_dir'.'@'=='@'. 'php_dir'.'@')
Definition helper.inc:2
global $bearsamppConfig
Definition homepage.php:46
$imagesPath
Definition homepage.php:57
const QUICKPICK_JSON_URL
Definition root.php:37
const QUICKPICK_API_URL
Definition root.php:34
const QUICKPICK_API_KEY
Definition root.php:33