Bearsampp 2026.3.26
API documentation
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 = $bearsamppCore->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 Util::logDebug('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 Util::logError( '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 Util::logError( '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 Util::logDebug( '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 Util::logDebug( '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 Util::logError( 'Invalid entry format in JSON data' );
313 }
314 }
315
316 if ( empty( $versions ) ) {
317 Util::logError( 'No versions found' );
318
319 return ['error' => 'No versions found'];
320 }
321
322 Util::logDebug( '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 Util::logDebug( 'getModuleUrl called for module: ' . $module . ' version: ' . $version );
344 $url = trim( $this->versions['module-' . strtolower( $module )][$version]['url'] );
345 if ( $url <> '' ) {
346 Util::logDebug( 'Found URL for version: ' . $version . ' URL: ' . $url );
347
348 return $url;
349 }
350 else {
351 Util::logError( '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 Util::logDebug( 'checkDownloadId method called.' );
377
378 // Ensure the global config is available
379 if ( !isset( $bearsamppConfig ) ) {
380 Util::logError( 'Global configuration is not set.' );
381
382 return false;
383 }
384
385 $DownloadId = $bearsamppConfig->getDownloadId();
386 Util::logDebug( 'DownloadId is: ' . $DownloadId );
387
388 // Ensure the license key is not empty
389 if ( empty( $DownloadId ) ) {
390 Util::logError( 'License key is empty.' );
391
392 return false;
393 }
394
395 $url = QUICKPICK_API_URL . QUICKPICK_API_KEY . '&download_id=' . $DownloadId;
396 Util::logDebug( '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 Util::logError( 'Failed to validate QuickPick license - API server unavailable' );
406 return false;
407 }
408
409 Util::logDebug( '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 Util::logError( '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 Util::logDebug( 'License key valid: ' . $DownloadId );
423
424 return true;
425 }
426
427 Util::logError( '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 Util::logError( 'Module URL not found for module: ' . $module . ' version: ' . $version );
453
454 return ['error' => 'Module URL not found'];
455 }
456
457 if ( empty( $moduleUrl ) ) {
458 Util::logError( 'Module URL not found for module: ' . $module . ' version: ' . $version );
459
460 return ['error' => 'Module URL not found'];
461 }
462
463 $state = Util::checkInternetState();
464 if ( $state ) {
465 $response = $this->fetchAndUnzipModule( $moduleUrl, $module );
466 Util::logDebug( 'Response is: ' . print_r( $response, true ) );
467
468 // Check if enhanced mode is enabled
469 global $bearsamppConfig;
470 $enhancedMode = $bearsamppConfig->getEnhancedQuickPick();
471
472 Util::logDebug('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 Util::logDebug('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 Util::logDebug('Config updated successfully, triggering reload to apply changes...');
483
484 // Send progress update to user - temporarily stop output buffering
485 $obLevel = ob_get_level();
486 while (ob_get_level() > 0) {
487 ob_end_flush();
488 }
489
490 echo json_encode(['phase' => 'updating', 'message' => 'Updating system configuration...']);
491 flush();
492
493 // Restart output buffering
494 for ($i = 0; $i < $obLevel; $i++) {
495 ob_start();
496 }
497
498 // Note: User must manually reload from tray menu to activate the new version
499 Util::logDebug('Installation complete - user must manually reload from tray menu');
500 $response['reload_required'] = true;
501 } else {
502 Util::logError('Config update failed for module: ' . $module);
503 $response['reload_triggered'] = false;
504 }
505 } else if (isset($response['success']) && $enhancedMode == 0) {
506 Util::logDebug('Enhanced mode disabled - skipping config update');
507 }
508
509 return $response;
510 }
511 else {
512 Util::logError( 'No internet connection available.' );
513
514 return ['error' => 'No internet connection'];
515 }
516 }
517
526 public function fetchAndUnzipModule(string $moduleUrl, string $module): array
527{
528 Util::logDebug("$module is: " . $module);
529
531 $tmpDir = $bearsamppRoot->getTmpPath();
532 Util::logDebug('Temporary Directory: ' . $tmpDir);
533
534 $fileName = basename($moduleUrl);
535 Util::logDebug('File Name: ' . $fileName);
536
537 $tmpFilePath = $tmpDir . '/' . $fileName;
538 Util::logDebug('File Path: ' . $tmpFilePath);
539
540 $moduleName = str_replace('module-', '', $module);
541 Util::logDebug('Module Name: ' . $moduleName);
542
543 // Find the correct module key by searching through the modules array
544 // This handles proper capitalization for all module types
545 $moduleKey = null;
546 foreach ($this->modules as $key => $moduleInfo) {
547 if (strtolower($key) === strtolower($moduleName)) {
548 $moduleKey = $key;
549 break;
550 }
551 }
552
553 if (!$moduleKey) {
554 Util::logError("Module not found in modules array: $moduleName");
555 return ['error' => 'Module configuration not found'];
556 }
557
558 $moduleType = $this->modules[$moduleKey]['type'];
559 Util::logDebug('Module Type: ' . $moduleType);
560
561 // Get module type
562 $destination = $this->getModuleDestinationPath($moduleType, $moduleName);
563 Util::logDebug('Destination: ' . $destination);
564
565 // Retrieve the file path from the URL using the bearsamppCore module,
566 // passing the module URL and temporary file path, with the use Progress Bar parameter set to true.
567 $result = $bearsamppCore->getFileFromUrl($moduleUrl, $tmpFilePath, true);
568
569 // Check if $result is false
570 if ($result === false) {
571 Util::logError('Failed to retrieve file from URL: ' . $moduleUrl);
572 return ['error' => 'Failed to retrieve file from URL'];
573 }
574
575 // Determine the file extension and call the appropriate unzipping function
576 $fileExtension = pathinfo($tmpFilePath, PATHINFO_EXTENSION);
577 Util::logDebug('File extension: ' . $fileExtension);
578
579 if ($fileExtension === '7z' || $fileExtension === 'zip') {
580 // Send phase indicator for extraction
581 echo json_encode(['phase' => 'extracting']);
582 if (ob_get_length()) {
583 ob_flush();
584 }
585 flush();
586
587 $unzipResult = $bearsamppCore->unzipFile($tmpFilePath, $destination, function ($currentPercentage) {
588 echo json_encode(['progress' => "$currentPercentage%"]);
589 if (ob_get_length()) {
590 ob_flush();
591 }
592 flush();
593 });
594
595 if ($unzipResult === false) {
596 return ['error' => 'Failed to unzip file. File: ' . $tmpFilePath . ' could not be unzipped', 'Destination: ' . $destination];
597 }
598 } else {
599 Util::logError('Unsupported file extension: ' . $fileExtension);
600 return ['error' => 'Unsupported file extension'];
601 }
602
603 return ['success' => 'Module installed successfully'];
604}
605
618 public function getModuleDestinationPath(string $moduleType, string $moduleName)
619 {
620 global $bearsamppRoot;
621 if ( $moduleType === 'application' ) {
622 $destination = $bearsamppRoot->getAppsPath() . '/' . strtolower( $moduleName ) . '/';
623 }
624 elseif ( $moduleType === 'binary' ) {
625 $destination = $bearsamppRoot->getBinPath() . '/' . strtolower( $moduleName ) . '/';
626 }
627 elseif ( $moduleType === 'tools' ) {
628 $destination = $bearsamppRoot->getToolsPath() . '/' . strtolower( $moduleName ) . '/';
629 }
630 else {
631 $destination = '';
632 }
633
634 return $destination;
635 }
636
643 private function regenerateMenuSafe(): string
644 {
645 Util::logDebug('Regenerating menu (AJAX-safe mode)...');
646
647 // Suppress errors temporarily during menu generation
648 $oldErrorReporting = error_reporting();
649 error_reporting($oldErrorReporting & ~E_WARNING);
650
651 try {
652 // Generate the menu content
653 $menuContent = TplApp::process();
654
655 // Restore error reporting
656 error_reporting($oldErrorReporting);
657
658 Util::logDebug('Menu regenerated successfully');
659 return $menuContent;
660
661 } catch (Exception $e) {
662 // Restore error reporting
663 error_reporting($oldErrorReporting);
664
665 Util::logWarning('Error during menu regeneration: ' . $e->getMessage());
666 throw $e;
667 }
668 }
669
679 private function updateModuleConfig(string $module, string $version): bool
680 {
681 try {
682 $bearsamppConfig = new Config();
683
684 // Remove 'module-' prefix if present and normalize the module name
685 $moduleName = str_replace('module-', '', $module);
686
687 // Find the correct module key by searching through the modules array
688 // This handles proper capitalization for all module types
689 $moduleKey = null;
690 foreach ($this->modules as $key => $moduleInfo) {
691 if (strtolower($key) === strtolower($moduleName)) {
692 $moduleKey = $key;
693 break;
694 }
695 }
696
697 if (!$moduleKey) {
698 Util::logError("Module not found in modules array: $moduleName");
699 return false;
700 }
701
702 $moduleType = $this->modules[$moduleKey]['type'];
703
704 // Map module names to their config section names
705 // For all types, use the lowercase name for the config key
706 $configSection = strtolower($moduleKey);
707
708 Util::logDebug("Updating config for module: $module (key: $moduleKey, type: $moduleType) to version: $version");
709 Util::logDebug("Config section: $configSection");
710
711 // Update the configuration file
712 // The Config class expects a flat key like "nodejsVersion" not a section
713 $configKey = $configSection . 'Version';
714 $bearsamppConfig->replace($configKey, $version);
715
716 Util::logInfo("Successfully updated $configSection version to $version in bearsampp.conf");
717
718 return true;
719
720 } catch (Exception $e) {
721 Util::logError("Failed to update module config: " . $e->getMessage());
722 return false;
723 }
724 }
725
733 public function getErrorModal(string $errorMessage): string
734 {
735 ob_start();
736 ?>
737 <div id="configErrorContainer" class="text-center mt-3 pe-3">
738 <div class="alert alert-danger d-inline-block" role="alert" style="max-width: 500px;">
739 <h4 class="alert-heading">
740 <i class="fas fa-exclamation-circle"></i> Configuration Error
741 </h4>
742 <hr>
743 <p class="mb-0">
744 <?php echo htmlspecialchars($errorMessage); ?>
745 </p>
746 <hr>
747 <small class="text-muted">
748 Please add the missing parameter to the <code>bearsampp.conf</code> file in the Bearsampp root directory.
749 </small>
750 </div>
751 </div>
752 <?php
753 return ob_get_clean();
754 }
755
770 public function getQuickpickMenu(array $modules, array $versions, string $imagesPath): string
771 {
772 global $bearsamppConfig;
773 $includePr = $bearsamppConfig->getIncludePr();
774 $enhancedMode = $bearsamppConfig->getEnhancedQuickPick();
775
776 ob_start();
777 if ( Util::checkInternetState() ) {
778
779 // Check if the license key is valid
780 if ( $this->checkDownloadId() ): ?>
781 <div id = 'quickPickContainer'>
782 <div class = 'quickpick'>
783
784 <div class = "custom-select">
785 <button class = "select-button" role = "combobox"
786 aria-label = "select button"
787 aria-haspopup = "listbox"
788 aria-expanded = "false"
789 aria-controls = "select-dropdown">
790 <span class = "selected-value">Select a module and version</span>
791 <span class = "arrow"></span>
792 </button>
793 <ul class = "select-dropdown" role = "listbox" id = "select-dropdown">
794
795 <?php
796 foreach ( $modules as $module ): ?>
797 <?php if ( is_string( $module ) ): ?>
798 <li role = "option" class = "moduleheader">
799 <?php echo htmlspecialchars( $module ); ?>
800 </li>
801
802 <?php
803 foreach ( $versions['module-' . strtolower( $module )] as $version_array ):
804 // Skip prerelease versions if includePr is not enabled
805 if (isset($version_array['prerelease']) && $version_array['prerelease'] === true && $includePr != 1) {
806 continue;
807 }
808 ?>
809 <li role = "option" class = "moduleoption"
810 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>-li">
811 <input type = "radio"
812 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>"
813 name = "module" data-module = "<?php echo htmlspecialchars( $module ); ?>"
814 data-value = "<?php echo htmlspecialchars( $version_array['version'] ); ?>">
815 <label
816 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>
817 </li>
818 <?php endforeach; ?>
819 <?php endif; ?>
820 <?php endforeach; ?>
821 </ul>
822 </div>
823 </div>
824 <div class = "progress " id = "progress" tabindex = "-1" style = "width:260px;display:none"
825 aria-labelledby = "progressbar" aria-hidden = "true">
826 <div class = "progress-bar progress-bar-striped progress-bar-animated" id = "progress-bar" role = "progressbar" aria-valuenow = "0" aria-valuemin = "0"
827 aria-valuemax = "100" data-module = "Module"
828 data-version = "0.0.0">0%
829 </div>
830 <div id = "download-module" style = "display: none">ModuleName</div>
831 <div id = "download-version" style = "display: none">Version</div>
832 </div>
833 </div>
834 <?php else: ?>
835 <div id = "subscribeContainer" class = "text-center mt-3 pe-3">
836 <a href = "<?php echo Util::getWebsiteUrl( 'subscribe' ); ?>" class = "btn btn-dark d-inline-flex align-items-center">
837 <img src = "<?php echo $imagesPath . 'subscribe.svg'; ?>" alt = "Subscribe Icon" class = "me-2">
838 Subscribe to QuickPick now
839 </a>
840 </div>
841 <?php endif;
842 }
843 else {
844 ?>
845 <div id = "InternetState" class = "text-center mt-3 pe-3">
846 <img src = "<?php echo $imagesPath . 'no-wifi-icon.svg'; ?>" alt = "No Wifi Icon" class = "me-2">
847 <span>No internet present</span>
848 </div>
849 <?php
850 }
851
852 return ob_get_clean();
853 }
854}
$result
global $bearsamppRoot
global $bearsamppCore
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()
static logError($data, $file=null)
static logInfo($data, $file=null)
static logDebug($data, $file=null)
static getWebsiteUrl($path='', $fragment='', $utmSource=true)
static checkInternetState()
static logWarning($data, $file=null)
global $bearsamppConfig
Definition homepage.php:41
$enhancedMode
Definition homepage.php:169
$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