dolibarr 22.0.5
externalModules.class.php
1<?php
2/*
3 * Copyright (C) 2025 Mohamed DAOUD <mdaoud@dolicloud.com>
4 * Copyright (C) 2025 MDW <mdeweerd@users.noreply.github.com>
5 * Copyright (C) 2025 Frédéric France <frederic.france@free.fr>
6 *
7 * This program is free software; you can redistribute it and/or modifyion 2.0 (the "License");
8 * it under the terms of the GNU General Public License as published bypliance with the License.
9 * the Free Software Foundation; either version 3 of the License, or
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 * or see https://www.gnu.org/
19 */
20
21include_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
22
27{
31 public $no_page;
32
36 public $per_page;
40 public $categorie;
41
45 public $search;
46
47 // setups
55 public $file_source_url;
56
60 public $cache_file;
61
66 public $url;
70 public $shop_url; // the url of the shop
74 public $lang; // the integer representing the lang in the store
78 public $debug_api; // useful if no dialog
82 public $dolistore_api_url;
86 public $dolistore_api_key;
87
91 public $dolistoreApiStatus;
92
96 public $dolistoreApiError;
97
101 public $githubFileStatus;
102
106 public $githubFileError;
107
111 public $error;
112
116 public $numberOfProviders;
117
121 public $products;
122
126 public $numberTotalOfProducts;
127
131 public $numberTotalOfPages;
132
136 public $numberOfProducts;
137
143 public function __construct($debug = false)
144 {
145 global $langs;
146
147 $this->debug_api = $debug;
148
149 $this->url = DOL_URL_ROOT.'/admin/modules.php?mode=marketplace';
150
151 // For dolistore modules
152 $this->dolistore_api_url = getDolGlobalString('MAIN_MODULE_DOLISTORE_API_SRV', 'https://www.dolistore.com/api/'); // 'https://www.dolistore.com/api/', 'https://admin2.dolibarr.org/api/index.php/marketplace/'
153 $this->dolistore_api_key = getDolGlobalString('MAIN_MODULE_DOLISTORE_API_KEY', 'dolistorepublicapi');
154 $this->shop_url = getDolGlobalString('MAIN_MODULE_DOLISTORE_SHOP_URL', 'https://www.dolistore.com');
155
156 // For community modules
157 $this->file_source_url = "https://raw.githubusercontent.com/Dolibarr/dolibarr-community-modules/refs/heads/main/index.yaml";
158 $this->cache_file = DOL_DATA_ROOT.'/admin/temp/remote_github_modules_file.yaml';
159
160 $lang = $langs->defaultlang;
161 $lang_array = array('en_US', 'fr_FR', 'es_ES', 'it_IT', 'de_DE');
162 if (!in_array($lang, $lang_array)) {
163 $lang = 'en_US';
164 }
165 $this->lang = $lang;
166 }
167
174 public function loadRemoteSources($debug = false)
175 {
176 // Check access to Community repo
177 if (getDolGlobalString('MAIN_ENABLE_EXTERNALMODULES_COMMUNITY')) {
178 $cachedelayforgithubrepo = getDolGlobalInt('MAIN_REMOTE_GITHUBREPO_CACHE_DELAY', 86400);
179
180 $this->getRemoteYamlFile($this->file_source_url, $cachedelayforgithubrepo);
181
182 $this->githubFileError = $this->error;
183 $this->githubFileStatus = dol_is_file($this->cache_file) ? 1 : 0;
184 }
185
186 // Check access to Dolistore API /api/categories -> /api/index.php/marketplace/categories
187 if (getDolGlobalString('MAIN_ENABLE_EXTERNALMODULES_DOLISTORE')) {
188 $this->dolistoreApiStatus = $this->checkApiStatus();
189 }
190
191 // Count the number of online providers
192 $this->numberOfProviders = $this->dolistoreApiStatus + $this->githubFileStatus;
193 }
194
202 public function callApi($resource, $options = false)
203 {
204 // If no dolistore_api_key is set, we can't access the API
205 if (empty($this->dolistore_api_key) || empty($this->dolistore_api_url)) {
206 return array('status_code' => 0, 'response' => null);
207 }
208
209 // Add basic auth if needed
210 $basicAuthLogin = getDolGlobalString('MAIN_MODULE_DOLISTORE_BASIC_LOGIN');
211 $basicAuthPassword = getDolGlobalString('MAIN_MODULE_DOLISTORE_BASIC_PASSWORD');
212
213 $httpheader = array('DOLAPIKEY: '.$this->dolistore_api_key);
214 if ($basicAuthLogin) {
215 $httpheader[] = 'Authorization: Basic '.base64_encode($basicAuthLogin.':'.$basicAuthPassword);
216 }
217
218 $url = $this->dolistore_api_url . (preg_match('/\/$/', $this->dolistore_api_url) ? '' : '/') . $resource;
219
220 $options['apikey'] = $this->dolistore_api_key;
221
222 if ($options) {
223 $url .= '?' . http_build_query($options);
224 }
225
226 $response = getURLContent($url, 'GET', '', 1, $httpheader, array('https'), 0, -1, 5, 5);
227
228 $status_code = $response['http_code'];
229 $body = 'Error';
230
231 if ($status_code == 200) {
232 $body = $response['content'];
233 $body = json_decode($body, true);
234 $returnarray = array(
235 'status_code' => $status_code,
236 'response' => $body
237 );
238 } else {
239 $returnarray = array(
240 'status_code' => $status_code,
241 'response' => $body
242 );
243 if (!empty($response['curl_error_no'])) {
244 $returnarray['curl_error_no'] = $response['curl_error_no'];
245 }
246 if (!empty($response['curl_error_msg'])) {
247 $returnarray['curl_error_msg'] = $response['curl_error_msg'];
248 }
249 }
250
251 return $returnarray;
252 }
253
260 public function fetchModulesFromFile($options = array())
261 {
262 $modules = array();
263
264 if (!empty($this->cache_file) && file_exists($this->cache_file)) {
265 dol_syslog(__METHOD__ . " - Loading cache file: " . $this->cache_file, LOG_DEBUG);
266
267 $content = file_get_contents($this->cache_file);
268 if ($content !== false) {
269 $modules = $this->readYaml($content);
270 } else {
271 dol_syslog(__METHOD__ . " - Error reading cache file", LOG_ERR);
272 }
273 }
274
275 return $modules;
276 }
277
284 public function getCategories($active = 0)
285 {
286 $organized_tree = array();
287 $html = '';
288
289 $data = [
290 'lang' => $this->lang
291 ];
292
293 $current = $active;
294
295 $resCategories = $this->callApi('categories', $data);
296 if (isset($resCategories['response']) && is_array($resCategories['response'])) {
297 $organized_tree = $resCategories['response'];
298 } else {
299 return $html ;
300 }
301
302 $html = '';
303 foreach ($organized_tree as $key => $value) {
304 if ($value['label'] != "Versions" && $value['label'] != "Specials") {
305 $html .= '<li' . ($current == $value['rowid'] ? ' class="active"' : '') . '>';
306 $html .= '<a href="?mode=marketplace&categorie=' . $value['rowid'] . '">' . $value['label'] . '</a>';
307 if (isset($value['children'])) {
308 $html .= '<ul>';
309 usort($value['children'], $this->buildSorter('position'));
310 foreach ($value['children'] as $key_children => $value_children) {
311 $html .= '<li' . ($current == $value_children['rowid'] ? ' class="active"' : '') . '>';
312 $html .= '<a href="?mode=marketplace&categorie=' . $value_children['rowid'] . '" title="' . dol_escape_htmltag(strip_tags($value_children['description'])) . '">' . $value_children['label'] . '</a>';
313 $html .= '</li>';
314 }
315 $html .= '</ul>';
316 }
317 $html .= '</li>';
318 }
319 }
320 return $html;
321 }
322
329 public function getProducts($options)
330 {
331 global $langs;
332
333 $langs->load("products");
334
335 $html = "";
336 $last_month = dol_now() - (30 * 24 * 60 * 60);
337 $dolibarrversiontouse = DOL_VERSION; // full string with version
338
339 $this->products = array();
340
341 $this->categorie = $options['categorie'] ?? 0;
342 $this->per_page = $options['per_page'] ?? 11;
343 $this->no_page = $options['no_page'] ?? 1;
344 $this->search = $options['search'] ?? '';
345
346 $this->per_page = 11; // We fix number of products per page to 11
347
348 // Length of $search must be at least 2 characters
349 if (!empty($this->search) && strlen(str_replace(' ', '', (string) $this->search)) < 2) {
350 $html .= '<tr class=""><td colspan="3" class="center">';
351 $html .= '<br><br>';
352 $html .= $langs->trans("SearchStringMinLength").'...';
353 $html .= '<br><br>';
354 $html .= '</td></tr>';
355 return $html;
356 }
357
358 $data = [
359 'categorieid' => $this->categorie,
360 'limit' => $this->per_page,
361 'page' => $this->no_page,
362 'search' => $this->search,
363 'lang' => $this->lang
364 ];
365
366
367 $this->numberTotalOfProducts = 0;
368
369 // Fetch the products from Dolistore source
370
371 $dolistoreProducts = array();
372 $dolistoreProductsTotal = 0;
373 if ($this->dolistoreApiStatus > 0 && getDolGlobalInt('MAIN_ENABLE_EXTERNALMODULES_DOLISTORE')) {
374 $getDolistoreProducts = $this->callApi('products', $data);
375
376 if (!isset($getDolistoreProducts['response']) || !is_array($getDolistoreProducts['response']) || ($getDolistoreProducts['status_code'] != 200 && $getDolistoreProducts['status_code'] != 201)) {
377 $dolistoreProducts = array();
378 $dolistoreProductsTotal = 0;
379 } else {
380 $dolistoreProducts = $this->adaptData($getDolistoreProducts['response']['products'], 'dolistore');
381 $dolistoreProductsTotal = $getDolistoreProducts['response']['total'];
382 $this->numberTotalOfProducts += $dolistoreProductsTotal;
383 }
384 }
385
386 // Fetch the products from the github repo
387
388 $fileProducts = array();
389 $fileProductsTotal = 0;
390 if (!empty($this->githubFileStatus) && getDolGlobalInt('MAIN_ENABLE_EXTERNALMODULES_COMMUNITY')) {
391 $fileProducts = $this->fetchModulesFromFile($data); // Return an array with all modules from the cache filecontent in $data
392
393 $fileProducts = $this->adaptData($fileProducts, 'githubcommunity');
394
395 $fileProducts = $this->applyFilters($fileProducts, $data);
396
397 $fileProductsTotal = $fileProducts['total'];
398
399 $this->numberTotalOfProducts += $fileProductsTotal;
400
401 $fileProducts = $fileProducts['data'];
402 }
403
404 // Number of pages
405 $this->numberTotalOfPages = (int) ceil(max($fileProductsTotal / $this->per_page, $dolistoreProductsTotal / $this->per_page));
406
407 // Merge both sources (github community modules have priority on dolistore).
408 $this->products = $dolistoreProducts;
409 foreach ($fileProducts as $fileProduct) {
410 $id = $fileProduct['id'];
411 if ($id > 0) {
412 if (empty($this->products[$id])) { // Not already present in array
413 array_unshift($this->products, $fileProduct);
414 } else {
415 $this->products[$id] = $fileProduct;
416 $this->products[$id]['category'] = $fileProduct['category'];
417 }
418 } else {
419 array_unshift($this->products, $fileProduct);
420 }
421 }
422
423
424 $i = 0;
425 foreach ($this->products as $product) {
426 $i++;
427
428 // check new product ?
429 $newapp = '';
430 if ($last_month < strtotime($product['datec'])) {
431 $newapp .= '<span class="newApp" title="'.$product['tms'].'">'.$langs->trans('New').'</span> ';
432 }
433
434 // check updated ?
435 if ($last_month < strtotime($product['tms']) && $newapp == '') {
436 $newapp .= '<span class="updatedApp" title="'.$product['tms'].'">'.$langs->trans('UpdatedRecently').'</span> ';
437 }
438
439 // add image or default ?
440 if ($product["cover_photo_url"] != '' && $product["cover_photo_url"] != '#') {
441 $images = '<a href="'.$product["cover_photo_url"].'" class="documentpreview" target="_blank" rel="noopener noreferrer" mime="image/png" title="'.dol_escape_htmltag($product["label"].', '.$langs->trans('Version').' '.$product["module_version"]).'">';
442 $images .= '<img class="imgstore" src="'.$product["cover_photo_url"].'" alt="" /></a>';
443 } else {
444 $images = '<img class="imgstore" src="'.DOL_URL_ROOT.'/public/theme/common/nophoto.png" />';
445 }
446
447 // free or pay ?
448 if (array_key_exists('price_ht', $product) && price2num($product["price_ht"]) > 0) {
449 $price = '<h3>'.price(price2num($product["price_ht"], 'MT'), 0, $langs, 1, -1, -1, 'EUR').' '.$langs->trans("HT").'</h3>';
450
451 $download_link = '<a class="paddingleft paddingright" target="_blank" title="'.$langs->trans("View").'" href="'.$this->shop_url.'/product.php?id='.((int) $product['id']).'">';
452 $download_link .= img_picto('', 'url', 'class="size2x paddingright"');
453 $download_link .= '</a>';
454 } else {
455 $download_link = '#';
456 if ($product['source'] === 'dolistore') { // 0 on dolistore may mean 0 or a complementary fee to subscribe
457 $urlview = $this->shop_url.'/product.php?id='.((int) $product["id"]);
458 $price = '<h3><a href="'.$urlview.'" target="_blank">'.$langs->trans('SeeOnDoliStore').'</a></h3>';
459 } elseif ($product['source'] === 'githubcommunity') {
460 if (array_key_exists('price_ht', $product) && empty($product['price_ht'])) {
461 $price = '<h3>'.$langs->trans('Free').'</h3>';
462 } else {
463 if ($product["dolistore-download"]) {
464 $price = '<h3><a href="'.$product["dolistore-download"].'" target="_blank">'.$langs->trans('SeeOnDoliStore').'</a></h3>';
465 } else {
466 $price = '<h3>'.$langs->trans('Unknown').'</h3>';
467 }
468 }
469 } else {
470 $price = '<h3>'.$langs->trans('Unknown').'</h3>';
471 }
472
473 if ($product['source'] === 'githubcommunity') {
474 $download_link = '<a class="paddingleft paddingright" target="_blank" title="'.$langs->trans("Sources").'" href="'.$product["link"].'">';
475 $download_link .= img_picto('', 'file-code', 'class="size2x paddingright colorgrey"');
476 $download_link .= '</a>';
477
478 $urlview = $product["dolistore-download"]; // In a future, we will have the download to the zip file
479 if ($urlview) {
480 $download_link .= '<a class="paddingleft paddingright" target="_blank" title="'.$langs->trans("View").'" href="'.$urlview.'" rel="noopener noreferrer">';
481 $download_link .= img_picto('', 'url', 'class="size2x"');
482 $download_link .= '</a>';
483 }
484
485 if (!empty($product['direct-download']) && $product['direct-download'] == 'yes') {
486 $reg = array();
487 if (preg_match('/https:.*\?id=(\d+)$/', $urlview, $reg)) {
488 $urldownload = 'https://www.dolistore.com/_service_download.php?t=free&p='.$reg[1];
489 $download_link .= '<a class="paddingleft paddingright" target="_blank" title="'.$langs->trans("Download").'" href="'.$urldownload.'" rel="noopener noreferrer">';
490 $download_link .= img_picto('', 'download', 'class="size2x paddingright"');
491 //$download_link .= '<img width="32" src="'.DOL_URL_ROOT.'/admin/remotestore/img/download.png" />';
492 $download_link .= '</a>';
493 }
494 }
495 } elseif ($product['source'] === 'dolistore') {
496 $urlview = $this->shop_url.'/product.php?id='.((int) $product["id"]);
497 $urldownload = 'https://www.dolistore.com/_service_download.php?t=free&p=' . $product['id'];
498 $download_link = '<a class="paddingleft paddingright" target="_blank" title="'.$langs->trans("View").'" href="'.$urlview.'">';
499 $download_link .= img_picto('', 'url', 'class="size2x"');
500 $download_link .= '</a>';
501 $download_link .= '<a class="paddingleft paddingright" target="_blank" title="'.$langs->trans("Download").'" href="'.$urldownload.'" rel="noopener noreferrer">';
502 $download_link .= img_picto('', 'download', 'class="size2x paddingright"');
503 //$download_link .= '<img width="32" src="'.DOL_URL_ROOT.'/admin/remotestore/img/download.png" />';
504 $download_link .= '</a>';
505 }
506 }
507
508 // Set and check version
509 $version = '';
510 $compatible = '';
511 if ($product["status"] == 'soon') {
512 $version = '<span class="warning">'.$langs->trans("NotYetAvailable").'</span>';
513 } elseif ($this->versionCompare($product["dolibarr_min"], $dolibarrversiontouse) <= 0) {
514 if (!empty($product["dolibarr_max"]) && $product["dolibarr_max"] != 'auto' && $product["dolibarr_max"] != 'unknown' && $this->versionCompare($product["dolibarr_max"], $dolibarrversiontouse) >= 0) {
515 //compatible
516 $version = '<span class="compatible">'.$langs->trans(
517 'CompatibleUpTo',
518 $dolibarrversiontouse,
519 $product["dolibarr_min"],
520 $product["dolibarr_max"]
521 ).'</span>';
522 $compatible = '';
523 } else {
524 // never compatible, module expired
525 $version = '<span class="warning hideonsmartphone">'.$langs->trans(
526 'NotCompatible',
527 $dolibarrversiontouse,
528 $product["dolibarr_min"],
529 $product["dolibarr_max"]
530 ).'</span>';
531 $compatible = 'NotCompatible';
532 }
533 } else {
534 if ($product["dolibarr_min"] == 'auto' || $product["dolibarr_min"] != 'unknown') {
535 // never compatible, module expired
536 $version = '<span class="warning">'.$langs->trans(
537 'NotCompatible',
538 $dolibarrversiontouse,
539 $product["dolibarr_min"],
540 $product["dolibarr_max"]
541 ).'</span>';
542 $compatible = 'NotCompatible';
543 } else {
544 //need update
545 $version = '<span class="compatibleafterupdate">'.$langs->trans(
546 'CompatibleAfterUpdate',
547 $dolibarrversiontouse,
548 $product["dolibarr_min"],
549 $product["dolibarr_max"]
550 ).'</span>';
551 $compatible = 'NotCompatible';
552 }
553 }
554
555 // Output the line
556 $html .= '<tr class="app oddeven nohover '.dol_escape_htmltag($compatible).'">';
557
558 // Logo
559 $html .= '<td class="center width150"><div class="newAppParent">';
560 $html .= $newapp.$images; // No dol_escape_htmltag, it is already escape html
561 $html .= '</div></td>';
562
563 // Description
564 $html .= '<td class="margeCote minwidth500imp"><h2 class="appTitle">';
565 $html .= dolPrintHTML(dol_string_nohtmltag(ucfirst($product["label"])));
566 if (!empty($product['author']) && $product['author'] != 'unkownauthor') {
567 $html .= '<small> &nbsp; - &nbsp; '.img_picto('', 'company', 'class="pictofixedwidth"');
568 if (!empty($product['author_url'])) {
569 $html .= '<a href="'.$product['author_url'].'" target="_blank">'.$product['author'].'</a>';
570 } else {
571 $html .= $product['author'];
572 }
573 $html .= '</small>';
574 }
575 $html .= '<br><small>';
576 $html .= $version; // Version Dolibarr. No dol_escape_htmltag, it is already escape html
577 $html .= '</small>';
578 $html .= '</h2>';
579
580 $html .= '<small class="appDateCreation appRef"> ';
581 if (empty($product['tms'])) {
582 $html .= img_picto($langs->trans('DateCreation'), 'calendar', 'class="pictofixedwidth"').'<span class="opacitymedium"><span class="hideonsmartphone">'.$langs->trans("DateCreation").': </span>';
583 $html .= (!empty($product['datec']) ? dol_print_date(dol_stringtotime($product['datec']), 'day') : $langs->trans("Unknown")).'</span>';
584 } else {
585 $html .= img_picto($langs->trans('DateModification'), 'calendar', 'class="pictofixedwidth"').'<span class="opacitymedium">'.dol_print_date(dol_stringtotime($product['tms']), 'day').'</span>';
586 }
587 $html .= ' &nbsp; '.$langs->trans('Ref').' '.dolPrintHTML(preg_replace('/@.*$/', '', $product["ref"]));
588 //$html .= ' - '.dol_escape_htmltag($langs->trans('Id')).': '.((int) $product["id"]);
589 $html .= '</small><br>';
590 //$html .= '<div class="appSource valignmiddle inline-block">'.$langs->trans('Source').' &nbsp; </div>';
591 $html .= '<div class="appSource valignmiddle inline-block">';
592 if ($product["source"] == 'dolistore') {
593 //$html .= img_picto('DoliStore', 'shop', 'class="pictofixedwidth"');
594 $html .= '<img border="0" title="'.dolPrintHTML($langs->trans('Source').": DoliStore").'" class="imgautosize imgmaxwidth100 valignmiddle" style="height: 14px" src="'.DOL_URL_ROOT.'/theme/dolistore_squarred.svg">';
595 } elseif ($product["source"] == 'githubcommunity') {
596 $html .= img_picto($langs->trans('Source').': GitHub community repo', 'group', 'class="pictofixedwidth valignmiddle"');
597 } else {
598 $html .= img_picto($langs->trans('Source').': '.$langs->trans('Other'), 'generic', 'class="pictofixedwidth"');
599 }
600 //$html .= $product["source"];
601 $html .= '</div> &nbsp;';
602 if (!empty($product['phpmin']) && $product['phpmin'] != 'unknown') {
603 $html .= ' <span class="badge-secondary small" style="padding: 3px; border-radius: 5px">PHP min '.$product['phpmin'].'</span>';
604 }
605 if (!empty($product['phpmax']) && $product['phpmax'] != 'unknown') {
606 $html .= ' <span class="badge-secondary small" style="padding: 3px; border-radius: 5px">PHP max '.$product['phpmax'].'</span>';
607 }
608 $html .= '<br>';
609
610 $html .= '<br>';
611 $html .= '<div class="storedesc">'.dolPrintHTML(dol_string_nohtmltag($product["description"])).'</div>';
612 $html .= '</td>';
613
614 // Price - do not load if display none
615 $html .= '<td class="margeCote center amount">';
616 $html .= $price;
617 $html .= '</td>';
618
619 // Links
620 $html .= '<td class="margeCote nowraponall">'.$download_link.'</td>';
621
622 $html .= '</tr>';
623 }
624
625 if (empty($this->products)) {
626 $html .= '<tr class=""><td colspan="3" class="center">';
627 $html .= '<br><br>';
628 $langs->load("website");
629 $html .= $langs->trans("noResultsWereFound").'...';
630 $html .= '<br><br>';
631 $html .= '</td></tr>';
632 }
633
634 $this->numberOfProducts = count($this->products);
635
636 return $html ;
637 }
638
645 public function buildSorter(string $key): Closure
646 {
647 return
653 function (array $a, array $b) use ($key) {
654 $valA = isset($a[$key]) && is_scalar($a[$key]) ? (string) $a[$key] : '';
655 $valB = isset($b[$key]) && is_scalar($b[$key]) ? (string) $b[$key] : '';
656
657 return strnatcmp($valA, $valB);
658 };
659 }
660
668 public function versionCompare($v1, $v2)
669 {
670 // Clean v1 and v2
671 $v1 = str_replace(array('v', 'V'), '', $v1);
672 $v2 = str_replace(array('v', 'V'), '', $v2);
673
674 $v1 = explode('.', $v1);
675 $v2 = explode('.', $v2);
676 $ret = 0;
677 $level = 0;
678 $count1 = count($v1);
679 $count2 = count($v2);
680 $maxcount = max($count1, $count2);
681 while ($level < $maxcount) {
682 $operande1 = isset($v1[$level]) ? $v1[$level] : 'x';
683 $operande2 = isset($v2[$level]) ? $v2[$level] : 'x';
684 $level++;
685 if (strtoupper($operande1) == 'X' || strtoupper($operande2) == 'X' || $operande1 == '*' || $operande2 == '*') {
686 break;
687 }
688 if ($operande1 < $operande2) {
689 $ret = -$level;
690 break;
691 }
692 if ($operande1 > $operande2) {
693 $ret = $level;
694 break;
695 }
696 }
697 //print join('.',$versionarray1).'('.count($versionarray1).') / '.join('.',$versionarray2).'('.count($versionarray2).') => '.$ret.'<br>'."\n";
698 return $ret;
699 }
700
701 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
708 public function get_previous_link($text = '<<')
709 {
710 // phpcs:enable
711 return '<a href="'.$this->get_previous_url().'" class="button">'.dol_escape_htmltag($text).'</a>';
712 }
713
714 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
721 public function get_next_link($text = '>>')
722 {
723 // phpcs:enable
724 return '<a href="'.$this->get_next_url().'" class="button">'.dol_escape_htmltag($text).'</a>';
725 }
726
727 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
733 public function get_previous_url()
734 {
735 // phpcs:enable
736 $param_array = array();
737 if ($this->no_page > 1) {
738 $sub = 1;
739 } else {
740 $sub = 0;
741 }
742 if (!empty($this->search)) {
743 $param_array['search_keyword'] = $this->search;
744 }
745 $param_array['no_page'] = $this->no_page - $sub;
746 if ($this->categorie != 0) {
747 $param_array['categorie'] = $this->categorie;
748 }
749 $param = http_build_query($param_array);
750 return $this->url."&".$param;
751 }
752
753 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
759 public function get_next_url()
760 {
761 // phpcs:enable
762 $param_array = array();
763 if ($this->products !== null && count($this->products) < $this->per_page) {
764 $add = 0;
765 } else {
766 $add = 1;
767 }
768 if (!empty($this->search)) {
769 $param_array['search_keyword'] = $this->search;
770 }
771 $param_array['no_page'] = $this->no_page + $add;
772 if ($this->categorie != 0) {
773 $param_array['categorie'] = $this->categorie;
774 }
775 $param = http_build_query($param_array);
776 return $this->url."&".$param;
777 }
778
784 public function getPagination()
785 {
786
787 global $langs;
788
789 $page = $this->no_page;
790 $limit = $this->per_page;
791 $totalnboflines = $this->numberTotalOfProducts ?: 0;
792 $num = $this->numberOfProducts;
793
794 $html = "";
795
796 // Show navigation bar
797 $pagelist = '';
798 if ($page > 0 || $num > $limit) {
799 if ($totalnboflines) {
800 if ($limit > 0) {
801 $nbpages = $this->numberTotalOfPages;
802 } else {
803 $nbpages = 1;
804 }
805
806 // Show previous page
807 if ($page > 1) {
808 $pagelist .= '<li class="pagination paginationpage paginationpageleft"><a class="paginationprevious reposition" href="'.$this->get_previous_url().'"><i class="fa fa-chevron-left" title="'.dol_escape_htmltag($langs->trans("Previous")).'"></i></a></li>';
809 }
810
811 $pagelist .= '<li class="pagination">';
812 $pagelist .= '<label for="page_input">Page </label>';
813 if ($this->categorie != 0) {
814 $pagelist .= '<input type="hidden" name="categorie" value="' . $this->categorie . '">';
815 }
816 $pagelist .= '<input type="text" id="page_input" name="no_page" value="'.($page).'" min="1" max="'.$nbpages.'" class="width40 page_input" oninput="if(this.value > '.$nbpages.') this.value='.$nbpages.'">';
817 $pagelist .= ' / '.$nbpages;
818 $pagelist .= '</li>';
819
820 // Show next page
821 if ($page < $nbpages) {
822 $pagelist .= '<li class="pagination paginationpage paginationpageright"><a class="paginationnext reposition" href="'.$this->get_next_url().'"><i class="fa fa-chevron-right" title="'.dol_escape_htmltag($langs->trans("Next")).'"></i></a></li>';
823 }
824 }
825 }
826
827 if ($limit || $pagelist) {
828 $html .= '<div class="pagination" style="padding: 7px;">';
829 $html .= '<ul>';
830 $html .= $pagelist;
831 $html .= '</ul>';
832 $html .= '</div>';
833 }
834
835 $html .= ajax_autoselect('.page_input');
836
837 return $html;
838 }
839
846 protected function checkStatusCode($request)
847 {
848 // Define error messages
849 $error_messages = [
850 204 => 'No content',
851 400 => 'Bad Request',
852 401 => 'Unauthorized',
853 404 => 'Not Found',
854 405 => 'Method Not Allowed',
855 500 => 'Internal Server Error',
856 ];
857
858 // If status code is 200 or 201, return an empty string
859 if ($request['status_code'] === 200 || $request['status_code'] === 201) {
860 return '';
861 }
862
863 // Get the predefined error message or use a default one
864 $error_message = $error_messages[$request['status_code']] ?? 'Unexpected HTTP status: ' . $request['status_code'];
865
866 // Append error details if available
867 if (!empty($request['response']) && isset($request['response']['errors']) && is_array($request['response']['errors'])) {
868 foreach ($request['response']['errors'] as $error) {
869 $error_message .= ' - (Code ' . $error['code'] . '): ' . $error['message'];
870 }
871 }
872
873 if (!empty($request['curl_error_msg'])) {
874 $error_message .= ' - ' . $request['curl_error_msg'];
875 }
876
877 // Return the formatted error message
878 return sprintf('This call to the API failed and returned an HTTP status of %d. That means: %s.', $request['status_code'], $error_message);
879 }
880
888 public function getRemoteYamlFile($file_source_url, $cache_time)
889 {
890 $yaml = '';
891 $cache_file = $this->cache_file;
892 $cache_folder = dirname($cache_file);
893
894 // Check if cache directory exists
895 if (!dol_is_dir($cache_folder)) {
896 dol_mkdir($cache_folder, DOL_DATA_ROOT);
897 }
898
899 if (!file_exists($cache_file) || filemtime($cache_file) < (dol_now() - $cache_time)) {
900 // We get remote url
901 $addheaders = array();
902 $result = getURLContent($file_source_url, 'GET', '', 1, $addheaders); // TODO Force timeout to 5 s on both connect and response.
903 if (!empty($result) && $result['http_code'] == 200) {
904 $yaml = $result['content'];
905 $result = file_put_contents($cache_file, $yaml);
906 if ($result === false) {
907 $this->error = 'Failed to create cache file: ' . $cache_file;
908 }
909 }
910 } else {
911 $yaml = file_get_contents($cache_file);
912 }
913
914 return $yaml;
915 }
916
917
924 public function readYaml($yaml)
925 {
926 $data = [];
927 $currentPackage = null;
928 $currentSection = null;
929
930 foreach (explode("\n", trim($yaml)) as $line) {
931 $trimmedLine = trim($line);
932
933 // Ignore empty lines and comments
934 if ($trimmedLine === '' || strpos($trimmedLine, '#') === 0) {
935 continue;
936 }
937
938 // Match a new package entry (e.g., "- modulename: 'helloasso'") - Found a break in file.
939 $matches = array();
940 if (preg_match('/^\s*-\s*modulename:\s*["\']?(.*?)["\']?$/', $trimmedLine, $matches)) {
941 if ($currentPackage !== null) {
942 // Add the package to $data
943 if (!empty($currentPackage['status']) && in_array($currentPackage['status'], array('enabled', 'soon'))) {
944 $data[] = $currentPackage;
945 }
946 }
947 $currentPackage = ['modulename' => $matches[1]];
948 $currentSection = null;
949 continue;
950 }
951
952 // If the key doesn't start with fr, en, es, it, de, treat it as a section
953 if (!preg_match('/^\s*(fr|en|es|it|de):\s*["\']?(.*?)["\']?$/', $trimmedLine)) {
954 $currentSection = null;
955 }
956
957 // Match a top-level key-value pair (e.g., "author: 'Dolicloud'")
958 if (preg_match('/^(\w[\w-]*):\s*["\']?(.*?)["\']?$/', $trimmedLine, $matches)) {
959 if ($currentPackage !== null) {
960 if ($currentSection) {
961 // Store in the sub section (language into label or description for example)
962 $currentPackage[$currentSection][$matches[1]] = $matches[2] === '' ? null : $matches[2];
963 } else {
964 // Store as a normal key-value pair
965 $currentPackage[$matches[1]] = $matches[2] === '' ? null : $matches[2];
966 }
967 }
968
969 // Match a nested section (e.g., "label:")
970 if (preg_match('/^\s*(label|description):\s*$/', $trimmedLine, $matches)) {
971 $currentSection = $matches[1];
972 $currentPackage[$currentSection] = []; // Initialize as an empty array for nested sections
973 }
974
975 continue;
976 }
977 }
978
979 // Add the last package if available
980 if ($currentPackage !== null) {
981 if (!empty($currentPackage['status']) && in_array($currentPackage['status'], array('enabled', 'soon'))) {
982 $data[] = $currentPackage;
983 }
984 }
985
986 return $data;
987 }
988
996 public function adaptData($data, $source)
997 {
998 $adaptedData = [];
999
1000 if (!is_array($data) || empty($data) || empty($source)) {
1001 return $adaptedData;
1002 }
1003
1004 if ($source === 'githubcommunity') {
1005 foreach ($data as $package) {
1006 if (empty($package['modulename'])) {
1007 continue;
1008 }
1009
1010 // Check if there is a known ID
1011 $reg = array();
1012 $id = 0;
1013 if (!empty($package['dolistore-download']) && preg_match('/www\.dolistore\.com\/product\.php\?id=(\d+)/', (string) $package['dolistore-download'], $reg)) {
1014 $id = $reg[1];
1015 }
1016
1017 $adaptedPackage = [
1018 'id' => $id,
1019 'ref' => str_replace(' ', '', $package['modulename'] . '-' . $package['current_version'] . '@' .
1020 (array_key_exists('author', $package) ? $package['author'] : 'unkownauthor')),
1021 'label' => !empty($package['label'][substr($this->lang, 0, 2)])
1022 ? $package['label'][substr($this->lang, 0, 2)]
1023 : (!empty($package['label']['en']) ? $package['label']['en'] : $package['modulename']),
1024 'description' => !empty($package['description'][substr($this->lang, 0, 2)])
1025 ? $package['description'][substr($this->lang, 0, 2)]
1026 : (!empty($package['description']['en']) ? $package['description']['en'] : ''),
1027 'datec' => (!empty($package['created_at']) && is_string($package['created_at']))
1028 ? date('Y-m-d H:i:s', strtotime($package['created_at']))
1029 : '',
1030 'tms' => (!empty($package['last_updated_at']) && is_string($package['last_updated_at']))
1031 ? date('Y-m-d H:i:s', strtotime($package['last_updated_at']))
1032 : '',
1033 'author' => array_key_exists('author', $package) ? $package['author'] : '',
1034 'author_url' => array_key_exists('author_url', $package) ? $package['author_url'] : '',
1035 'dolibarr_min' => !empty($package['dolibarrmin'])
1036 ? $package['dolibarrmin']
1037 : 'unknown',
1038 'dolibarr_max' => !empty($package['dolibarrmax'])
1039 ? $package['dolibarrmax']
1040 : 'unknown',
1041 'phpmin' => !empty($package['phpmin'])
1042 ? $package['phpmin']
1043 : 'unknown',
1044 'phpmax' => !empty($package['phpmax'])
1045 ? $package['phpmax']
1046 : 'unknown',
1047 'module_version' => !empty($package['current_version'])
1048 ? $package['current_version']
1049 : 'unknown',
1050 'cover_photo_url' => !empty($package['cover'])
1051 ? $package['cover']
1052 : '#',
1053 'category' => (!empty($package['category']) && is_string($package['category']))
1054 ? explode(',', str_replace(' ', '', (string) $package['category']))
1055 : array(),
1056 'link' => !empty($package['git'])
1057 ? $package['git']
1058 : '#',
1059 'source' => 'githubcommunity',
1060 'status' => !empty($package['status']) ? $package['status'] : '',
1061 'direct-download' => !empty($package['direct-download'])
1062 ? $package['direct-download']
1063 : '',
1064 'dolistore-download' => !empty($package['dolistore-download'])
1065 ? $package['dolistore-download']
1066 : '',
1067 ];
1068
1069 // If a price entry exists
1070 if (array_key_exists('price', $package) && $package['price'] != null) {
1071 $adaptedPackage['price_ht'] = $package['price'];
1072 }
1073
1074 $adaptedData[] = $adaptedPackage;
1075 }
1076 }
1077
1078 if ($source === 'dolistore') {
1079 foreach ($data as $package) {
1080 $urlphoto = $this->shop_url.$package['cover_photo_url'];
1081
1082 if (preg_match('/^\/?wrapper\.php\?hashp=/', $package['cover_photo_url']) && !preg_match('/attachment=/', $package['cover_photo_url'])) {
1083 $urlphoto .= '&attachment=0';
1084 }
1085
1086 $adaptedPackage = [
1087 'id' => $package['id'],
1088 'ref' => $package['ref'],
1089 'label' => $package['label'],
1090 'description' => $package['description'],
1091 'datec' => $package['datec'],
1092 'tms' => $package['tms'],
1093 'author' => array_key_exists('author', $package) ? $package['author'] : '',
1094 'author_url' => array_key_exists('author_url', $package) ? $package['author_url'] : '',
1095 'price_ttc' => $package['price_ttc'],
1096 'price_ht' => $package['price_ht'],
1097 'dolibarr_min' => $package['dolibarr_min'],
1098 'dolibarr_max' => $package['dolibarr_max'],
1099 'phpmin' => empty($package['phpmin']) ? '' : $package['phpmin'],
1100 'phpmax' => empty($package['phpmax']) ? '' : $package['phpmax'],
1101 'module_version' => $package['module_version'],
1102 'cover_photo_url' => $urlphoto,
1103 'source' => 'dolistore',
1104 'status' => empty($package['status']) ? '' : $package['status']
1105 ];
1106
1107 $adaptedData[$package['id']] = $adaptedPackage;
1108 }
1109 }
1110
1111 return $adaptedData;
1112 }
1113
1121 public function applyFilters($list, $options)
1122 {
1123 $filteredData = $list;
1124
1125 // Sort products list by datec
1126 usort(
1127 $filteredData,
1135 static function ($a, $b) {
1136 return strtotime($b['datec'] ?? '0') - strtotime($a['datec'] ?? '0');
1137 }
1138 );
1139
1140 if (!empty($options['search'])) {
1141 $filteredData = array_filter(
1142 $filteredData,
1150 static function ($package) use ($options) {
1151 return stripos($package['label'], $options['search']) !== false || stripos($package['description'], $options['search']) !== false;
1152 }
1153 );
1154 }
1155
1156 if (!empty($options['categorieid'])) {
1157 $filteredData = array_filter(
1158 $filteredData,
1166 static function ($package) use ($options) {
1167 return in_array($options['categorieid'], $package['category']);
1168 }
1169 );
1170 }
1171
1172 $total = count($filteredData);
1173
1174 // Pagination
1175 $filteredData = array_values($filteredData);
1176 $filteredData = array_slice($filteredData, ($options['page'] - 1) * $options['limit'], $options['limit']);
1177
1178 return ['total' => $total, 'data' => $filteredData];
1179 }
1180
1186 public function checkApiStatus()
1187 {
1188 // Call remote API
1189 $testRequest = $this->callApi('categories');
1190
1191 if (!isset($testRequest['response']) || !is_array($testRequest['response']) || ($testRequest['status_code'] != 200 && $testRequest['status_code'] != 201)) {
1192 $this->dolistoreApiError = $this->checkStatusCode($testRequest);
1193 return 0;
1194 } else {
1195 return 1;
1196 }
1197 }
1198
1207 public function libStatus($status, $mode = 3, $moretext = '')
1208 {
1209 global $langs;
1210
1211 $statusType = 'status4';
1212 if ($status == 0) {
1213 $statusType = 'status8';
1214 }
1215
1216 $labelStatus = [];
1217 $labelStatusShort = [];
1218
1219 $labelStatus[0] = $langs->transnoentitiesnoconv("NotConnected");
1220 $labelStatus[1] = $langs->transnoentitiesnoconv("online");
1221 $labelStatusShort[0] = $langs->transnoentitiesnoconv("NotConnected");
1222 $labelStatusShort[1] = $langs->transnoentitiesnoconv("online");
1223
1224 return dolGetStatus($labelStatus[$status], $labelStatusShort[$status], '', $statusType, $mode, '', array('badgeParams' => array('attr' => array('class' => 'classfortooltip', 'title' => $labelStatusShort[$status].$moretext))));
1225 }
1226}
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:48
Class ExternalModules.
__construct($debug=false)
Constructor.
libStatus($status, $mode=3, $moretext='')
Retrieve the status icon.
checkApiStatus()
Check if an Dolistore API is up.
getProducts($options)
Generate HTML for products.
versionCompare($v1, $v2)
version compare
getPagination()
Generate pagination for navigating through pages of products.
getRemoteYamlFile($file_source_url, $cache_time)
Get YAML file from remote source and put it into the cache file.
get_previous_link($text='<<')
get previous link
buildSorter(string $key)
Sort an array by a key.
getCategories($active=0)
Generate HTML for categories and their children.
applyFilters($list, $options)
Apply filters to the data.
get_next_link($text='> >')
get next link
fetchModulesFromFile($options=array())
Fetch modules from a cache YAML file.
checkStatusCode($request)
Check the status code of the request.
readYaml($yaml)
Read a YAML string and convert it to an array.
callApi($resource, $options=false)
Test if we can access to remote Dolistore market place.
adaptData($data, $source)
Adapter data fetched from github remote source to the expected format.
loadRemoteSources($debug=false)
loadRemoteSources
get_previous_url()
get previous url
dol_stringtotime($string, $gm=1)
Convert a string date into a GM Timestamps date Warning: YYYY-MM-DDTHH:MM:SS+02:00 (RFC3339) is not s...
Definition date.lib.php:431
dol_is_file($pathoffile)
Return if path is a file.
dol_is_dir($folder)
Test if filename is a directory.
img_picto($titlealt, $picto, $moreatt='', $pictoisfullpath=0, $srconly=0, $notitle=0, $alt='', $morecss='', $marginleftonlyshort=2, $allowothertags=array())
Show picto whatever it's its name (generic function)
dolPrintHTML($s, $allowiframe=0)
Return a string (that can be on several lines) ready to be output on a HTML page.
dol_string_nohtmltag($stringtoclean, $removelinefeed=1, $pagecodeto='UTF-8', $strip_tags=0, $removedoublespaces=1)
Clean a string from all HTML tags and entities.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
dol_now($mode='auto')
Return date for now.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
ajax_autoselect($htmlname, $addlink='', $textonlink='Link')
Make content of an input box selected when we click into input field.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false)
Output date in a string format according to outputlangs (or langs if not defined).
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
dol_mkdir($dir, $dataroot='', $newmask='')
Creation of a directory (this can create recursive subdir)
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
Returns text escaped for inclusion in HTML alt or title or value tags, or into values of HTML input f...
getURLContent($url, $postorget='GET', $param='', $followlocation=1, $addheaders=array(), $allowedschemes=array('http', 'https'), $localurl=0, $ssl_verifypeer=-1, $timeoutconnect=0, $timeoutresponse=0)
Function to get a content from an URL (use proxy if proxy defined).