Bearsampp 2026.7.28
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: Launch the reload action to apply the new version automatically.
482 // QuickPick runs in the AJAX/web context, where the winbinder GUI used by
483 // the reload action is unavailable, so we spawn it as a detached process
484 // (the same "php-win.exe root.php reload" command the tray menu runs). The
485 // reload restarts the database services so clients such as phpMyAdmin pick
486 // up the new version without the user having to reload manually.
487 Log::debug('Config updated successfully, launching reload to apply changes...');
488
489 // Send progress update to user - flush output
490 if (ob_get_level() > 0) {
491 ob_flush();
492 }
493 echo json_encode(['phase' => 'updating', 'message' => 'Applying version changes...']) . PHP_EOL;
494 flush();
495
496 // Clear caches before the reload runs so it reads fresh values from disk
497 Log::debug('Clearing caches before reload...');
499
500 // Build and launch the reload command detached from this request.
501 // Leading "" is the (empty) window title required by cmd.exe "start".
502 $reloadCmd = '"" "' . Path::getPhpExe() . '" "'
503 . Path::getCorePath() . '/' . Core::isRoot_FILE . '" '
505 Log::debug('Launching reload command: ' . $reloadCmd);
506 CommandRunner::background($reloadCmd);
507
508 $response['reload_triggered'] = true;
509 } else {
510 Log::error('Config update failed for module: ' . $module);
511 $response['reload_triggered'] = false;
512 }
513 } else if (isset($response['success']) && $enhancedMode == 0) {
514 Log::debug('Enhanced mode disabled - skipping config update');
515
516 // Even if not updating config, clear cache to be safe as new files were added
517 Log::debug('Clearing caches after module installation (Standard mode)...');
519 }
520
521 return $response;
522 }
523 else {
524 Log::error( 'No internet connection available.' );
525
526 return ['error' => 'No internet connection'];
527 }
528 }
529
538 public function fetchAndUnzipModule(string $moduleUrl, string $module): array
539{
540 Log::debug("$module is: " . $module);
541
543 $tmpDir = Path::getTmpPath();
544 Log::debug('Temporary Directory: ' . $tmpDir);
545
546 $fileName = basename($moduleUrl);
547 Log::debug('File Name: ' . $fileName);
548
549 $tmpFilePath = $tmpDir . '/' . $fileName;
550 Log::debug('File Path: ' . $tmpFilePath);
551
552 $moduleName = str_replace('module-', '', $module);
553 Log::debug('Module Name: ' . $moduleName);
554
555 // Find the correct module key by searching through the modules array
556 // This handles proper capitalization for all module types
557 $moduleKey = null;
558 foreach ($this->modules as $key => $moduleInfo) {
559 if (strtolower($key) === strtolower($moduleName)) {
560 $moduleKey = $key;
561 break;
562 }
563 }
564
565 if (!$moduleKey) {
566 Log::error("Module not found in modules array: $moduleName");
567 return ['error' => 'Module configuration not found'];
568 }
569
570 $moduleType = $this->modules[$moduleKey]['type'];
571 Log::debug('Module Type: ' . $moduleType);
572
573 // Get module type
574 $destination = $this->getModuleDestinationPath($moduleType, $moduleName);
575 Log::debug('Destination: ' . $destination);
576
577 // Retrieve the file path from the URL using the bearsamppCore module,
578 // passing the module URL and temporary file path, with the use Progress Bar parameter set to true.
579 $result = $bearsamppCore->getFileFromUrl($moduleUrl, $tmpFilePath, true);
580
581 // Check if $result is false
582 if ($result === false) {
583 Log::error('Failed to retrieve file from URL: ' . $moduleUrl);
584 return ['error' => 'Failed to retrieve file from URL'];
585 }
586
587 // Determine the file extension and call the appropriate unzipping function
588 $fileExtension = pathinfo($tmpFilePath, PATHINFO_EXTENSION);
589 Log::debug('File extension: ' . $fileExtension);
590
591 if ($fileExtension === '7z' || $fileExtension === 'zip') {
592 echo json_encode(['phase' => 'extracting']) . PHP_EOL;
593 if (ob_get_length()) {
594 ob_flush();
595 }
596 flush();
597
598 $unzipResult = $bearsamppCore->unzipFile($tmpFilePath, $destination, function ($currentPercentage) {
599 $progressStr = is_numeric($currentPercentage) ? "$currentPercentage%" : $currentPercentage;
600 echo json_encode(['progress' => $progressStr]) . PHP_EOL;
601 if (ob_get_length()) {
602 ob_flush();
603 }
604 flush();
605 });
606
607 if ($unzipResult === false) {
608 return ['error' => 'Failed to unzip file. File: ' . $tmpFilePath . ' could not be unzipped', 'Destination: ' . $destination];
609 }
610 } else {
611 Log::error('Unsupported file extension: ' . $fileExtension);
612 return ['error' => 'Unsupported file extension'];
613 }
614
615 return ['success' => 'Module installed successfully'];
616}
617
630 public function getModuleDestinationPath(string $moduleType, string $moduleName)
631 {
632 global $bearsamppRoot;
633 if ( $moduleType === 'application' ) {
634 $destination = Path::getAppsPath() . '/' . strtolower( $moduleName ) . '/';
635 }
636 elseif ( $moduleType === 'binary' ) {
637 $destination = Path::getBinPath() . '/' . strtolower( $moduleName ) . '/';
638 }
639 elseif ( $moduleType === 'tools' ) {
640 $destination = Path::getToolsPath() . '/' . strtolower( $moduleName ) . '/';
641 }
642 else {
643 $destination = '';
644 }
645
646 return $destination;
647 }
648
655 private function regenerateMenuSafe(): string
656 {
657 Log::debug('Regenerating menu (AJAX-safe mode)...');
658
659 // Suppress errors temporarily during menu generation
660 $oldErrorReporting = error_reporting();
661 error_reporting($oldErrorReporting & ~E_WARNING);
662
663 try {
664 // Generate the menu content
665 $menuContent = TplApp::process();
666
667 // Restore error reporting
668 error_reporting($oldErrorReporting);
669
670 Log::debug('Menu regenerated successfully');
671 return $menuContent;
672
673 } catch (Exception $e) {
674 // Restore error reporting
675 error_reporting($oldErrorReporting);
676
677 Log::warning('Error during menu regeneration: ' . $e->getMessage());
678 throw $e;
679 }
680 }
681
691 private function updateModuleConfig(string $module, string $version): bool
692 {
693 try {
694 $bearsamppConfig = new Config();
695
696 // Remove 'module-' prefix if present and normalize the module name
697 $moduleName = str_replace('module-', '', $module);
698
699 // Find the correct module key by searching through the modules array
700 // This handles proper capitalization for all module types
701 $moduleKey = null;
702 foreach ($this->modules as $key => $moduleInfo) {
703 if (strtolower($key) === strtolower($moduleName)) {
704 $moduleKey = $key;
705 break;
706 }
707 }
708
709 if (!$moduleKey) {
710 Log::error("Module not found in modules array: $moduleName");
711 return false;
712 }
713
714 $moduleType = $this->modules[$moduleKey]['type'];
715
716 // Map module names to their config section names
717 // For all types, use the lowercase name for the config key
718 $configSection = strtolower($moduleKey);
719
720 Log::debug("Updating config for module: $module (key: $moduleKey, type: $moduleType) to version: $version");
721 Log::debug("Config section: $configSection");
722
723 // Update the configuration file
724 // The Config class expects a flat key like "nodejsVersion" not a section
725 $configKey = $configSection . 'Version';
726 $bearsamppConfig->replace($configKey, $version);
727
728 Log::info("Successfully updated $configSection version to $version in bearsampp.conf");
729
730 return true;
731
732 } catch (Exception $e) {
733 Log::error("Failed to update module config: " . $e->getMessage());
734 return false;
735 }
736 }
737
745 public function getErrorModal(string $errorMessage): string
746 {
747 ob_start();
748 ?>
749 <div id="configErrorContainer" class="text-center mt-3 pe-3">
750 <div class="alert alert-danger d-inline-block" role="alert" style="max-width: 500px;">
751 <h4 class="alert-heading">
752 <i class="fas fa-exclamation-circle"></i> Configuration Error
753 </h4>
754 <hr>
755 <p class="mb-0">
756 <?php echo htmlspecialchars($errorMessage); ?>
757 </p>
758 <hr>
759 <small class="text-muted">
760 Please add the missing parameter to the <code>bearsampp.conf</code> file in the Bearsampp root directory.
761 </small>
762 </div>
763 </div>
764 <?php
765 return ob_get_clean();
766 }
767
782 public function getQuickpickMenu(array $modules, array $versions, string $imagesPath): string
783 {
784 global $bearsamppConfig;
785 $includePr = $bearsamppConfig->getIncludePr();
786 $enhancedMode = $bearsamppConfig->getEnhancedQuickPick();
787
788 ob_start();
790
791 // Check if the license key is valid
792 if ( $this->checkDownloadId() ): ?>
793 <div class = "enhanced-mode-toggle">
794 <label class = "form-check-label me-2" for = "enhancedQuickPickSwitch">
795 Enhanced Mode
796 </label>
797 <div class = "form-check form-switch mb-0">
798 <input class = "form-check-input" type = "checkbox" role = "switch" id = "enhancedQuickPickSwitch"
799 <?php echo $enhancedMode == 1 ? 'checked' : ''; ?>
800 data-bs-toggle = "tooltip" data-bs-placement = "bottom"
801 title = "Toggle between enhanced (auto-config update) and standard QuickPick mode">
802 </div>
803 </div>
804 <div id = 'quickPickContainer'>
805 <div class = 'quickpick'>
806
807 <div class = "custom-select">
808 <button class = "select-button" role = "combobox"
809 aria-label = "select button"
810 aria-haspopup = "listbox"
811 aria-expanded = "false"
812 aria-controls = "select-dropdown">
813 <span class = "selected-value">Select a module and version</span>
814 <span class = "arrow"></span>
815 </button>
816 <ul class = "select-dropdown" role = "listbox" id = "select-dropdown">
817
818 <?php
819 foreach ( $modules as $module ): ?>
820 <?php if ( is_string( $module ) ): ?>
821 <li role = "option" class = "moduleheader">
822 <?php echo htmlspecialchars( $module ); ?>
823 </li>
824
825 <?php
826 foreach ( $versions['module-' . strtolower( $module )] as $version_array ):
827 // Skip prerelease versions if includePr is not enabled
828 if (isset($version_array['prerelease']) && $version_array['prerelease'] === true && $includePr != 1) {
829 continue;
830 }
831 ?>
832 <li role = "option" class = "moduleoption"
833 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>-li"
834 data-module = "<?php echo htmlspecialchars( $module ); ?>"
835 data-value = "<?php echo htmlspecialchars( $version_array['version'] ); ?>">
836 <input type = "radio"
837 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>"
838 name = "module" data-module = "<?php echo htmlspecialchars( $module ); ?>"
839 data-value = "<?php echo htmlspecialchars( $version_array['version'] ); ?>">
840 <label
841 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>
842 </li>
843 <?php endforeach; ?>
844 <?php endif; ?>
845 <?php endforeach; ?>
846 </ul>
847 </div>
848 </div>
849 <div class = "progress " id = "progress" tabindex = "-1" style = "width:260px;display:none"
850 aria-labelledby = "progressbar" aria-hidden = "true">
851 <div class = "progress-bar progress-bar-striped progress-bar-animated" id = "progress-bar" role = "progressbar" aria-valuenow = "0" aria-valuemin = "0"
852 aria-valuemax = "100" data-module = "Module"
853 data-version = "0.0.0">0%
854 </div>
855 <div id = "download-module" style = "display: none">ModuleName</div>
856 <div id = "download-version" style = "display: none">Version</div>
857 </div>
858 </div>
859 <?php else: ?>
860 <div id = "subscribeContainer" class = "text-center">
861 <a href = "<?php echo HttpClient::getWebsiteUrl( 'subscribe' ); ?>" class = "btn btn-dark d-inline-flex align-items-center">
862 <img src = "<?php echo $imagesPath . 'subscribe.svg'; ?>" alt = "Subscribe Icon" class = "me-2">
863 Subscribe to QuickPick now
864 </a>
865 </div>
866 <?php endif;
867 }
868 else {
869 ?>
870 <div id = "InternetState" class = "text-center">
871 <img src = "<?php echo $imagesPath . 'no-wifi-icon.svg'; ?>" alt = "No Wifi Icon" class = "me-2">
872 <span>No internet present</span>
873 </div>
874 <?php
875 }
876
877 return ob_get_clean();
878 }
879}
$result
global $bearsamppRoot
global $bearsamppCore
const RELOAD
static background(string $command)
const isRoot_FILE
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 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)
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