2024.8.23
Loading...
Searching...
No Matches
class.action.quickPick.php
Go to the documentation of this file.
1<?php
2/*
3 * Copyright (c) 2021-2024 Bearsampp
4 * License: GNU General Public License version 3 or later; see LICENSE.txt
5 * Author: Bear
6 * Website: https://bearsampp.com
7 * Github: https://github.com/Bearsampp
8 */
9
10/**
11 * Class QuickPick
12 *
13 * The QuickPick class provides functionalities for managing and installing various modules
14 * within the Bearsampp application. It includes methods for retrieving available modules,
15 * fetching module versions, parsing release properties, and validating license keys.
16 */
18{
19 // Membership Pro api key & url
20 const API_KEY = '4abe15e5-95f2-4663-ad12-eadb245b28b4';
21 const API_URL = 'https://bearsampp.com/index.php?option=com_osmembership&task=api.get_active_plan_ids&api_key=';
22
23 // URL where quickpick-releases.json lives.
24 const JSON_URL = 'https://raw.githubusercontent.com/Bearsampp/Bearsampp/main/core/resources/quickpick-releases.json';
25
26 /**
27 * @var array $modules
28 *
29 * An associative array where the key is the module name and the value is an array containing the module type.
30 * The module type can be one of the following:
31 * - 'application'
32 * - 'binary'
33 * - 'tool'
34 */
35 public $modules = [
36 'Adminer' => ['type' => 'application'],
37 'Apache' => ['type' => 'binary'],
38 'Composer' => ['type' => 'tools'],
39 'ConsoleZ' => ['type' => 'tools'],
40 'Ghostscript' => ['type' => 'tools'],
41 'Git' => ['type' => 'tools'],
42 'Mailpit' => ['type' => 'binary'],
43 'MariaDB' => ['type' => 'binary'],
44 'Memcached' => ['type' => 'binary'],
45 'MySQL' => ['type' => 'binary'],
46 'Ngrok' => ['type' => 'tools'],
47 'NodeJS' => ['type' => 'binary'],
48 'Perl' => ['type' => 'tools'],
49 'PHP' => ['type' => 'binary'],
50 'PhpMyAdmin' => ['type' => 'application'],
51 'PhpPgAdmin' => ['type' => 'application'],
52 'PostgreSQL' => ['type' => 'binary'],
53 'Python' => ['type' => 'tools'],
54 'Ruby' => ['type' => 'tools'],
55 'Webgrind' => ['type' => 'application'],
56 'Xlight' => ['type' => 'binary'],
57 'Yarn' => ['type' => 'tools']
58 ];
59
60 /**
61 * @var array $versions
62 *
63 * An associative array where the key is the module name and the value is an array containing the module versions.
64 */
65 private $versions = [];
66
67 /**
68 * @var string $jsonFilePath
69 *
70 * The file path to the local quickpick-releases.json file.
71 */
73
74 /**
75 * Constructor to initialize the jsonFilePath.
76 */
77 public function __construct()
78 {
79 global $bearsamppCore;
80 $this->jsonFilePath = $bearsamppCore->getResourcesPath() . '/quickpick-releases.json';
81 }
82
83 /**
84 * Retrieves the list of available modules.
85 *
86 * @return array An array of module names.
87 */
88 public function getModules(): array
89 {
90 return array_keys( $this->modules );
91 }
92
93 /**
94 * Loads the QuickPick interface with the available modules and their versions.
95 *
96 * @param string $imagesPath The path to the images directory.
97 *
98 * @return string The HTML content of the QuickPick interface.
99 *
100 * @throws Exception
101 */
102 public function loadQuickpick(string $imagesPath): string
103 {
104 $this->checkQuickpickJson();
105
106 $modules = $this->getModules();
107 $versions = $this->getVersions();
108
109 return $this->getQuickpickMenu( $modules, $versions, $imagesPath );
110 }
111
112 /**
113 * Checks if the local `quickpick-releases.json` file is up-to-date with the remote version.
114 *
115 * This method compares the creation time of the local JSON file with the remote file's last modified time.
116 * If the remote file is newer or the local file does not exist, it fetches the latest JSON data by calling
117 * the `rebuildQuickpickJson` method.
118 *
119 * @return array|false Returns the JSON data if the remote file is newer or the local file does not exist,
120 * otherwise returns false.
121 * @throws Exception
122 */
123 public function checkQuickpickJson()
124 {
125 // Initialize variables
126 $localFileCreationTime = 0;
127
128 // Get the creation time of the local file if it exists
129 if ( file_exists( $this->jsonFilePath ) ) {
130 $localFileCreationTime = filectime( $this->jsonFilePath );
131 }
132 else {
133 $result = $this->rebuildQuickpickJson();
134 }
135
136 // Get the creation time of the remote file
137 $headers = get_headers( self::JSON_URL, 1 );
138 if ( $headers === false || !isset( $headers['Last-Modified'] ) ) {
139 // If we cannot get the headers or Last-Modified is not set, assume no update is needed
140 return false;
141 }
142 $remoteFileCreationTime = strtotime( $headers['Last-Modified'] );
143
144 // Compare the creation times
145 if ( $remoteFileCreationTime > $localFileCreationTime || $localFileCreationTime === 0 ) {
146 return $this->rebuildQuickpickJson();
147 }
148
149 // Return false if the local file is up-to-date
150 return false;
151 }
152
153 /**
154 * Retrieves the QuickPick JSON data from the local file.
155 *
156 * @return array The decoded JSON data, or an error message if the file cannot be fetched or decoded.
157 */
158 public function getQuickpickJson(): array
159 {
160 $content = @file_get_contents( $this->jsonFilePath );
161 if ( $content === false ) {
162 Util::logError( 'Error fetching content from JSON file: ' . $this->jsonFilePath );
163
164 return ['error' => 'Error fetching JSON file'];
165 }
166
167 $data = json_decode( $content, true );
168 if ( json_last_error() !== JSON_ERROR_NONE ) {
169 Util::logError( 'Error decoding JSON content: ' . json_last_error_msg() );
170
171 return ['error' => 'Error decoding JSON content'];
172 }
173
174 return $data;
175 }
176
177 /**
178 * Rebuilds the local quickpick-releases.json file by fetching the latest data from the remote URL.
179 *
180 * @return array An array containing the status and message of the rebuild process.
181 * @throws Exception If the JSON content cannot be fetched or saved.
182 */
183 public function rebuildQuickpickJson(): array
184 {
185 Util::logDebug( 'Fetching JSON file: ' . $this->jsonFilePath );
186
187 // Define the URL of the remote JSON file
188 $url = self::JSON_URL;
189
190 // Fetch the JSON content from the URL
191 $jsonContent = file_get_contents( $url );
192
193 if ( $jsonContent === false ) {
194 // Handle error if the file could not be fetched
195 throw new Exception( 'Failed to fetch JSON content from the URL.' );
196 }
197
198 // Save the JSON content to the specified path
199 $result = file_put_contents( $this->jsonFilePath, $jsonContent );
200
201 if ( $result === false ) {
202 // Handle error if the file could not be saved
203 throw new Exception( 'Failed to save JSON content to the specified path.' );
204 }
205
206 // Return success message
207 return ['success' => 'JSON content fetched and saved successfully'];
208 }
209
210 /**
211 * Retrieves the list of available versions for all modules.
212 *
213 * This method fetches the QuickPick JSON data and returns an array of versions or If no versions are found, an error
214 * message is logged and returned.
215 *
216 * @return array An array of version strings for the specified module, or an error message if no versions are found.
217 */
218 public function getVersions(): array
219 {
220 Util::logDebug( 'Versions called' );
221
222 $versions = [];
223
224 $jsonData = $this->getQuickpickJson();
225
226 foreach ( $jsonData as $entry ) {
227 if ( is_array( $entry ) ) {
228 if ( isset( $entry['module'] ) && is_string( $entry['module'] ) ) {
229 if ( isset( $entry['versions'] ) && is_array( $entry['versions'] ) ) {
230 $versions[$entry['module']] = array_column( $entry['versions'], null, 'version' );
231 }
232 }
233 }
234 else {
235 Util::logError( 'Invalid entry format in JSON data' );
236 }
237 }
238
239 if ( empty( $versions ) ) {
240 Util::logError( 'No versions found' );
241
242 return ['error' => 'No versions found'];
243 }
244
245 Util::logDebug( 'Found versions' );
246
247 $this->versions = $versions;
248
249 return $versions;
250 }
251
252 /**
253 * Fetches the URL of a specified module version from the local quickpick-releases.json file.
254 *
255 * This method reads the quickpick-releases.json file to find the URL associated with the given module
256 * and version. It logs the process and returns the URL if found, or an error message if not.
257 *
258 * @param string $module The name of the module.
259 * @param string $version The version of the module.
260 *
261 * @return string|array The URL of the specified module version or an error message if the version is not found.
262 */
263 public function getModuleUrl(string $module, string $version)
264 {
265 $this->getVersions();
266 Util::logDebug( 'getModuleUrl called for module: ' . $module . ' version: ' . $version );
267 $url = trim( $this->versions['module-' . strtolower( $module )][$version]['url'] );
268 if ( $url <> '' ) {
269 Util::logDebug( 'Found URL for version: ' . $version . ' URL: ' . $url );
270
271 return $url;
272 }
273 else {
274 Util::logError( 'Version not found: ' . $version );
275
276 return ['error' => 'Version not found'];
277 }
278 }
279
280 /**
281 * Validates the format of a given username key by checking it against an external API.
282 *
283 * This method performs several checks to ensure the validity of the username key:
284 * 1. Logs the method call.
285 * 2. Ensures the global configuration is available.
286 * 3. Retrieves the username key from the global configuration.
287 * 4. Ensures the username key is not empty.
288 * 5. Constructs the API URL using the username key.
289 * 6. Fetches the API response.
290 * 7. Decodes the JSON response.
291 * 8. Validates the response data.
292 *
293 * @return bool True if the username key is valid, false otherwise.
294 */
295 public function checkDownloadId(): bool
296 {
297 global $bearsamppConfig;
298
299 Util::logDebug( 'checkDownloadId method called.' );
300
301 // Ensure the global config is available
302 if ( !isset( $bearsamppConfig ) ) {
303 Util::logError( 'Global configuration is not set.' );
304
305 return false;
306 }
307
308 $DownloadId = $bearsamppConfig->getDownloadId();
309 Util::logDebug( 'DownloadId is: ' . $DownloadId );
310
311 // Ensure the license key is not empty
312 if ( empty( $DownloadId ) ) {
313 Util::logError( 'License key is empty.' );
314
315 return false;
316 }
317
318 $url = self::API_URL . self::API_KEY . '&download_id=' . $DownloadId;
319 Util::logDebug( 'API URL: ' . $url );
320
321 $response = @file_get_contents( $url );
322
323 // Check if the response is false
324 if ( $response === false ) {
325 $error = error_get_last();
326 Util::logError( 'Error fetching API response: ' . $error['message'] );
327
328 return false;
329 }
330
331 Util::logDebug( 'API response: ' . $response );
332
333 $data = json_decode( $response, true );
334
335 // Check if the JSON decoding was successful
336 if ( json_last_error() !== JSON_ERROR_NONE ) {
337 Util::logError( 'Error decoding JSON response: ' . json_last_error_msg() );
338
339 return false;
340 }
341
342 // Validate the response data
343 if ( isset( $data['success'] ) && $data['success'] === true && isset( $data['data'] ) && is_array( $data['data'] ) && count( $data['data'] ) > 0 ) {
344 Util::logDebug( 'License key valid: ' . $DownloadId );
345
346 return true;
347 }
348
349 Util::logError( 'Invalid license key: ' . $DownloadId );
350
351 return false;
352 }
353
354 /**
355 * Installs a specified module by fetching its URL and unzipping its contents.
356 *
357 * This method retrieves the URL of the specified module and version from the QuickPick JSON data.
358 * If the URL is found, it fetches and unzips the module. If the URL is not found, it logs an error
359 * and returns an error message.
360 *
361 * @param string $module The name of the module to install.
362 * @param string $version The version of the module to install.
363 *
364 * @return array An array containing the status and message of the installation process.
365 * If successful, it returns the response from the fetchAndUnzipModule method.
366 * If unsuccessful, it returns an error message indicating the issue.
367 */
368 public function installModule(string $module, string $version): array
369 {
370 // Find the module URL and module name from the data
371 $moduleUrl = $this->getModuleUrl( $module, $version );
372
373 if ( is_array( $moduleUrl ) && isset( $moduleUrl['error'] ) ) {
374 Util::logError( 'Module URL not found for module: ' . $module . ' version: ' . $version );
375
376 return ['error' => 'Module URL not found'];
377 }
378
379 if ( empty( $moduleUrl ) ) {
380 Util::logError( 'Module URL not found for module: ' . $module . ' version: ' . $version );
381
382 return ['error' => 'Module URL not found'];
383 }
384
385 $state = Util::checkInternetState();
386 if ( $state ) {
387 $response = $this->fetchAndUnzipModule( $moduleUrl, $module );
388 Util::logDebug( 'Response is: ' . print_r( $response, true ) );
389
390 return $response;
391 }
392 else {
393 Util::logError( 'No internet connection available.' );
394
395 return ['error' => 'No internet connection'];
396 }
397 }
398
399 /**
400 * Fetches the module URL and stores it in /tmp, then unzips the file based on its extension.
401 *
402 * @param string $moduleUrl The URL of the module to fetch.
403 * @param string $module The name of the module.
404 *
405 * @return array An array containing the status and message.
406 */
407 public function fetchAndUnzipModule(string $moduleUrl, string $module): array
408 {
409 Util::logDebug( "$module is: " . $module );
410
412 $tmpDir = $bearsamppRoot->getTmpPath();
413 Util::logDebug( 'Temporary Directory: ' . $tmpDir );
414
415 $fileName = basename( $moduleUrl );
416 Util::logDebug( 'File Name: ' . $fileName );
417
418 $tmpFilePath = $tmpDir . '/' . $fileName;
419 Util::logDebug( 'File Path: ' . $tmpFilePath );
420
421 $moduleName = str_replace( 'module-', '', $module );
422 Util::logDebug( 'Module Name: ' . $moduleName );
423
424 $moduleType = $this->modules[$module]['type'];
425 Util::logDebug( 'Module Type: ' . $moduleType );
426
427 // Get module type
428 $destination = $this->getModuleDestinationPath( $moduleType, $moduleName );
429 Util::logDebug( 'Destination: ' . $destination );
430
431 // Retrieve the file path from the URL using the bearsamppCore module,
432 // passing the module URL and temporary file path, with the use Progress Bar parameter set to true.
433 $result = $bearsamppCore->getFileFromUrl( $moduleUrl, $tmpFilePath, true );
434
435 // Check if $result is false
436 if ( $result === false ) {
437 Util::logError( 'Failed to retrieve file from URL: ' . $moduleUrl );
438
439 return ['error' => 'Failed to retrieve file from URL'];
440 }
441
442 // Determine the file extension and call the appropriate unzipping function
443 $fileExtension = pathinfo( $tmpFilePath, PATHINFO_EXTENSION );
444 Util::logDebug( 'File extension: ' . $fileExtension );
445
446 if ( $fileExtension === '7z' || $fileExtension === 'zip' ) {
447 // Send phase indicator for extraction
448 echo json_encode( ['phase' => 'extracting'] );
449 if ( ob_get_length() ) {
450 ob_flush();
451 }
452 flush();
453
454 $unzipResult = $bearsamppCore->unzipFile( $tmpFilePath, $destination, function ($currentFile, $totalFiles) {
455 echo json_encode( ['progress' => "$currentFile of $totalFiles"] );
456 if ( ob_get_length() ) {
457 ob_flush();
458 }
459 flush();
460 } );
461
462 if ( $unzipResult === false ) {
463 return ['error' => 'Failed to unzip file. File: ' . $tmpFilePath . ' could not be unzipped', 'Destination: ' . $destination];
464 }
465 }
466 else {
467 Util::logError( 'Unsupported file extension: ' . $fileExtension );
468
469 return ['error' => 'Unsupported file extension'];
470 }
471
472 return ['success' => 'Module installed successfully'];
473 }
474
475 /**
476 * Get the destination path for a given module type and name.
477 *
478 * This method constructs the destination path based on the type of module
479 * (application, binary, or tools) and the module name. It utilizes the
480 * `bearsamppRoot` global object to retrieve the base paths for each module type.
481 *
482 * @param string $moduleType The type of the module ('application', 'binary', or 'tools').
483 * @param string $moduleName The name of the module.
484 *
485 * @return string The constructed destination path for the module.
486 */
487 public function getModuleDestinationPath(string $moduleType, string $moduleName)
488 {
489 global $bearsamppRoot;
490 if ( $moduleType === 'application' ) {
491 $destination = $bearsamppRoot->getAppsPath() . '/' . strtolower( $moduleName ) . '/';
492 }
493 elseif ( $moduleType === 'binary' ) {
494 $destination = $bearsamppRoot->getBinPath() . '/' . strtolower( $moduleName ) . '/';
495 }
496 elseif ( $moduleType === 'tools' ) {
497 $destination = $bearsamppRoot->getToolsPath() . '/' . strtolower( $moduleName ) . '/';
498 }
499 else {
500 $destination = '';
501 }
502
503 return $destination;
504 }
505
506 /**
507 * Generates the HTML content for the QuickPick menu.
508 *
509 * This method creates the HTML structure for the QuickPick interface, including a dropdown
510 * for selecting modules and their respective versions. It checks if the license key is valid
511 * before displaying the modules. If the license key is invalid, it displays a subscription prompt.
512 * If there is no internet connection, it displays a message indicating the lack of internet.
513 *
514 * @param array $modules An array of available modules.
515 * @param array $versions An associative array where the key is the module name and the value is an array containing the module versions.
516 * @param string $imagesPath The path to the images directory.
517 *
518 * @return string The HTML content of the QuickPick menu.
519 */
520 public function getQuickpickMenu(array $modules, array $versions, string $imagesPath): string
521 {
522 ob_start();
523 if ( Util::checkInternetState() ) {
524
525 // Check if the license key is valid
526 if ( $this->checkDownloadId() ): ?>
527 <div id = 'quickPickContainer'>
528 <div class = 'quickpick me-5'>
529
530 <div class = "custom-select">
531 <button class = "select-button" role = "combobox"
532 aria-label = "select button"
533 aria-haspopup = "listbox"
534 aria-expanded = "false"
535 aria-controls = "select-dropdown">
536 <span class = "selected-value">Select a module and version</span>
537 <span class = "arrow"></span>
538 </button>
539 <ul class = "select-dropdown" role = "listbox" id = "select-dropdown">
540
541 <?php
542 foreach ( $modules as $module ): ?>
543 <?php if ( is_string( $module ) ): ?>
544 <li role = "option" class = "moduleheader">
545 <?php echo htmlspecialchars( $module ); ?>
546 </li>
547
548 <?php
549 foreach ( $versions['module-' . strtolower( $module )] as $version_array ): ?>
550 <li role = "option" class = "moduleoption"
551 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>-li">
552 <input type = "radio"
553 id = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>"
554 name = "module" data-module = "<?php echo htmlspecialchars( $module ); ?>"
555 data-value = "<?php echo htmlspecialchars( $version_array['version'] ); ?>">
556 <label
557 for = "<?php echo htmlspecialchars( $module ); ?>-version-<?php echo htmlspecialchars( $version_array['version'] ); ?>"><?php echo htmlspecialchars( $version_array['version'] ); ?></label>
558 </li>
559 <?php endforeach; ?>
560 <?php endif; ?>
561 <?php endforeach; ?>
562 </ul>
563 </div>
564 </div>
565 <div class = "progress " id = "progress" tabindex = "-1" style = "width:260px;display:none"
566 aria-labelledby = "progressbar" aria-hidden = "true">
567 <div class = "progress-bar progress-bar-striped progress-bar-animated" id = "progress-bar" role = "progressbar" aria-valuenow = "0" aria-valuemin = "0"
568 aria-valuemax = "100" data-module = "Module"
569 data-version = "0.0.0">0%
570 </div>
571 <div id = "download-module" style = "display: none">ModuleName</div>
572 <div id = "download-version" style = "display: none">Version</div>
573 </div>
574 </div>
575 <?php else: ?>
576 <div id = "subscribeContainer" class = "text-center mt-3 pe-3">
577 <a href = "<?php echo Util::getWebsiteUrl( 'subscribe' ); ?>" class = "btn btn-dark d-inline-flex align-items-center">
578 <img src = "<?php echo $imagesPath . 'subscribe.svg'; ?>" alt = "Subscribe Icon" class = "me-2">
579 Subscribe to QuickPick now
580 </a>
581 </div>
582 <?php endif;
583 }
584 else {
585 ?>
586 <div id = "InternetState" class = "text-center mt-3 pe-3">
587 <img src = "<?php echo $imagesPath . 'no-wifi-icon.svg'; ?>" alt = "No Wifi Icon" class = "me-2">
588 <span>No internet present</span>
589 </div>
590 <?php
591 }
592
593 return ob_get_clean();
594 }
595}
$result
global $bearsamppRoot
global $bearsamppCore
$response
getQuickpickMenu(array $modules, array $versions, string $imagesPath)
getModuleDestinationPath(string $moduleType, string $moduleName)
fetchAndUnzipModule(string $moduleUrl, string $module)
installModule(string $module, string $version)
loadQuickpick(string $imagesPath)
getModuleUrl(string $module, string $version)
static logError($data, $file=null)
static getWebsiteUrl($path='', $fragment='', $utmSource=true)
static logDebug($data, $file=null)
static checkInternetState()
global $bearsamppConfig
Definition homepage.php:26
$imagesPath
Definition homepage.php:37