21include_once DOL_DOCUMENT_ROOT.
'/core/lib/admin.lib.php';
55 public $file_source_url;
82 public $dolistore_api_url;
86 public $dolistore_api_key;
91 public $dolistoreApiStatus;
96 public $dolistoreApiError;
101 public $githubFileStatus;
106 public $githubFileError;
116 public $numberOfProviders;
126 public $numberTotalOfProducts;
131 public $numberTotalOfPages;
136 public $numberOfProducts;
147 $this->debug_api = $debug;
149 $this->url = DOL_URL_ROOT.
'/admin/modules.php?mode=marketplace';
152 $this->dolistore_api_url =
getDolGlobalString(
'MAIN_MODULE_DOLISTORE_API_SRV',
'https://www.dolistore.com/api/');
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');
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';
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)) {
178 $cachedelayforgithubrepo =
getDolGlobalInt(
'MAIN_REMOTE_GITHUBREPO_CACHE_DELAY', 86400);
182 $this->githubFileError = $this->error;
183 $this->githubFileStatus =
dol_is_file($this->cache_file) ? 1 : 0;
192 $this->numberOfProviders = $this->dolistoreApiStatus + $this->githubFileStatus;
202 public function callApi($resource, $options =
false)
205 if (empty($this->dolistore_api_key) || empty($this->dolistore_api_url)) {
206 return array(
'status_code' => 0,
'response' =>
null);
213 $httpheader = array(
'DOLAPIKEY: '.$this->dolistore_api_key);
214 if ($basicAuthLogin) {
215 $httpheader[] =
'Authorization: Basic '.base64_encode($basicAuthLogin.
':'.$basicAuthPassword);
218 $url = $this->dolistore_api_url . (preg_match(
'/\/$/', $this->dolistore_api_url) ?
'' :
'/') . $resource;
220 $options[
'apikey'] = $this->dolistore_api_key;
223 $url .=
'?' . http_build_query($options);
226 $response =
getURLContent($url,
'GET',
'', 1, $httpheader, array(
'https'), 0, -1, 5, 5);
228 $status_code = $response[
'http_code'];
231 if ($status_code == 200) {
232 $body = $response[
'content'];
233 $body = json_decode($body,
true);
234 $returnarray = array(
235 'status_code' => $status_code,
239 $returnarray = array(
240 'status_code' => $status_code,
243 if (!empty($response[
'curl_error_no'])) {
244 $returnarray[
'curl_error_no'] = $response[
'curl_error_no'];
246 if (!empty($response[
'curl_error_msg'])) {
247 $returnarray[
'curl_error_msg'] = $response[
'curl_error_msg'];
264 if (!empty($this->cache_file) && file_exists($this->cache_file)) {
265 dol_syslog(__METHOD__ .
" - Loading cache file: " . $this->cache_file, LOG_DEBUG);
267 $content = file_get_contents($this->cache_file);
268 if ($content !==
false) {
269 $modules = $this->
readYaml($content);
271 dol_syslog(__METHOD__ .
" - Error reading cache file", LOG_ERR);
286 $organized_tree = array();
290 'lang' => $this->lang
295 $resCategories = $this->
callApi(
'categories', $data);
296 if (isset($resCategories[
'response']) && is_array($resCategories[
'response'])) {
297 $organized_tree = $resCategories[
'response'];
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'])) {
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>';
334 $langs->load(
"products");
337 $last_month =
dol_now() - (30 * 24 * 60 * 60);
338 $dolibarrversiontouse = DOL_VERSION;
340 $this->products = array();
343 $installedModules = array();
344 if (is_array($modules)) {
345 foreach ($modules as $objMod) {
346 if (is_object($objMod) && $objMod->isCoreOrExternalModule() !=
'core') {
347 $moduleName = strtolower($objMod->name);
348 $moduleVersion = $objMod->getVersion(0);
349 $installedModules[$moduleName] = $moduleVersion;
354 $this->categorie = $options[
'categorie'] ?? 0;
355 $this->per_page = $options[
'per_page'] ?? 11;
356 $this->no_page = $options[
'no_page'] ?? 1;
357 $this->search = $options[
'search'] ??
'';
359 $this->per_page = 11;
362 if (!empty($this->search) && strlen(str_replace(
' ',
'', (
string) $this->search)) < 2) {
363 $html .=
'<tr class=""><td colspan="3" class="center">';
365 $html .= $langs->trans(
"SearchStringMinLength").
'...';
367 $html .=
'</td></tr>';
372 'categorieid' => $this->categorie,
373 'limit' => $this->per_page,
374 'page' => $this->no_page,
375 'search' => $this->search,
376 'lang' => $this->lang
380 $this->numberTotalOfProducts = 0;
383 if ($this->categorie == 87) {
384 $html =
'<div class="shop-container">
385 <div class="shop-image">
386 <a href="https://merch.dolibarr.org/" target="_blank">
387 <img src="https://www.dolistore.com/medias/image/marketplace/img/goodies-shop.jpg" width="50%" alt="DoliStore Merch and Gifts" />
388 <div class="shop-overlay">
389 <button target="new" class="shop-button">'.$langs->trans(
"GoodiesButtonTitle").
' <i class="icon-chevron-right"></i></button>
400 $dolistoreProducts = array();
401 $dolistoreProductsTotal = 0;
402 if ($this->dolistoreApiStatus > 0 &&
getDolGlobalInt(
'MAIN_ENABLE_EXTERNALMODULES_DOLISTORE')) {
403 $getDolistoreProducts = $this->
callApi(
'products', $data);
405 if (!isset($getDolistoreProducts[
'response']) || !is_array($getDolistoreProducts[
'response']) || ($getDolistoreProducts[
'status_code'] != 200 && $getDolistoreProducts[
'status_code'] != 201)) {
406 $dolistoreProducts = array();
407 $dolistoreProductsTotal = 0;
409 $dolistoreProducts = $this->
adaptData($getDolistoreProducts[
'response'][
'products'],
'dolistore');
410 $dolistoreProductsTotal = (int) $getDolistoreProducts[
'response'][
'total'];
411 $this->numberTotalOfProducts += $dolistoreProductsTotal;
417 $fileProducts = array();
418 $fileProductsTotal = 0;
419 if (!empty($this->githubFileStatus) &&
getDolGlobalInt(
'MAIN_ENABLE_EXTERNALMODULES_COMMUNITY')) {
422 $fileProducts = $this->
adaptData($fileProducts,
'githubcommunity');
424 $fileProducts = $this->
applyFilters($fileProducts, $data);
426 $fileProductsTotal = $fileProducts[
'total'];
428 $this->numberTotalOfProducts += $fileProductsTotal;
430 $fileProducts = $fileProducts[
'data'];
434 $this->numberTotalOfPages = (int) ceil(max($fileProductsTotal / $this->per_page, $dolistoreProductsTotal / $this->per_page));
437 $this->products = $dolistoreProducts;
438 foreach ($fileProducts as $fileProduct) {
439 $id = $fileProduct[
'id'];
441 if (empty($this->products[
$id])) {
442 array_unshift($this->products, $fileProduct);
444 $this->products[
$id] = $fileProduct;
445 $this->products[
$id][
'category'] = $fileProduct[
'category'];
448 array_unshift($this->products, $fileProduct);
454 foreach ($this->products as $product) {
459 if ($last_month < strtotime($product[
'datec']) && $product[
"status"] !=
'soon' && $product[
"status"] !=
'development' && $product[
"status"] !=
'experimental') {
460 $newapp .=
'<span class="newApp" title="'.$product[
'tms'].
'">'.$langs->trans(
'New').
'</span> ';
464 if ($newapp ==
'' && $last_month < strtotime($product[
'tms']) && $product[
"status"] !=
'soon' && $product[
"status"] !=
'development' && $product[
"status"] !=
'experimental') {
465 $newapp .=
'<span class="updatedApp" title="'.$product[
'tms'].
'">'.$langs->trans(
'UpdatedRecently').
'</span> ';
469 if ($product[
"cover_photo_url"] !=
'' && $product[
"cover_photo_url"] !=
'#') {
470 $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"]).
'">';
471 $images .=
'<img class="imgstore" src="'.$product[
"cover_photo_url"].
'" alt="" /></a>';
473 $images =
'<img class="imgstore" src="'.DOL_URL_ROOT.
'/public/theme/common/nophoto.png" />';
479 if ($product[
"status"] ==
'soon' || $product[
"status"] ==
'development' || $product[
"status"] ==
'experimental') {
480 $version =
'<span class="warning">'.$langs->trans(
"NotYetAvailable").
' - '.$langs->trans(
"StillInDevelopment").
'</span>';
481 $compatible =
'NotCompatible';
482 } elseif ($this->
versionCompare($product[
"dolibarr_min"], $dolibarrversiontouse) <= 0) {
483 if (!empty($product[
"dolibarr_max"]) && $product[
"dolibarr_max"] !=
'auto' && $product[
"dolibarr_max"] !=
'unknown' && $this->
versionCompare($product[
"dolibarr_max"], $dolibarrversiontouse) >= 0) {
485 $version =
'<span class="compatible hideonsmartphone">'.$langs->trans(
487 $dolibarrversiontouse,
488 $product[
"dolibarr_min"],
489 $product[
"dolibarr_max"]
494 $version =
'<span class="warning">'.$langs->trans(
496 $dolibarrversiontouse,
497 $product[
"dolibarr_min"],
498 $product[
"dolibarr_max"]
500 $compatible =
'NotCompatible';
503 if ($product[
"dolibarr_min"] ==
'auto' || $product[
"dolibarr_min"] !=
'unknown') {
505 $version =
'<span class="warning">'.$langs->trans(
507 $dolibarrversiontouse,
508 $product[
"dolibarr_min"],
509 $product[
"dolibarr_max"]
511 $compatible =
'NotCompatible';
514 $version =
'<span class="compatibleafterupdate">'.$langs->trans(
515 'CompatibleAfterUpdate',
516 $dolibarrversiontouse,
517 $product[
"dolibarr_min"],
518 $product[
"dolibarr_max"]
520 $compatible =
'NotCompatible';
526 if (array_key_exists(
'price_ht', $product) &&
price2num($product[
"price_ht"]) > 0) {
527 $price =
'<h3>'.price(
price2num($product[
"price_ht"],
'MT'), 0, $langs, 1, -1, -1,
'EUR').
' '.$langs->trans(
"HT").
'</h3>';
529 $download_link =
'<a class="paddingleft paddingright valignmiddle" target="_blank" title="'.$langs->trans(
"View").
'" href="'.$this->shop_url.
'/product.php?id='.((int) $product[
'id']).
'">';
530 $download_link .=
img_picto(
'',
'url',
'class="size2x paddingright"');
531 $download_link .=
'</a>';
533 $download_link =
'#';
534 if ($product[
'source'] ===
'dolistore') {
535 $urlview = $this->shop_url.
'/product.php?id='.((int) $product[
"id"]);
536 $price =
'<h3><a href="'.$urlview.
'" target="_blank">'.$langs->trans(
'SeeOnDoliStore').
'</a></h3>';
537 } elseif ($product[
'source'] ===
'githubcommunity') {
538 if (array_key_exists(
'price_ht', $product) && empty($product[
'price_ht'])) {
539 if ($product[
'status'] ==
'soon') {
540 $price =
'<h3>'.$langs->trans(
'StillInDevelopment').
'</h3>';
542 $price =
'<h3>'.$langs->trans(
'Free').
'</h3>';
545 if ($product[
"dolistore-download"]) {
546 $price =
'<h3><a href="'.$product[
"dolistore-download"].
'" target="_blank">'.$langs->trans(
'SeeOnDoliStore').
'</a></h3>';
548 $price =
'<h3>'.$langs->trans(
'Unknown').
'</h3>';
552 $price =
'<h3>'.$langs->trans(
'Unknown').
'</h3>';
555 if ($product[
'source'] ===
'githubcommunity') {
556 $download_link =
'<a class="paddingleft paddingright valignmiddle" target="_blank" title="'.$langs->trans(
"Sources").
'" href="'.$product[
"link"].
'">';
557 $download_link .=
img_picto(
'',
'file-code',
'class="size2x paddingright colorgrey"');
558 $download_link .=
'</a>';
560 $urlview = $product[
"dolistore-download"];
562 $download_link .=
'<a class="paddingleft paddingright valignmiddle" target="_blank" title="'.$langs->trans(
"View").
'" href="'.$urlview.
'" rel="noopener noreferrer">';
563 $download_link .=
img_picto(
'',
'url',
'class="size2x"');
564 $download_link .=
'</a>';
567 if (!empty($product[
'direct-download']) && $product[
'direct-download'] ==
'yes') {
569 if (preg_match(
'/https:.*\?id=(\d+)$/', $urlview, $reg)) {
570 $urldownload =
'https://www.dolistore.com/_service_download.php?t=free&p='.$reg[1];
571 $download_link .=
'<a class="paddingleft paddingright valignmiddle" target="_blank" title="'.$langs->trans(
"Download").
'" href="'.$urldownload.
'" rel="noopener noreferrer">';
572 $download_link .=
img_picto(
'',
'download',
'class="size2x paddingright"');
574 $download_link .=
'</a>';
577 } elseif ($product[
'source'] ===
'dolistore') {
578 $urlview = $this->shop_url.
'/product.php?id='.((int) $product[
"id"]);
579 $urldownload =
'https://www.dolistore.com/_service_download.php?t=free&p=' . $product[
'id'];
580 $download_link =
'<a class="paddingleft paddingright valignmiddle" target="_blank" title="'.$langs->trans(
"View").
'" href="'.$urlview.
'">';
581 $download_link .=
img_picto(
'',
'url',
'class="size2x"');
582 $download_link .=
'</a>';
583 $download_link .=
'<a class="paddingleft paddingright" target="_blank" title="'.$langs->trans(
"Download").
'" href="'.$urldownload.
'" rel="noopener noreferrer">';
584 $download_link .=
img_picto(
'',
'download',
'class="size2x paddingright"');
586 $download_link .=
'</a>';
590 if (($product[
'direct-download'] && in_array($product[
'direct-download'], array(
'yes',
'dolistore'))) || $product[
'source'] ===
'dolistore') {
593 if ($product[
'source'] ===
'githubcommunity') {
594 $current_version = $product[
'module_version'] ??
'';
595 $module_name = strtolower(preg_replace(
'/@.*$/',
'', $product[
'ref'] ??
''));
598 $module_name = preg_replace(
'/-' . preg_quote($current_version,
'/') .
'$/',
'', $module_name);
600 $urldownload =
'https://github.com/Dolibarr/dolibarr-community-modules/raw/refs/heads/main/dev/build/bin/module_' . $module_name .
'-' . $current_version .
'.zip';
603 $urlview = $product[
"dolistore-download"];
607 if ($product[
"direct-download"] ==
'dolistore' ||
getDolGlobalString(
"MAIN_DOWNLOAD_FROM_DOLISTORE_IN_PRIORITY")) {
608 if (preg_match(
'/https:.*\?id=(\d+)$/', $urlview, $reg)) {
609 $urldownload =
'https://www.dolistore.com/_service_download.php?t=free&p='.$reg[1];
613 if ($product[
'source'] ===
'dolistore') {
614 $urldownload =
'https://www.dolistore.com/_service_download.php?t=free&p=' . $product[
'id'];
618 $disableInstall = ($compatible ===
'NotCompatible') && !
getDolGlobalInt(
'MAIN_FEATURES_LEVEL');
621 $fields = [
'action' =>
'install',
'token' =>
newToken()];
622 foreach ($product as $key => $value) {
623 $fields[
'producttoinstall['.$key.
']'] = $value;
626 $installConfirmMessage = $langs->transnoentities(
627 "extModuleConfirmInstallText",
628 $product[
'label'] ??
'',
629 $product[
'module_version'] ??
'',
630 $product[
'ref'] ??
'',
631 !empty($product[
'tms']) ?
dol_print_date($product[
'tms'],
'%d/%m/%Y') :
''
633 $installConfirmMessage .= $langs->trans(
"Path").
' : '.$urldownload;
636 $buttonLabel = $langs->trans(
"Install");
637 $remoteVersion = $product[
'module_version'] ??
'';
638 $remoteModuleName = strtolower(preg_replace(
'/@.*$/',
'', $product[
'ref'] ??
''));
640 $remoteModuleName = preg_replace(
'/-' . preg_quote($remoteVersion,
'/') .
'$/',
'', $remoteModuleName);
641 if (!empty($installedModules[$remoteModuleName]) && $remoteVersion && $remoteVersion !=
'unknown') {
642 $localVersion = $installedModules[$remoteModuleName];
644 if ($localVersion !=
'unknown') {
645 $versionDiff = $this->
versionCompare($localVersion, $remoteVersion);
646 if ($versionDiff < 0) {
647 $buttonLabel = $langs->trans(
"Upgrade");
648 } elseif ($versionDiff == 0) {
649 $buttonLabel = $langs->trans(
"ReInstall");
654 $install_link =
'<button class="valignmiddle ' . ($disableInstall ?
'butActionRefused' :
'butAction') .
' paddingleft paddingright"'
656 . (!$disableInstall ?
' data-confirm' :
'')
657 . (!$disableInstall ?
' data-fields="' .
dol_escape_htmltag(json_encode($fields)) .
'"' :
'')
659 . (!$disableInstall ?
' data-confirm-title="' .
dol_escape_htmltag($langs->trans(
"extModuleConfirmInstallTitle")) .
'"' :
'')
660 . (!$disableInstall ?
' data-confirm-text="' .
dol_escape_htmltag($installConfirmMessage) .
'"' :
'')
661 . (!$disableInstall ?
' data-confirm-label="' .
dol_escape_htmltag($buttonLabel) .
'"' :
'')
662 .
'>' . $buttonLabel .
'</button>';
667 $html .=
'<tr class="'.(getDolOptimizeSmallScreen() ?
'app' :
'app app2').
' oddeven nohover '.
dol_escape_htmltag($compatible).
'">';
670 $html .=
'<td class="center width150"><div class="newAppParent">';
671 $html .= $newapp.$images;
672 $html .=
'</div></td>';
675 $html .=
'<td class="margeCote minwidth400imp"><h2 class="appTitle">';
677 if (!empty($product[
'author']) && $product[
'author'] !=
'unkownauthor') {
678 $html .=
'<span class="small"> - '.img_picto(
'',
'company',
'class="pictofixedwidth"');
679 if (!empty($product[
'author_url'])) {
680 $html .=
'<a href="'.$product[
'author_url'].
'" target="_blank">'.$product[
'author'].
'</a>';
682 $html .= $product[
'author'];
686 $html .=
'<br><span class="small">';
691 $html .=
'<small class="appDateCreation appRef"> ';
692 if (empty($product[
'tms'])) {
693 $html .=
img_picto($langs->trans(
'DateCreation'),
'calendar',
'class="pictofixedwidth"').
'<span class="opacitymedium"><span class="hideonsmartphone">'.$langs->trans(
"DateCreation").
': </span>';
696 $html .=
img_picto($langs->trans(
'DateModification'),
'calendar',
'class="pictofixedwidth"').
'<span class="opacitymedium">'.
dol_print_date(
dol_stringtotime($product[
'tms']),
'day').
'</span>';
698 $html .=
' ';
700 $html .=
'<div class="appSource inline-block valigntop">';
701 if ($product[
"source"] ==
'dolistore') {
702 $html .=
'<img border="0" title="'.dolPrintHTML($langs->trans(
'Source').
": DoliStore").
'" class="imgautosize valignmiddle inline-block pictofixedwidth" style="height: 14px" src="'.DOL_URL_ROOT.
'/theme/dolistore_squarred.svg">';
703 } elseif ($product[
"source"] ==
'githubcommunity') {
704 $html .=
img_picto($langs->trans(
'Source').
': GitHub community repo',
'group',
'class="pictofixedwidth valignmiddle"');
706 $html .=
img_picto($langs->trans(
'Source').
': '.$langs->trans(
'Other'),
'generic',
'class="pictofixedwidth"');
710 $html .= $langs->trans(
'Ref').
' '.
dolPrintHTML(preg_replace(
'/@.*$/',
'', $product[
"ref"]));
711 $html .=
'</small><br>';
715 if (!empty($product[
'phpmin']) && $product[
'phpmin'] !=
'unknown') {
716 $html .=
' <span class="badge-secondary small" style="padding: 3px; border-radius: 5px">PHP min '.$product[
'phpmin'].
'</span>';
718 if (!empty($product[
'phpmax']) && $product[
'phpmax'] !=
'unknown') {
719 $html .=
' <span class="badge-secondary small" style="padding: 3px; border-radius: 5px">PHP max '.$product[
'phpmax'].
'</span>';
724 $html .=
'<div class="storedesc">'.dolPrintHTML(
dol_string_nohtmltag($product[
"description"])).
'</div>';
728 $html .=
'</tr><tr class="app2 oddeven nohover borderbottom '.dol_escape_htmltag($compatible).
'">';
732 $html .=
'<td class="margeCote center amount'.(getDolOptimizeSmallScreen() ?
' left" colspan="2"' :
'"').
'>';
734 if (($product[
'direct-download'] && in_array($product[
'direct-download'], array(
'yes',
'dolistore'))) || ($product[
'source'] ===
'dolistore' && empty((
float) $product[
'price_ht']))) {
736 $html .= $install_link;
742 $html .=
'<td class="margeCote nowraponall">';
746 $html .= $download_link;
752 if (empty($this->products)) {
754 $langs->load(
"website");
756 $html .=
'<tr class=""><td colspan="'.$colspan.
'" class="center">';
758 $html .= $langs->trans(
"noResultsWereFound").
'...';
760 $html .=
'</td></tr>';
764 $confirmLabel = $langs->trans(
"Install");
765 $cancelLabel = $langs->trans(
"Cancel");
767 $(document).on("click","[data-confirm]",function(){
768 var button = $(this);
769 var confirmTitle = button.data("confirm-title");
770 var confirmText = button.data("confirm-text");
772 buttons[button.data("confirm-label")||\'' .
dol_escape_js($confirmLabel) .
'\'] =
function(){
773 var form = $(
"<form method=\'POST\' style=\'display:none\'>").attr(
"action",
button.data(
"url"));
774 $.each(
button.data(
"fields"),
function(
name, value){
775 form.append($(
"<input type=\'hidden\'>").attr(
"name", name).val(value));
777 $(
"body").append(form);
779 $(
this).dialog(
"close");
781 buttons[\
'' .
dol_escape_js($cancelLabel) .
'\'] =
function(){$(
this).dialog(
"close");};
782 $(
"<div>").html(confirmText).dialog({
791 $this->numberOfProducts = count($this->products);
802 public function buildSorter(string $key): Closure
810 function (array $a, array $b) use ($key) {
811 $valA = isset($a[$key]) && is_scalar($a[$key]) ? (string) $a[$key] : '
';
812 $valB = isset($b[$key]) && is_scalar($b[$key]) ? (string) $b[$key] : '';
814 return strnatcmp($valA, $valB);
825 public function versionCompare($v1, $v2)
828 $v1 = str_replace(array('v
', 'V
'), '', $v1);
829 $v2 = str_replace(array('v
', 'V
'), '', $v2);
831 $v1 = explode('.
', $v1);
832 $v2 = explode('.
', $v2);
835 $count1 = count($v1);
836 $count2 = count($v2);
837 $maxcount = max($count1, $count2);
838 while ($level < $maxcount) {
839 $operande1 = isset($v1[$level]) ? $v1[$level] : 'x
';
840 $operande2 = isset($v2[$level]) ? $v2[$level] : 'x
';
842 if (strtoupper($operande1) == 'X
' || strtoupper($operande2) == 'X
' || $operande1 == '*
' || $operande2 == '*
') {
845 if ($operande1 < $operande2) {
849 if ($operande1 > $operande2) {
854 //print join('.
',$versionarray1).'(
'.count($versionarray1).') /
'.join('.
',$versionarray2).'(
'.count($versionarray2).') =>
'.$ret.'<br>
'."\n";
858 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
865 public function get_previous_link($text = '<<
')
868 return '<a href=
"'.$this->get_previous_url().'" class=
"button">
'.dol_escape_htmltag($text).'</a>
';
871 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
878 public function get_next_link($text = '>>
')
881 return '<a href=
"'.$this->get_next_url().'" class=
"button">
'.dol_escape_htmltag($text).'</a>
';
884 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
890 public function get_previous_url()
893 $param_array = array();
894 if ($this->no_page > 1) {
899 if (!empty($this->search)) {
900 $param_array['search_keyword
'] = $this->search;
902 $param_array['no_page
'] = $this->no_page - $sub;
903 if ($this->categorie != 0) {
904 $param_array['categorie
'] = $this->categorie;
906 $param = http_build_query($param_array);
907 return $this->url."&".$param;
910 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
916 public function get_next_url()
919 $param_array = array();
920 if ($this->products !== null && count($this->products) < $this->per_page) {
925 if (!empty($this->search)) {
926 $param_array['search_keyword
'] = $this->search;
928 $param_array['no_page
'] = $this->no_page + $add;
929 if ($this->categorie != 0) {
930 $param_array['categorie
'] = $this->categorie;
932 $param = http_build_query($param_array);
933 return $this->url."&".$param;
941 public function getPagination()
946 $page = $this->no_page;
947 $limit = $this->per_page;
948 $totalnboflines = $this->numberTotalOfProducts ?: 0;
949 $num = $this->numberOfProducts;
953 // Show navigation bar
955 if ($page > 0 || $num > $limit) {
956 if ($totalnboflines) {
958 $nbpages = $this->numberTotalOfPages;
963 // Show previous page
965 $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>
';
968 $pagelist .= '<li
class=
"pagination">
';
969 $pagelist .= '<label
for=
"page_input">Page </label>
';
970 if ($this->categorie != 0) {
971 $pagelist .= '<input
type=
"hidden" name=
"categorie" value=
"' . $this->categorie . '">
';
973 $pagelist .= '<input
type=
"text" id=
"page_input" name=
"no_page" value=
"'.($page).'" min=
"1" max=
"'.$nbpages.'" class=
"width40 page_input right" oninput=
"if(this.value > '.$nbpages.') this.value='.$nbpages.'">
';
974 $pagelist .= ' /
'.$nbpages;
975 $pagelist .= '</li>
';
978 if ($page < $nbpages) {
979 $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>
';
984 if ($limit || $pagelist) {
985 $html .= '<div
class=
"pagination" style=
"padding: 7px;">
';
992 $html .= ajax_autoselect('.page_input
');
1003 protected function checkStatusCode($request)
1005 // Define error messages
1007 204 => 'No content
',
1008 400 => 'Bad Request
',
1009 401 => 'Unauthorized
',
1011 405 => 'Method Not Allowed
',
1012 500 => 'Internal Server Error
',
1015 // If status code is 200 or 201, return an empty string
1016 if ($request['status_code
'] === 200 || $request['status_code
'] === 201) {
1020 // Get the predefined error message or use a default one
1021 $error_message = $error_messages[$request['status_code
']] ?? 'Unexpected HTTP
status:
' . $request['status_code
'];
1023 // Append error details if available
1024 if (!empty($request['response
']) && isset($request['response
']['errors
']) && is_array($request['response
']['errors
'])) {
1025 foreach ($request['response
']['errors
'] as $error) {
1026 $error_message .= ' - (Code
' . $error['code
'] . '):
' . $error['message
'];
1030 if (!empty($request['curl_error_msg
'])) {
1031 $error_message .= ' -
' . $request['curl_error_msg
'];
1034 // Return the formatted error message
1035 return sprintf('This call to the API failed and returned an HTTP
status of %d. That means: %s.
', $request['status_code
'], $error_message);
1045 public function getRemoteYamlFile($file_source_url, $cache_time)
1048 $cache_file = $this->cache_file;
1049 $cache_folder = dirname($cache_file);
1051 // Check if cache directory exists
1052 if (!dol_is_dir($cache_folder)) {
1053 dol_mkdir($cache_folder, DOL_DATA_ROOT);
1056 if (!file_exists($cache_file) || filemtime($cache_file) < (dol_now() - $cache_time)) {
1057 // We get remote url
1058 $addheaders = array();
1059 $result = getURLContent($file_source_url, 'GET
', '', 1, $addheaders); // TODO Force timeout to 5 s on both connect and response.
1060 if (!empty($result) && $result['http_code
'] == 200) {
1061 $yaml = $result['content
'];
1062 $result = file_put_contents($cache_file, $yaml);
1063 if ($result === false) {
1064 $this->error = 'Failed to create cache file:
' . $cache_file;
1066 dolChmod($cache_file);
1070 $yaml = file_get_contents($cache_file);
1083 public function readYaml($yaml)
1086 $currentPackage = null;
1087 $currentSection = null;
1089 foreach (explode("\n", trim($yaml)) as $line) {
1090 $trimmedLine = trim($line);
1092 // Ignore empty lines and comments
1093 if ($trimmedLine === '
' || strpos($trimmedLine, '#
') === 0) {
1097 // Match a new package entry (e.g., "- modulename: 'helloasso
'") - Found a break in file.
1099 if (preg_match('/^\s*-\s*modulename:\s*[
"\']?(.*?)["\
']?$/', $trimmedLine, $matches)) {
1100 if ($currentPackage !==
null) {
1102 if (!empty($currentPackage[
'status']) && in_array($currentPackage[
'status'], array(
'enabled',
'soon'))) {
1103 $data[] = $currentPackage;
1106 $currentPackage = [
'modulename' => $matches[1]];
1107 $currentSection =
null;
1112 if (!preg_match(
'/^\s*(fr|en|es|it|de):\s*["\']?(.*?)["\']?$/', $trimmedLine)) {
1113 $currentSection =
null;
1117 if (preg_match(
'/^(\w[\w-]*):\s*["\']?(.*?)["\']?$/', $trimmedLine, $matches)) {
1118 if ($currentPackage !==
null) {
1119 if ($currentSection) {
1121 $currentPackage[$currentSection][$matches[1]] = $matches[2] ===
'' ? null : $matches[2];
1124 $currentPackage[$matches[1]] = $matches[2] ===
'' ? null : $matches[2];
1129 if (preg_match(
'/^\s*(label|description):\s*$/', $trimmedLine, $matches)) {
1130 $currentSection = $matches[1];
1131 $currentPackage[$currentSection] = [];
1139 if ($currentPackage !==
null) {
1140 if (!empty($currentPackage[
'status']) && in_array($currentPackage[
'status'], array(
'enabled',
'soon'))) {
1141 $data[] = $currentPackage;
1159 if (!is_array($data) || empty($data) || empty($source)) {
1160 return $adaptedData;
1163 if ($source ===
'githubcommunity') {
1164 foreach ($data as $package) {
1165 if (empty($package[
'modulename'])) {
1172 if (!empty($package[
'dolistore-download']) && preg_match(
'/www\.dolistore\.com\/product\.php\?id=(\d+)/', (
string) $package[
'dolistore-download'], $reg)) {
1178 'ref' => str_replace(
' ',
'', $package[
'modulename'] .
'-' . $package[
'current_version'] .
'@' .
1179 (array_key_exists(
'author', $package) ? $package[
'author'] :
'unkownauthor')),
1180 'label' => !empty($package[
'label'][substr($this->lang, 0, 2)])
1181 ? $package[
'label'][substr($this->lang, 0, 2)]
1182 : (!empty($package[
'label'][
'en']) ? $package[
'label'][
'en'] : $package[
'modulename']),
1183 'description' => !empty($package[
'description'][substr($this->lang, 0, 2)])
1184 ? $package[
'description'][substr($this->lang, 0, 2)]
1185 : (!empty($package[
'description'][
'en']) ? $package[
'description'][
'en'] :
''),
1186 'datec' => (!empty($package[
'created_at']) && is_string($package[
'created_at']))
1187 ?
date(
'Y-m-d H:i:s', strtotime($package[
'created_at']))
1189 'tms' => (!empty($package[
'last_updated_at']) && is_string($package[
'last_updated_at']))
1190 ?
date(
'Y-m-d H:i:s', strtotime($package[
'last_updated_at']))
1192 'author' => array_key_exists(
'author', $package) ? $package[
'author'] :
'',
1193 'author_url' => array_key_exists(
'author_url', $package) ? $package[
'author_url'] :
'',
1194 'dolibarr_min' => !empty($package[
'dolibarrmin'])
1195 ? $package[
'dolibarrmin']
1197 'dolibarr_max' => !empty($package[
'dolibarrmax'])
1198 ? $package[
'dolibarrmax']
1200 'phpmin' => !empty($package[
'phpmin'])
1201 ? $package[
'phpmin']
1203 'phpmax' => !empty($package[
'phpmax'])
1204 ? $package[
'phpmax']
1206 'module_version' => !empty($package[
'current_version'])
1207 ? $package[
'current_version']
1209 'cover_photo_url' => !empty($package[
'cover'])
1212 'category' => (!empty($package[
'category']) && is_string($package[
'category']))
1213 ? explode(
',', str_replace(
' ',
'', (
string) $package[
'category']))
1215 'link' => !empty($package[
'git'])
1218 'source' =>
'githubcommunity',
1219 'status' => !empty($package[
'status']) ? $package[
'status'] :
'',
1220 'direct-download' => !empty($package[
'direct-download'])
1221 ? $package[
'direct-download']
1223 'dolistore-download' => !empty($package[
'dolistore-download'])
1224 ? $package[
'dolistore-download']
1229 if (array_key_exists(
'price', $package) && $package[
'price'] !=
null) {
1230 $adaptedPackage[
'price_ht'] = $package[
'price'];
1233 $adaptedData[] = $adaptedPackage;
1237 if ($source ===
'dolistore') {
1238 foreach ($data as $package) {
1239 $urlphoto = $this->shop_url.$package[
'cover_photo_url'];
1241 if (preg_match(
'/^\/?wrapper\.php\?hashp=/', $package[
'cover_photo_url']) && !preg_match(
'/attachment=/', $package[
'cover_photo_url'])) {
1242 $urlphoto .=
'&attachment=0';
1246 'id' => $package[
'id'],
1247 'ref' => $package[
'ref'],
1248 'label' => $package[
'label'],
1249 'description' => $package[
'description'],
1250 'datec' => $package[
'datec'],
1251 'tms' => $package[
'tms'],
1252 'author' => array_key_exists(
'author', $package) ? $package[
'author'] :
'',
1253 'author_url' => array_key_exists(
'author_url', $package) ? $package[
'author_url'] :
'',
1254 'price_ttc' => $package[
'price_ttc'],
1255 'price_ht' => $package[
'price_ht'],
1256 'dolibarr_min' => $package[
'dolibarr_min'],
1257 'dolibarr_max' => $package[
'dolibarr_max'],
1258 'phpmin' => empty($package[
'phpmin']) ?
'' : $package[
'phpmin'],
1259 'phpmax' => empty($package[
'phpmax']) ?
'' : $package[
'phpmax'],
1260 'module_version' => $package[
'module_version'],
1261 'cover_photo_url' => $urlphoto,
1262 'source' =>
'dolistore',
1263 'status' => empty($package[
'status']) ?
'' : $package[
'status']
1266 $adaptedData[$package[
'id']] = $adaptedPackage;
1270 return $adaptedData;
1282 $filteredData = $list;
1294 static function ($a, $b) {
1295 return strtotime($b[
'datec'] ??
'0') - strtotime($a[
'datec'] ??
'0');
1299 if (!empty($options[
'search'])) {
1300 $filteredData = array_filter(
1309 static function ($package) use ($options) {
1310 return stripos($package[
'label'], $options[
'search']) !==
false || stripos($package[
'description'], $options[
'search']) !==
false;
1315 if (!empty($options[
'categorieid'])) {
1316 $filteredData = array_filter(
1325 static function ($package) use ($options) {
1326 return in_array($options[
'categorieid'], $package[
'category']);
1331 $total = count($filteredData);
1334 $filteredData = array_values($filteredData);
1335 $filteredData = array_slice($filteredData, ($options[
'page'] - 1) * $options[
'limit'], $options[
'limit']);
1337 return [
'total' => $total,
'data' => $filteredData];
1348 $testRequest = $this->callApi(
'categories');
1350 if (!isset($testRequest[
'response']) || !is_array($testRequest[
'response']) || ($testRequest[
'status_code'] != 200 && $testRequest[
'status_code'] != 201)) {
1351 $this->dolistoreApiError = $this->checkStatusCode($testRequest);
1366 public function libStatus($status, $mode = 3, $moretext =
'')
1370 $statusType =
'status4';
1372 $statusType =
'status3';
1376 $labelStatusShort = [];
1378 $labelStatus[0] = $langs->transnoentitiesnoconv(
"NotConnected");
1379 $labelStatus[1] = $langs->transnoentitiesnoconv(
"online");
1380 $labelStatusShort[0] = $langs->transnoentitiesnoconv(
"NotConnected");
1381 $labelStatusShort[1] = $langs->transnoentitiesnoconv(
"online");
1383 return dolGetStatus($labelStatus[$status], $labelStatusShort[$status],
'', $statusType, $mode,
'', array(
'badgeParams' => array(
'attr' => array(
'class' =>
'classfortooltip',
'title' => $labelStatusShort[$status].$moretext))));
1398 if (!function_exists(
'curl_init')) {
1399 dol_syslog(__METHOD__ .
': cURL is not available', LOG_ERR);
1404 if (empty($producttoinstall[
'ref'])) {
1405 dol_syslog(__METHOD__ .
': Missing producttoinstall', LOG_ERR);
1409 $current_version = $producttoinstall[
'module_version'] ??
'';
1410 $module_name = strtolower(preg_replace(
'/@.*$/',
'', $producttoinstall[
'ref'] ??
''));
1413 $module_name = preg_replace(
'/-' . preg_quote($current_version,
'/') .
'$/',
'', $module_name);
1415 if (empty($module_name) || empty($current_version) || $current_version ==
'unknown') {
1416 dol_syslog(__METHOD__ .
': Missing or unknown module name/version for product', LOG_ERR);
1421 $tmpdir =
$conf->admin->dir_temp .
'/remotestoredl';
1424 $downloaded =
false;
1425 switch ($producttoinstall[
'source']) {
1427 if ($producttoinstall[
'id'] > 0) {
1428 $source_url =
'https://www.dolistore.com/_service_download.php?t=free&p=' . $producttoinstall[
'id'];
1429 $downloaded = $this->_downloadFile($source_url, $tmpdir);
1431 dol_syslog(__METHOD__ .
': Dolistore download failed: ' . $source_url, LOG_ERR);
1435 dol_syslog(__METHOD__ .
': Invalid product ID for Dolistore download: ' . $producttoinstall[
'id'], LOG_ERR);
1439 case 'githubcommunity':
1440 if ($producttoinstall[
'direct-download'] && in_array($producttoinstall[
'direct-download'], array(
'yes',
'dolistore'))) {
1441 $source_url =
'https://github.com/Dolibarr/dolibarr-community-modules/raw/refs/heads/main/dev/build/bin/module_' . $module_name .
'-' . $current_version .
'.zip';
1442 $downloaded = $this->_downloadFile($source_url, $tmpdir);
1444 dol_syslog(__METHOD__ .
': GitHub community module download failed: ' . $source_url .
', Try to find a Dolistore link', LOG_WARNING);
1445 if ($producttoinstall[
'id'] > 0) {
1446 $source_url =
'https://www.dolistore.com/_service_download.php?t=free&p=' . $producttoinstall[
'id'];
1447 $downloaded = $this->_downloadFile($source_url, $tmpdir);
1449 dol_syslog(__METHOD__ .
': Dolistore download failed: ' . $source_url, LOG_ERR);
1453 dol_syslog(__METHOD__ .
': No direct download available for this GitHub community module', LOG_ERR);
1458 dol_syslog(__METHOD__ .
': No direct download available for this GitHub community module', LOG_ERR);
1463 dol_syslog(__METHOD__ .
': Unsupported source type: ' . $producttoinstall[
'source'], LOG_ERR);
1467 dol_syslog(__METHOD__ .
': Module downloaded successfully to: ' . $downloaded, LOG_DEBUG);
1485 if (!empty($head[
'header'])) {
1486 if (preg_match_all(
'/Content-Disposition:.*filename=["\']?([^"\';\r\n]+)/i', $head[
'header'], $m)) {
1487 $filename = trim(end($m[1]),
" \t\"'");
1492 if (empty($filename)) {
1493 $filename = basename(parse_url($url, PHP_URL_PATH));
1497 if (empty($filename) || !preg_match(
'/^module_[a-z0-9_]+-[0-9]+\.[0-9]+\.[0-9]+\.zip$/i', $filename)) {
1498 dol_syslog(__METHOD__ .
': Cannot determine filename from URL: ' . $url, LOG_ERR);
1504 if (empty($response[
'content']) || (isset($response[
'http_code']) && $response[
'http_code'] !== 200)) {
1506 __METHOD__ .
': Download failed — HTTP ' . ($response[
'http_code'] ??
'unknown') .
' — ' . $url,
1513 $dest_file = $dest_path .
'/' . $filename;
1518 $writtenfile = file_put_contents(
dol_osencode($dest_file), $response[
'content']);
1519 if ($writtenfile ===
false || $writtenfile === 0) {
1520 dol_syslog(__METHOD__ .
': Cannot write file: ' . $dest_file, LOG_ERR);
1525 dol_syslog(__METHOD__ .
': Downloaded successfully to: ' . $dest_file, LOG_DEBUG);
$id
Support class for third parties, contacts, members, users or resources.
__construct($debug=false)
Constructor.
libStatus($status, $mode=3, $moretext='')
Retrieve the status icon.
checkApiStatus()
Check if an Dolistore API is up.
getProducts($options, $modules=array())
Generate HTML for products.
versionCompare($v1, $v2)
version compare
getRemoteYamlFile($file_source_url, $cache_time)
Get YAML file from remote source and put it into the cache file.
buildSorter(string $key)
Sort an array by a key.
getModuleZIP($producttoinstall=array())
Download a Dolibarr module from a Git repository URL or Dolistore download URL.
getCategories($active=0)
Generate HTML for categories and their children.
applyFilters($list, $options)
Apply filters to the data.
fetchModulesFromFile($options=array())
Fetch modules from a cache YAML file.
_downloadFile(string $url, string $dest_path)
Download a remote URL to a local file using getURLContent (native Dolibarr).
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
print $langs trans("Ref").' m titre as m m statut as status
Or an array listing all the potential status of the object: array: int of the status => translated la...
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...
dol_is_file($pathoffile)
Return if path is a file.
dol_now($mode='gmt')
Return date for now.
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, $moreallowedtags=array())
Return a string (that can be on several lines) ready to be output on a HTML page.
dol_osencode($str)
Return a string encoded into OS filesystem encoding.
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 '.
getDolOptimizeSmallScreen()
Return if render must be optimized for small screen.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_escape_js($stringtoescape, $mode=0, $noescapebackslashn=0)
Returns text escaped for inclusion into JavaScript code.
newToken()
Return the value of token currently saved into session with name 'newtoken'.
dolGetStatus($statusLabel='', $statusLabelShort='', $html='', $statusType='status0', $displayMode=0, $url='', $params=array())
Output the badge of a status.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false, $decorate=0)
Output date in a string format according to outputlangs (or langs if not defined).
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, $otherCurlOptions=array(), $morelogsuffix='')
Function to get a content from an URL (use proxy if proxy defined).
multi select button
0 = Do not include form tag and submit button -1 = Do not include form tag but include submit button
print $langs trans('Date')." left Ref Label right Qty right Price right TotalHT right TotalTTC right right right right right right right right right centpercent right TotalHT right n right VAT right n right TotalVAT right n No sujeto a RE IRPF right TotalLT1 right n right TotalLT2 right n right TotalTTC right n takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency right TotalTTC takeposcustomercurrency right takeposcustomercurrency n right Paid right PaymentTypeShortLIQ right SELECT p pos_change as p datep as date
if(preg_match('/(crypted|dolcrypt):/i', $dolibarr_main_db_pass)||!empty($dolibarr_main_db_encrypted_pass)) $conf db type
'integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter[:Sortfield]]]',...
$conf db name
Only used if Module[ID]Name translation string is not found.