Bearsampp 2026.7.11
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
69 public function __construct()
70 {
71 global $bearsamppCore;
72 $this->jsonFilePath = Path::getResourcesPath() . '/quickpick-releases.json';
73 }
74
82 private function formatVersionLabel($version, $isPrerelease = false) {
83 global $bearsamppConfig;
84 $includePr = $bearsamppConfig->getIncludePr();
85
86 if ($isPrerelease && $includePr == 1) {
87 return '<span class="text-danger">' . htmlspecialchars($version) . ' PR</span>';
88 }
89
90 return htmlspecialchars($version);
91 }
92
100 public function normalizeModuleName(string $moduleName): ?string
101 {
102 // Remove 'module-' prefix if present
103 $moduleName = str_replace('module-', '', $moduleName);
104
105 // Find the correct module key by searching through the modules array
106 // This handles proper capitalization for all module types
107 foreach ($this->modules as $key => $moduleInfo) {
108 if (strtolower($key) === strtolower($moduleName)) {
109 return $key;
110 }
111 }
112
113 return null;
114 }
115
121 public function getModules(): array
122 {
123 return array_keys( $this->modules );
124 }
125
135 public function loadQuickpick(string $imagesPath): string
136 {
137 global $bearsamppConfig;
138
139 // Validate EnhancedQuickPick parameter
140 $validation = $bearsamppConfig->validateEnhancedQuickPick();
141 if (!$validation['valid']) {
142 return $this->getErrorModal($validation['error']);
143 }
144
145 $this->checkQuickpickJson();
146
147 $modules = $this->getModules();
148 $versions = $this->getVersions();
149
151 }
152
164 public function checkQuickpickJson()
165 {
166 global $bearsamppConfig;
167
168 // Determine local file creation time or rebuild if missing
169 $localFileCreationTime = $this->getLocalFileCreationTime();
170
171 // Attempt to retrieve remote file headers
172 $headers = get_headers(QUICKPICK_JSON_URL, 1);
173 if (!$this->isValidHeaderResponse($headers)) {
174 // If headers or Date are invalid, assume no update needed
175 return false;
176 }
177
178 // Optionally log headers for verbose output
179 $this->logHeaders($headers);
180
181 // Compare the creation times (remote vs. local)
182 $remoteFileCreationTime = strtotime(isset($headers['Date']) ? $headers['Date'] : '');
183 if ($remoteFileCreationTime > $localFileCreationTime) { return $this->rebuildQuickpickJson(); }
184
185 // Return false if local file is already up-to-date
186 return false;
187 }
188
194 private function getLocalFileCreationTime()
195 {
196 if (!file_exists($this->jsonFilePath)) {
197 // If local file is missing, rebuild it immediately
198 $this->rebuildQuickpickJson();
199 return 0;
200 }
201 return filectime($this->jsonFilePath);
202 }
203
210 private function isValidHeaderResponse($headers): bool
211 {
212 // If headers retrieval failed or Date is not set, return false
213 if ($headers === false || !isset($headers['Date'])) {
214 return false;
215 }
216 return true;
217 }
218
224 private function logHeaders(array $headers): void
225 {
226 global $bearsamppConfig;
227
228 if ($bearsamppConfig->getLogsVerbose() === 2) {
229 Log::debug('Headers: ' . print_r($headers, true));
230 }
231 }
232
238 public function getQuickpickJson(): array
239 {
240 $content = @file_get_contents( $this->jsonFilePath );
241 if ( $content === false ) {
242 Log::error( 'Error fetching content from JSON file: ' . $this->jsonFilePath );
243
244 return ['error' => 'Error fetching JSON file'];
245 }
246
247 $data = json_decode( $content, true );
248 if ( json_last_error() !== JSON_ERROR_NONE ) {
249 Log::error( 'Error decoding JSON content: ' . json_last_error_msg() );
250
251 return ['error' => 'Error decoding JSON content'];
252 }
253
254 return $data;
255 }
256
263 public function rebuildQuickpickJson(): array
264 {
265 Log::debug( 'Fetching JSON file: ' . $this->jsonFilePath );
266
267 // Fetch the JSON content from the URL
268 $jsonContent = file_get_contents( QUICKPICK_JSON_URL );
269
270 if ( $jsonContent === false ) {
271 // Handle error if the file could not be fetched
272 throw new Exception( 'Failed to fetch JSON content from the URL.' );
273 }
274
275 // Save the JSON content to the specified path
276 $result = file_put_contents( $this->jsonFilePath, $jsonContent );
277
278 if ( $result === false ) {
279 // Handle error if the file could not be saved
280 throw new Exception( 'Failed to save JSON content to the specified path.' );
281 }
282
283 // Return success message
284 return ['success' => 'JSON content fetched and saved successfully'];
285 }
286
295 public function getVersions(): array
296 {
297 Log::debug( 'Versions called' );
298
299 $versions = [];
300
301 $jsonData = $this->getQuickpickJson();
302
303 foreach ( $jsonData as $entry ) {
304 if ( is_array( $entry ) ) {
305 if ( isset( $entry['module'] ) && is_string( $entry['module'] ) ) {
306 if ( isset( $entry['versions'] ) && is_array( $entry['versions'] ) ) {
307 $versions[$entry['module']] = array_column( $entry['versions'], null, 'version' );
308 }
309 }
310 }
311 else {
312 Log::error( 'Invalid entry format in JSON data' );
313 }
314 }
315
316 if ( empty( $versions ) ) {
317 Log::error( 'No versions found' );
318
319 return ['error' => 'No versions found'];
320 }
321
322 Log::debug( 'Found versions' );
323
324 $this->versions = $versions;
325
326 return $versions;
327 }
328
340 public function getModuleUrl(string $module, string $version)
341 {
342 $this->getVersions();
343 Log::debug( 'getModuleUrl called for module: ' . $module . ' version: ' . $version );
344 $url = trim( $this->versions['module-' . strtolower( $module )][$version]['url'] );
345 if ( $url <> '' ) {
346 Log::debug( 'Found URL for version: ' . $version . ' URL: ' . $url );
347
348 return $url;
349 }
350 else {
351 Log::error( 'Version not found: ' . $version );
352
353 return ['error' => 'Version not found'];
354 }
355 }
356
372 public function checkDownloadId(): bool
373 {
374 global $bearsamppConfig;
375
376 Log::debug( 'checkDownloadId method called.' );
377
378 // Ensure the global config is available
379 if ( !isset( $bearsamppConfig ) ) {
380 Log::error( 'Global configuration is not set.' );
381
382 return false;
383 }
384
385 $DownloadId = $bearsamppConfig->getDownloadId();
386 Log::debug( 'DownloadId is: ' . $DownloadId );
387
388 // Ensure the license key is not empty
389 if ( empty( $DownloadId ) ) {
390 Log::error( 'License key is empty.' );
391
392 return false;
393 }
394
395 $url = QUICKPICK_API_URL . QUICKPICK_API_KEY . '&download_id=' . $DownloadId;
396 Log::debug( 'API URL: ' . $url );
397
398 // Attempt to fetch the API response
399 // Note: If this fails, PHP will generate a warning which will be logged by the error handler
400 // This is expected behavior when the API server is unavailable
401 $response = file_get_contents( $url );
402
403 // Check if the response is false
404 if ( $response === false ) {
405 Log::error( 'Failed to validate QuickPick license - API server unavailable' );
406 return false;
407 }
408
409 Log::debug( 'API response: ' . $response );
410
411 $data = json_decode( $response, true );
412
413 // Check if the JSON decoding was successful
414 if ( json_last_error() !== JSON_ERROR_NONE ) {
415 Log::error( 'Error decoding JSON response: ' . json_last_error_msg() );
416
417 return false;
418 }
419
420 // Validate the response data
421 if ( isset( $data['success'] ) && $data['success'] === true && isset( $data['data'] ) && is_array( $data['data'] ) && count( $data['data'] ) > 0 ) {
422 Log::debug( 'License key valid: ' . $DownloadId );
423
424 return true;
425 }
426
427 Log::error( 'Invalid license key: ' . $DownloadId );
428
429 return false;
430 }
431
446 public function installModule(string $module, string $version): array
447 {
448 // Find the module URL and module name from the data
449 $moduleUrl = $this->getModuleUrl( $module, $version );
450
451 if ( is_array( $moduleUrl ) && isset( $moduleUrl['error'] ) ) {
452 Log::error( 'Module URL not found for module: ' . $module . ' version: ' . $version );
453
454 return ['error' => 'Module URL not found'];
455 }
456
457 if ( empty( $moduleUrl ) ) {
458 Log::error( 'Module URL not found for module: ' . $module . ' version: ' . $version );
459
460 return ['error' => 'Module URL not found'];
461 }
462
464 if ( $state ) {
465 $response = $this->fetchAndUnzipModule( $moduleUrl, $module );
466 Log::debug( 'Response is: ' . print_r( $response, true ) );
467
468 // Check if enhanced mode is enabled
469 global $bearsamppConfig;
470 $enhancedMode = $bearsamppConfig->getEnhancedQuickPick();
471
472 Log::debug('Enhanced mode: ' . ($enhancedMode ? 'enabled' : 'disabled'));
473
474 // If installation was successful and enhanced mode is enabled, update config
475 if (isset($response['success']) && $enhancedMode == 1) {
476 // Step 1: Update config FIRST (so reload can pick up the new version)
477 Log::debug('Enhanced mode enabled - Updating config for module: ' . $module . ' version: ' . $version);
478 $configUpdated = $this->updateModuleConfig($module, $version);
479
480 if ($configUpdated) {
481 // Step 2: Trigger reload AFTER config update (reload will apply the new version)
482 Log::debug('Config updated successfully, triggering reload to apply changes...');
483
484 // Send progress update to user - flush output
485 if (ob_get_level() > 0) {
486 ob_flush();
487 }
488 echo json_encode(['phase' => 'updating', 'message' => 'Updating system configuration...']) . PHP_EOL;
489 flush();
490
491 // Note: User must manually reload from tray menu to activate the new version
492 Log::debug('Installation complete - user must manually reload from tray menu');
493 $response['reload_required'] = true;
494
495 // Clear both disk and memory caches to ensure the UI shows correct icons and labels
496 Log::debug('Clearing caches after module installation...');
498 } else {
499 Log::error('Config update failed for module: ' . $module);
500 $response['reload_triggered'] = false;
501 }
502 } else if (isset($response['success']) && $enhancedMode == 0) {
503 Log::debug('Enhanced mode disabled - skipping config update');
504
505 // Even if not updating config, clear cache to be safe as new files were added
506 Log::debug('Clearing caches after module installation (Standard mode)...');
508 }
509
510 return $response;
511 }
512 else {
513 Log::error( 'No internet connection available.' );
514
515 return ['error' => 'No internet connection'];
516 }
517 }
518
527 public function fetchAndUnzipModule(string $moduleUrl, string $module): array
528{
529 Log::debug("$module is: " . $module);
530
532 $tmpDir = Path::getTmpPath();
533 Log::debug('Temporary Directory: ' . $tmpDir);
534
535 $fileName = basename($moduleUrl);
536 Log::debug('File Name: ' . $fileName);
537
538 $tmpFilePath = $tmpDir . '/' . $fileName;
539 Log::debug('File Path: ' . $tmpFilePath);
540
541 $moduleName = str_replace('module-', '', $module);
542 Log::debug('Module Name: ' . $moduleName);
543
544 // Find the correct module key by searching through the modules array
545 // This handles proper capitalization for all module types
546 $moduleKey = null;
547 foreach ($this->modules as $key => $moduleInfo) {
548 if (strtolower($key) === strtolower($moduleName)) {
549 $moduleKey = $key;
550 break;
551 }
552 }
553
554 if (!$moduleKey) {
555 Log::error("Module not found in modules array: $moduleName");
556 return ['error' => 'Module configuration not found'];
557 }
558
559 $moduleType = $this->modules[$moduleKey]['type'];
560 Log::debug('Module Type: ' . $moduleType);
561
562 // Get module type
563 $destination = $this->getModuleDestinationPath($moduleType, $moduleName);
564 Log::debug('Destination: ' . $destination);
565
566 // Retrieve the file path from the URL using the bearsamppCore module,
567 // passing the module URL and temporary file path, with the use Progress Bar parameter set to true.
568 $result = $bearsamppCore->getFileFromUrl($moduleUrl, $tmpFilePath, true);
569
570 // Check if $result is false
571 if ($result === false) {
572 Log::error('Failed to retrieve file from URL: ' . $moduleUrl);
573 return ['error' => 'Failed to retrieve file from URL'];
574 }
575
576 // Determine the file extension and call the appropriate unzipping function
577 $fileExtension = pathinfo($tmpFilePath, PATHINFO_EXTENSION);
578 Log::debug('File extension: ' . $fileExtension);
579
580 if ($fileExtension === '7z' || $fileExtension === 'zip') {
581 echo json_encode(['phase' => 'extracting']) . PHP_EOL;
582 if (ob_get_length()) {
583 ob_flush();
584 }
585 flush();
586
587 $unzipResult = $bearsamppCore->unzipFile($tmpFilePath, $destination, function ($currentPercentage) {
588 $progressStr = is_numeric($currentPercentage) ? "$currentPercentage%" : $currentPercentage;
589 echo json_encode(['progress' => $progressStr]) . PHP_EOL;
590 if (ob_get_length()) {
591 ob_flush();
592 }
593 flush();
594 });
595
596 if ($unzipResult === false) {
597 return ['error' => 'Failed to unzip file. File: ' . $tmpFilePath . ' could not be unzipped', 'Destination: ' . $destination];
598 }
599 } else {
600 Log::error('Unsupported file extension: ' . $fileExtension);
601 return ['error' => 'Unsupported file extension'];
602 }
603
604 return ['success' => 'Module installed successfully'];
605}
606
619 public function getModuleDestinationPath(string $moduleType, string $moduleName)
620 {
621 global $bearsamppRoot;
622 if ( $moduleType === 'application' ) {
623 $destination = Path::getAppsPath() . '/' . strtolower( $moduleName ) . '/';
624 }
625 elseif ( $moduleType === 'binary' ) {
626 $destination = Path::getBinPath() . '/' . strtolower( $moduleName ) . '/';
627 }
628 elseif ( $moduleType === 'tools' ) {
629 $destination = Path::getToolsPath() . '/' . strtolower( $moduleName ) . '/';
630 }
631 else {
632 $destination = '';
633 }
634
635 return $destination;
636 }
637
644 private function regenerateMenuSafe(): string
645 {
646 Log::debug('Regenerating menu (AJAX-safe mode)...');
647
648 // Suppress errors temporarily during menu generation
649 $oldErrorReporting = error_reporting();
650 error_reporting($oldErrorReporting & ~E_WARNING);
651
652 try {
653 // Generate the menu content
654 $menuContent = TplApp::process();
655
656 // Restore error reporting
657 error_reporting($oldErrorReporting);
658
659 Log::debug('Menu regenerated successfully');
660 return $menuContent;
661
662 } catch (Exception $e) {
663 // Restore error reporting
664 error_reporting($oldErrorReporting);
665
666 Log::warning('Error during menu regeneration: ' . $e->getMessage());
667 throw $e;
668 }
669 }
670
680 private function updateModuleConfig(string $module, string $version): bool
681 {
682 try {
683 $bearsamppConfig = new Config();
684
685 // Remove 'module-' prefix if present and normalize the module name
686 $moduleName = str_replace('module-', '', $module);
687
688 // Find the correct module key by searching through the modules array
689 // This handles proper capitalization for all module types
690 $moduleKey = null;
691 foreach ($this->modules as $key => $moduleInfo) {
692 if (strtolower($key) === strtolower($moduleName)) {
693 $moduleKey = $key;
694 break;
695 }
696 }
697
698 if (!$moduleKey) {
699 Log::error("Module not found in modules array: $moduleName");
700 return false;
701 }
702
703 $moduleType = $this->modules[$moduleKey]['type'];
704
705 // Map module names to their config section names
706 // For all types, use the lowercase name for the config key
707 $configSection = strtolower($moduleKey);
708
709 Log::debug("Updating config for module: $module (key: $moduleKey, type: $moduleType) to version: $version");
710 Log::debug("Config section: $configSection");
711
712 // Update the configuration file
713 // The Config class expects a flat key like "nodejsVersion" not a section
714 $configKey = $configSection . 'Version';
715 $bearsamppConfig->replace($configKey, $version);
716
717 Log::info("Successfully updated $configSection version to $version in bearsampp.conf");
718
719 return true;
720
721 } catch (Exception $e) {
722 Log::error("Failed to update module config: " . $e->getMessage());
723 return false;
724 }
725 }
726
734 public function getErrorModal(string $errorMessage): string
735 {
736 ob_start();
737 ?>
738 <div id="configErrorContainer" class="text-center mt-3 pe-3">
739 <div class="alert alert-danger d-inline-block" role="alert" style="max-width: 500px;">
740 <h4 class="alert-heading">
741 <i class="fas fa-exclamation-circle"></i> Configuration Error
742 </h4>
743 <hr>
744 <p class="mb-0">
745 <?php echo htmlspecialchars($errorMessage); ?>
746 </p>
747 <hr>
748 <small class="text-muted">
749 Please add the missing parameter to the <code>bearsampp.conf</code> file in the Bearsampp root directory.
750 </small>
751 </div>
752 </div>
753 <?php
754 return ob_get_clean();
755 }
756
771 public function getQuickpickMenu(array $modules, array $versions, string $imagesPath): string
772 {
773 global $bearsamppConfig;
774 $includePr = $bearsamppConfig->getIncludePr();
775 $enhancedMode = $bearsamppConfig->getEnhancedQuickPick();
776
777 ob_start();
779
780 // Check if the license key is valid
781 if ( $this->checkDownloadId() ): ?>
782 <div class = "enhanced-mode-toggle">
783 <label class = "form-check-label me-2" for = "enhancedQuickPickSwitch">
784 Enhanced Mode
785 </label>
786 <div class = "form-check form-switch mb-0">
787 <input class = "form-check-input" type = "checkbox" role = "switch" id = "enhancedQuickPickSwitch"
788 <?php echo $enhancedMode == 1 ? 'checked' : ''; ?>
789 data-bs-toggle = "tooltip" data-bs-placement = "bottom"
790 title = "Toggle between enhanced (auto-config update) and standard QuickPick mode">
791 </div>
792 </div>
793 <div id = 'quickPickContainer'>
794 <div class = 'quickpick'>
795
796 <div class = "custom-select">
797 <button class = "select-button" role = "combobox"
798 aria-label = "select button"
799 aria-haspopup = "listbox"
800 aria-expanded = "false"
801 aria-controls = "select-dropdown">
802 <span class = "selected-value">Select a module and version</span>
803 <span class = "arrow"></span>
804 </button>
805 <ul class = "select-dropdown" role = "listbox" id = "select-dropdown">
806
807 <?php
808 foreach ( $modules as $module ): ?>
809 <?php if ( is_string( $module ) ): ?>
810 <li role = "option" class = "moduleheader">
811 <?php echo htmlspecialchars( $module ); ?>
812 </li>
813
814 <?php
815 foreach ( $versions['module-' . strtolower( $module )] as $version_array ):
816 // Skip prerelease versions if includePr is not enabled
817 if (isset($version_array['prerelease']) && $version_array['prerelease'] === true && $includePr != 1) {
818 continue;
819 }
820 ?>
821 <li role = "option" class = "moduleoption"
822 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>-li"
823 data-module = "<?php echo htmlspecialchars( $module ); ?>"
824 data-value = "<?php echo htmlspecialchars( $version_array['version'] ); ?>">
825 <input type = "radio"
826 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>"
827 name = "module" data-module = "<?php echo htmlspecialchars( $module ); ?>"
828 data-value = "<?php echo htmlspecialchars( $version_array['version'] ); ?>">
829 <label
830 for = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>"><?php echo $this->formatVersionLabel( $version_array['version'], isset($version_array['prerelease']) && $version_array['prerelease'] === true ); ?></label>
831 </li>
832 <?php endforeach; ?>
833 <?php endif; ?>
834 <?php endforeach; ?>
835 </ul>
836 </div>
837 </div>
838 <div class = "progress " id = "progress" tabindex = "-1" style = "width:260px;display:none"
839 aria-labelledby = "progressbar" aria-hidden = "true">
840 <div class = "progress-bar progress-bar-striped progress-bar-animated" id = "progress-bar" role = "progressbar" aria-valuenow = "0" aria-valuemin = "0"
841 aria-valuemax = "100" data-module = "Module"
842 data-version = "0.0.0">0%
843 </div>
844 <div id = "download-module" style = "display: none">ModuleName</div>
845 <div id = "download-version" style = "display: none">Version</div>
846 </div>
847 </div>
848 <?php else: ?>
849 <div id = "subscribeContainer" class = "text-center">
850 <a href = "<?php echo HttpClient::getWebsiteUrl( 'subscribe' ); ?>" class = "btn btn-dark d-inline-flex align-items-center">
851 <img src = "<?php echo $imagesPath . 'subscribe.svg'; ?>" alt = "Subscribe Icon" class = "me-2">
852 Subscribe to QuickPick now
853 </a>
854 </div>
855 <?php endif;
856 }
857 else {
858 ?>
859 <div id = "InternetState" class = "text-center">
860 <img src = "<?php echo $imagesPath . 'no-wifi-icon.svg'; ?>" alt = "No Wifi Icon" class = "me-2">
861 <span>No internet present</span>
862 </div>
863 <?php
864 }
865
866 return ob_get_clean();
867 }
868}
$result
global $bearsamppRoot
global $bearsamppCore
static getWebsiteUrl($path='', $fragment='', $utmSource=true)
static checkInternetState()
static info($data, $file=null)
static debug($data, $file=null)
static warning($data, $file=null)
static error($data, $file=null)
static getToolsPath($aetrayPath=false)
static getResourcesPath($aetrayPath=false)
static getBinPath($aetrayPath=false)
static getTmpPath($aetrayPath=false)
static getAppsPath($aetrayPath=false)
isValidHeaderResponse($headers)
fetchAndUnzipModule(string $moduleUrl, string $module)
getModuleUrl(string $module, string $version)
normalizeModuleName(string $moduleName)
getErrorModal(string $errorMessage)
installModule(string $module, string $version)
updateModuleConfig(string $module, string $version)
getModuleDestinationPath(string $moduleType, string $moduleName)
getQuickpickMenu(array $modules, array $versions, string $imagesPath)
formatVersionLabel($version, $isPrerelease=false)
logHeaders(array $headers)
loadQuickpick(string $imagesPath)
static process()
global $bearsamppConfig
Definition homepage.php:41
$imagesPath
Definition homepage.php:52
const QUICKPICK_JSON_URL
Definition root.php:27
const QUICKPICK_API_URL
Definition root.php:24
const QUICKPICK_API_KEY
Definition root.php:23