Bearsampp 2025.8.29
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 'ConsoleZ' => ['type' => 'tools'],
34 'Ghostscript' => ['type' => 'tools'],
35 'Git' => ['type' => 'tools'],
36 'Mailpit' => ['type' => 'binary'],
37 'MariaDB' => ['type' => 'binary'],
38 'Memcached' => ['type' => 'binary'],
39 'MySQL' => ['type' => 'binary'],
40 'Ngrok' => ['type' => 'tools'],
41 'NodeJS' => ['type' => 'binary'],
42 'Perl' => ['type' => 'tools'],
43 'PHP' => ['type' => 'binary'],
44 'PhpMyAdmin' => ['type' => 'application'],
45 'PhpPgAdmin' => ['type' => 'application'],
46 'PostgreSQL' => ['type' => 'binary'],
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
98 public function getModules(): array
99 {
100 return array_keys( $this->modules );
101 }
102
112 public function loadQuickpick(string $imagesPath): string
113 {
114 $this->checkQuickpickJson();
115
116 $modules = $this->getModules();
117 $versions = $this->getVersions();
118
120 }
121
133 public function checkQuickpickJson()
134 {
135 global $bearsamppConfig;
136
137 // Determine local file creation time or rebuild if missing
138 $localFileCreationTime = $this->getLocalFileCreationTime();
139
140 // Attempt to retrieve remote file headers
141 $headers = get_headers(QUICKPICK_JSON_URL, 1);
142 if (!$this->isValidHeaderResponse($headers)) {
143 // If headers or Date are invalid, assume no update needed
144 return false;
145 }
146
147 // Optionally log headers for verbose output
148 $this->logHeaders($headers);
149
150 // Compare the creation times (remote vs. local)
151 $remoteFileCreationTime = strtotime(isset($headers['Date']) ? $headers['Date'] : '');
152 if ($remoteFileCreationTime > $localFileCreationTime) { return $this->rebuildQuickpickJson(); }
153
154 // Return false if local file is already up-to-date
155 return false;
156 }
157
163 private function getLocalFileCreationTime()
164 {
165 if (!file_exists($this->jsonFilePath)) {
166 // If local file is missing, rebuild it immediately
167 $this->rebuildQuickpickJson();
168 return 0;
169 }
170 return filectime($this->jsonFilePath);
171 }
172
179 private function isValidHeaderResponse($headers): bool
180 {
181 // If headers retrieval failed or Date is not set, return false
182 if ($headers === false || !isset($headers['Date'])) {
183 return false;
184 }
185 return true;
186 }
187
193 private function logHeaders(array $headers): void
194 {
195 global $bearsamppConfig;
196
197 if ($bearsamppConfig->getLogsVerbose() === 2) {
198 Util::logDebug('Headers: ' . print_r($headers, true));
199 }
200 }
201
207 public function getQuickpickJson(): array
208 {
209 $content = @file_get_contents( $this->jsonFilePath );
210 if ( $content === false ) {
211 Util::logError( 'Error fetching content from JSON file: ' . $this->jsonFilePath );
212
213 return ['error' => 'Error fetching JSON file'];
214 }
215
216 $data = json_decode( $content, true );
217 if ( json_last_error() !== JSON_ERROR_NONE ) {
218 Util::logError( 'Error decoding JSON content: ' . json_last_error_msg() );
219
220 return ['error' => 'Error decoding JSON content'];
221 }
222
223 return $data;
224 }
225
232 public function rebuildQuickpickJson(): array
233 {
234 Util::logDebug( 'Fetching JSON file: ' . $this->jsonFilePath );
235
236 // Fetch the JSON content from the URL
237 $jsonContent = file_get_contents( QUICKPICK_JSON_URL );
238
239 if ( $jsonContent === false ) {
240 // Handle error if the file could not be fetched
241 throw new Exception( 'Failed to fetch JSON content from the URL.' );
242 }
243
244 // Save the JSON content to the specified path
245 $result = file_put_contents( $this->jsonFilePath, $jsonContent );
246
247 if ( $result === false ) {
248 // Handle error if the file could not be saved
249 throw new Exception( 'Failed to save JSON content to the specified path.' );
250 }
251
252 // Return success message
253 return ['success' => 'JSON content fetched and saved successfully'];
254 }
255
264 public function getVersions(): array
265 {
266 Util::logDebug( 'Versions called' );
267
268 $versions = [];
269
270 $jsonData = $this->getQuickpickJson();
271
272 foreach ( $jsonData as $entry ) {
273 if ( is_array( $entry ) ) {
274 if ( isset( $entry['module'] ) && is_string( $entry['module'] ) ) {
275 if ( isset( $entry['versions'] ) && is_array( $entry['versions'] ) ) {
276 $versions[$entry['module']] = array_column( $entry['versions'], null, 'version' );
277 }
278 }
279 }
280 else {
281 Util::logError( 'Invalid entry format in JSON data' );
282 }
283 }
284
285 if ( empty( $versions ) ) {
286 Util::logError( 'No versions found' );
287
288 return ['error' => 'No versions found'];
289 }
290
291 Util::logDebug( 'Found versions' );
292
293 $this->versions = $versions;
294
295 return $versions;
296 }
297
309 public function getModuleUrl(string $module, string $version)
310 {
311 $this->getVersions();
312 Util::logDebug( 'getModuleUrl called for module: ' . $module . ' version: ' . $version );
313 $url = trim( $this->versions['module-' . strtolower( $module )][$version]['url'] );
314 if ( $url <> '' ) {
315 Util::logDebug( 'Found URL for version: ' . $version . ' URL: ' . $url );
316
317 return $url;
318 }
319 else {
320 Util::logError( 'Version not found: ' . $version );
321
322 return ['error' => 'Version not found'];
323 }
324 }
325
341 public function checkDownloadId(): bool
342 {
343 global $bearsamppConfig;
344
345 Util::logDebug( 'checkDownloadId method called.' );
346
347 // Ensure the global config is available
348 if ( !isset( $bearsamppConfig ) ) {
349 Util::logError( 'Global configuration is not set.' );
350
351 return false;
352 }
353
354 $DownloadId = $bearsamppConfig->getDownloadId();
355 Util::logDebug( 'DownloadId is: ' . $DownloadId );
356
357 // Ensure the license key is not empty
358 if ( empty( $DownloadId ) ) {
359 Util::logError( 'License key is empty.' );
360
361 return false;
362 }
363
364 $url = QUICKPICK_API_URL . QUICKPICK_API_KEY . '&download_id=' . $DownloadId;
365 Util::logDebug( 'API URL: ' . $url );
366
367 $response = @file_get_contents( $url );
368
369 // Check if the response is false
370 if ( $response === false ) {
371 $error = error_get_last();
372 Util::logError( 'Error fetching API response: ' . $error['message'] );
373
374 return false;
375 }
376
377 Util::logDebug( 'API response: ' . $response );
378
379 $data = json_decode( $response, true );
380
381 // Check if the JSON decoding was successful
382 if ( json_last_error() !== JSON_ERROR_NONE ) {
383 Util::logError( 'Error decoding JSON response: ' . json_last_error_msg() );
384
385 return false;
386 }
387
388 // Validate the response data
389 if ( isset( $data['success'] ) && $data['success'] === true && isset( $data['data'] ) && is_array( $data['data'] ) && count( $data['data'] ) > 0 ) {
390 Util::logDebug( 'License key valid: ' . $DownloadId );
391
392 return true;
393 }
394
395 Util::logError( 'Invalid license key: ' . $DownloadId );
396
397 return false;
398 }
399
414 public function installModule(string $module, string $version): array
415 {
416 // Find the module URL and module name from the data
417 $moduleUrl = $this->getModuleUrl( $module, $version );
418
419 if ( is_array( $moduleUrl ) && isset( $moduleUrl['error'] ) ) {
420 Util::logError( 'Module URL not found for module: ' . $module . ' version: ' . $version );
421
422 return ['error' => 'Module URL not found'];
423 }
424
425 if ( empty( $moduleUrl ) ) {
426 Util::logError( 'Module URL not found for module: ' . $module . ' version: ' . $version );
427
428 return ['error' => 'Module URL not found'];
429 }
430
431 $state = Util::checkInternetState();
432 if ( $state ) {
433 $response = $this->fetchAndUnzipModule( $moduleUrl, $module );
434 Util::logDebug( 'Response is: ' . print_r( $response, true ) );
435
436 return $response;
437 }
438 else {
439 Util::logError( 'No internet connection available.' );
440
441 return ['error' => 'No internet connection'];
442 }
443 }
444
453 public function fetchAndUnzipModule(string $moduleUrl, string $module): array
454{
455 Util::logDebug("$module is: " . $module);
456
458 $tmpDir = $bearsamppRoot->getTmpPath();
459 Util::logDebug('Temporary Directory: ' . $tmpDir);
460
461 $fileName = basename($moduleUrl);
462 Util::logDebug('File Name: ' . $fileName);
463
464 $tmpFilePath = $tmpDir . '/' . $fileName;
465 Util::logDebug('File Path: ' . $tmpFilePath);
466
467 $moduleName = str_replace('module-', '', $module);
468 Util::logDebug('Module Name: ' . $moduleName);
469
470 $moduleType = $this->modules[$module]['type'];
471 Util::logDebug('Module Type: ' . $moduleType);
472
473 // Get module type
474 $destination = $this->getModuleDestinationPath($moduleType, $moduleName);
475 Util::logDebug('Destination: ' . $destination);
476
477 // Retrieve the file path from the URL using the bearsamppCore module,
478 // passing the module URL and temporary file path, with the use Progress Bar parameter set to true.
479 $result = $bearsamppCore->getFileFromUrl($moduleUrl, $tmpFilePath, true);
480
481 // Check if $result is false
482 if ($result === false) {
483 Util::logError('Failed to retrieve file from URL: ' . $moduleUrl);
484 return ['error' => 'Failed to retrieve file from URL'];
485 }
486
487 // Determine the file extension and call the appropriate unzipping function
488 $fileExtension = pathinfo($tmpFilePath, PATHINFO_EXTENSION);
489 Util::logDebug('File extension: ' . $fileExtension);
490
491 if ($fileExtension === '7z' || $fileExtension === 'zip') {
492 // Send phase indicator for extraction
493 echo json_encode(['phase' => 'extracting']);
494 if (ob_get_length()) {
495 ob_flush();
496 }
497 flush();
498
499 $unzipResult = $bearsamppCore->unzipFile($tmpFilePath, $destination, function ($currentPercentage) {
500 echo json_encode(['progress' => "$currentPercentage%"]);
501 if (ob_get_length()) {
502 ob_flush();
503 }
504 flush();
505 });
506
507 if ($unzipResult === false) {
508 return ['error' => 'Failed to unzip file. File: ' . $tmpFilePath . ' could not be unzipped', 'Destination: ' . $destination];
509 }
510 } else {
511 Util::logError('Unsupported file extension: ' . $fileExtension);
512 return ['error' => 'Unsupported file extension'];
513 }
514
515 return ['success' => 'Module installed successfully'];
516}
517
530 public function getModuleDestinationPath(string $moduleType, string $moduleName)
531 {
532 global $bearsamppRoot;
533 if ( $moduleType === 'application' ) {
534 $destination = $bearsamppRoot->getAppsPath() . '/' . strtolower( $moduleName ) . '/';
535 }
536 elseif ( $moduleType === 'binary' ) {
537 $destination = $bearsamppRoot->getBinPath() . '/' . strtolower( $moduleName ) . '/';
538 }
539 elseif ( $moduleType === 'tools' ) {
540 $destination = $bearsamppRoot->getToolsPath() . '/' . strtolower( $moduleName ) . '/';
541 }
542 else {
543 $destination = '';
544 }
545
546 return $destination;
547 }
548
563 public function getQuickpickMenu(array $modules, array $versions, string $imagesPath): string
564 {
565 global $bearsamppConfig;
566 $includePr = $bearsamppConfig->getIncludePr();
567
568 ob_start();
569 if ( Util::checkInternetState() ) {
570
571 // Check if the license key is valid
572 if ( $this->checkDownloadId() ): ?>
573 <div id = 'quickPickContainer'>
574 <div class = 'quickpick me-5'>
575
576 <div class = "custom-select">
577 <button class = "select-button" role = "combobox"
578 aria-label = "select button"
579 aria-haspopup = "listbox"
580 aria-expanded = "false"
581 aria-controls = "select-dropdown">
582 <span class = "selected-value">Select a module and version</span>
583 <span class = "arrow"></span>
584 </button>
585 <ul class = "select-dropdown" role = "listbox" id = "select-dropdown">
586
587 <?php
588 foreach ( $modules as $module ): ?>
589 <?php if ( is_string( $module ) ): ?>
590 <li role = "option" class = "moduleheader">
591 <?php echo htmlspecialchars( $module ); ?>
592 </li>
593
594 <?php
595 foreach ( $versions['module-' . strtolower( $module )] as $version_array ):
596 // Skip prerelease versions if includePr is not enabled
597 if (isset($version_array['prerelease']) && $version_array['prerelease'] === true && $includePr != 1) {
598 continue;
599 }
600 ?>
601 <li role = "option" class = "moduleoption"
602 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>-li">
603 <input type = "radio"
604 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>"
605 name = "module" data-module = "<?php echo htmlspecialchars( $module ); ?>"
606 data-value = "<?php echo htmlspecialchars( $version_array['version'] ); ?>">
607 <label
608 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>
609 </li>
610 <?php endforeach; ?>
611 <?php endif; ?>
612 <?php endforeach; ?>
613 </ul>
614 </div>
615 </div>
616 <div class = "progress " id = "progress" tabindex = "-1" style = "width:260px;display:none"
617 aria-labelledby = "progressbar" aria-hidden = "true">
618 <div class = "progress-bar progress-bar-striped progress-bar-animated" id = "progress-bar" role = "progressbar" aria-valuenow = "0" aria-valuemin = "0"
619 aria-valuemax = "100" data-module = "Module"
620 data-version = "0.0.0">0%
621 </div>
622 <div id = "download-module" style = "display: none">ModuleName</div>
623 <div id = "download-version" style = "display: none">Version</div>
624 </div>
625 </div>
626 <?php else: ?>
627 <div id = "subscribeContainer" class = "text-center mt-3 pe-3">
628 <a href = "<?php echo Util::getWebsiteUrl( 'subscribe' ); ?>" class = "btn btn-dark d-inline-flex align-items-center">
629 <img src = "<?php echo $imagesPath . 'subscribe.svg'; ?>" alt = "Subscribe Icon" class = "me-2">
630 Subscribe to QuickPick now
631 </a>
632 </div>
633 <?php endif;
634 }
635 else {
636 ?>
637 <div id = "InternetState" class = "text-center mt-3 pe-3">
638 <img src = "<?php echo $imagesPath . 'no-wifi-icon.svg'; ?>" alt = "No Wifi Icon" class = "me-2">
639 <span>No internet present</span>
640 </div>
641 <?php
642 }
643
644 return ob_get_clean();
645 }
646}
$result
global $bearsamppRoot
global $bearsamppCore
$response
isValidHeaderResponse($headers)
fetchAndUnzipModule(string $moduleUrl, string $module)
getModuleUrl(string $module, string $version)
installModule(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 logError($data, $file=null)
static logDebug($data, $file=null)
static getWebsiteUrl($path='', $fragment='', $utmSource=true)
static checkInternetState()
global $bearsamppConfig
Definition homepage.php:27
$imagesPath
Definition homepage.php:38
const QUICKPICK_JSON_URL
Definition root.php:27
const QUICKPICK_API_URL
Definition root.php:24
const QUICKPICK_API_KEY
Definition root.php:23