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"');
573 $download_link .=
'</a>';
576 } elseif ($product[
'source'] ===
'dolistore') {
577 $urlview = $this->shop_url.
'/product.php?id='.((int) $product[
"id"]);
578 $urldownload =
'https://www.dolistore.com/_service_download.php?t=free&p=' . $product[
'id'];
579 $download_link =
'<a class="paddingleft paddingright valignmiddle" target="_blank" title="'.$langs->trans(
"View").
'" href="'.$urlview.
'">';
580 $download_link .= img_picto(
'',
'url',
'class="size2x"');
581 $download_link .=
'</a>';
582 $download_link .=
'<a class="paddingleft paddingright" target="_blank" title="'.$langs->trans(
"Download").
'" href="'.$urldownload.
'" rel="noopener noreferrer">';
583 $download_link .= img_picto(
'',
'download',
'class="size2x paddingright"');
584 $download_link .=
'</a>';
588 if (($product[
'direct-download'] && in_array($product[
'direct-download'], array(
'yes',
'dolistore'))) || $product[
'source'] ===
'dolistore') {
591 if ($product[
'source'] ===
'githubcommunity') {
592 $current_version = $product[
'module_version'] ??
'';
593 $module_name = strtolower(preg_replace(
'/@.*$/',
'', $product[
'ref'] ??
''));
596 $module_name = preg_replace(
'/-' . preg_quote($current_version,
'/') .
'$/',
'', $module_name);
598 $urldownload =
'https://github.com/Dolibarr/dolibarr-community-modules/raw/refs/heads/main/dev/build/bin/module_' . $module_name .
'-' . $current_version .
'.zip';
601 $urlview = $product[
"dolistore-download"];
605 if ($product[
"direct-download"] ==
'dolistore' ||
getDolGlobalString(
"MAIN_DOWNLOAD_FROM_DOLISTORE_IN_PRIORITY")) {
606 if (preg_match(
'/https:.*\?id=(\d+)$/', $urlview, $reg)) {
607 $urldownload =
'https://www.dolistore.com/_service_download.php?t=free&p='.$reg[1];
611 if ($product[
'source'] ===
'dolistore') {
612 $urldownload =
'https://www.dolistore.com/_service_download.php?t=free&p=' . $product[
'id'];
616 $disableInstall = ($compatible ===
'NotCompatible') && !
getDolGlobalInt(
'MAIN_FEATURES_LEVEL');
619 $fields = [
'action' =>
'install',
'token' =>
newToken()];
620 foreach ($product as $key => $value) {
621 $fields[
'producttoinstall['.$key.
']'] = $value;
624 $installConfirmMessage = $langs->transnoentities(
625 "extModuleConfirmInstallText",
626 $product[
'label'] ??
'',
627 $product[
'module_version'] ??
'',
628 $product[
'ref'] ??
'',
629 !empty($product[
'tms']) ?
dol_print_date($product[
'tms'],
'%d/%m/%Y') :
''
631 $installConfirmMessage .= $langs->trans(
"Path").
' : '.$urldownload;
634 $buttonLabel = $langs->trans(
"Install");
635 $remoteVersion = $product[
'module_version'] ??
'';
636 $remoteModuleName = strtolower(preg_replace(
'/@.*$/',
'', $product[
'ref'] ??
''));
638 $remoteModuleName = preg_replace(
'/-' . preg_quote($remoteVersion,
'/') .
'$/',
'', $remoteModuleName);
639 if (!empty($installedModules[$remoteModuleName]) && $remoteVersion && $remoteVersion !=
'unknown') {
640 $localVersion = $installedModules[$remoteModuleName];
642 if ($localVersion !=
'unknown') {
643 $versionDiff = $this->
versionCompare($localVersion, $remoteVersion);
644 if ($versionDiff < 0) {
645 $buttonLabel = $langs->trans(
"Upgrade");
646 } elseif ($versionDiff == 0) {
647 $buttonLabel = $langs->trans(
"ReInstall");
652 $install_link =
'<button class="valignmiddle ' . ($disableInstall ?
'butActionRefused' :
'butAction') .
' paddingleft paddingright"'
654 . (!$disableInstall ?
' data-confirm' :
'')
655 . (!$disableInstall ?
' data-fields="' .
dol_escape_htmltag(json_encode($fields)) .
'"' :
'')
657 . (!$disableInstall ?
' data-confirm-title="' .
dol_escape_htmltag($langs->trans(
"extModuleConfirmInstallTitle")) .
'"' :
'')
658 . (!$disableInstall ?
' data-confirm-text="' .
dol_escape_htmltag($installConfirmMessage) .
'"' :
'')
659 . (!$disableInstall ?
' data-confirm-label="' .
dol_escape_htmltag($buttonLabel) .
'"' :
'')
660 .
'>' . $buttonLabel .
'</button>';
665 $html .=
'<tr class="'.(getDolOptimizeSmallScreen() ?
'app' :
'app app2').
' oddeven nohover '.
dol_escape_htmltag($compatible).
'">';
668 $html .=
'<td class="center width150"><div class="newAppParent">';
669 $html .= $newapp.$images;
670 $html .=
'</div></td>';
673 $html .=
'<td class="margeCote minwidth400imp"><h2 class="appTitle">';
675 if (!empty($product[
'author']) && $product[
'author'] !=
'unkownauthor') {
676 $html .=
'<span class="small"> - '.img_picto(
'',
'company',
'class="pictofixedwidth"');
677 if (!empty($product[
'author_url'])) {
678 $html .=
'<a href="'.$product[
'author_url'].
'" target="_blank">'.$product[
'author'].
'</a>';
680 $html .= $product[
'author'];
684 $html .=
'<br><span class="small">';
689 $html .=
'<small class="appDateCreation appRef"> ';
690 if (empty($product[
'tms'])) {
691 $html .= img_picto($langs->trans(
'DateCreation'),
'calendar',
'class="pictofixedwidth"').
'<span class="opacitymedium"><span class="hideonsmartphone">'.$langs->trans(
"DateCreation").
': </span>';
694 $html .= img_picto($langs->trans(
'DateModification'),
'calendar',
'class="pictofixedwidth"').
'<span class="opacitymedium">'.
dol_print_date(
dol_stringtotime($product[
'tms']),
'day').
'</span>';
696 $html .=
' ';
698 $html .=
'<div class="appSource inline-block valigntop">';
699 if ($product[
"source"] ==
'dolistore') {
700 $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">';
701 } elseif ($product[
"source"] ==
'githubcommunity') {
702 $html .= img_picto($langs->trans(
'Source').
': GitHub community repo',
'group',
'class="pictofixedwidth valignmiddle"');
704 $html .= img_picto($langs->trans(
'Source').
': '.$langs->trans(
'Other'),
'generic',
'class="pictofixedwidth"');
708 $html .= $langs->trans(
'Ref').
' '.dolPrintHTML(preg_replace(
'/@.*$/',
'', $product[
"ref"]));
709 $html .=
'</small><br>';
713 if (!empty($product[
'phpmin']) && $product[
'phpmin'] !=
'unknown') {
714 $html .=
' <span class="badge-secondary small" style="padding: 3px; border-radius: 5px">PHP min '.$product[
'phpmin'].
'</span>';
716 if (!empty($product[
'phpmax']) && $product[
'phpmax'] !=
'unknown') {
717 $html .=
' <span class="badge-secondary small" style="padding: 3px; border-radius: 5px">PHP max '.$product[
'phpmax'].
'</span>';
722 $html .=
'<div class="storedesc">'.dolPrintHTML(
dol_string_nohtmltag($product[
"description"])).
'</div>';
726 $html .=
'</tr><tr class="app2 oddeven nohover borderbottom '.dol_escape_htmltag($compatible).
'">';
730 $html .=
'<td class="margeCote center amount'.(getDolOptimizeSmallScreen() ?
' left" colspan="2"' :
'"').
'>';
732 if (($product[
'direct-download'] && in_array($product[
'direct-download'], array(
'yes',
'dolistore'))) || ($product[
'source'] ===
'dolistore' && empty((
float) $product[
'price_ht']))) {
734 $html .= $install_link;
740 $html .=
'<td class="margeCote nowraponall">';
744 $html .= $download_link;
750 if (empty($this->products)) {
752 $langs->load(
"website");
754 $html .=
'<tr class=""><td colspan="'.$colspan.
'" class="center">';
756 $html .= $langs->trans(
"noResultsWereFound").
'...';
758 $html .=
'</td></tr>';
762 $confirmLabel = $langs->trans(
"Install");
763 $cancelLabel = $langs->trans(
"Cancel");
765 $(document).on("click","[data-confirm]",function(){
766 var button = $(this);
767 var confirmTitle = button.data("confirm-title");
768 var confirmText = button.data("confirm-text");
770 buttons[button.data("confirm-label")||\'' .
dol_escape_js($confirmLabel) .
'\'] =
function(){
771 var form = $(
"<form method=\'POST\' style=\'display:none\'>").attr(
"action",
button.data(
"url"));
772 $.each(
button.data(
"fields"),
function(
name, value){
773 form.append($(
"<input type=\'hidden\'>").attr(
"name", name).val(value));
775 $(
"body").append(form);
777 $(
this).dialog(
"close");
779 buttons[\
'' .
dol_escape_js($cancelLabel) .
'\'] =
function(){$(
this).dialog(
"close");};
780 $(
"<div>").html(confirmText).dialog({
789 $this->numberOfProducts = count($this->products);
1081 public function readYaml($yaml)
1084 $currentPackage = null;
1085 $currentSection = null;
1087 foreach (explode("\n", trim($yaml)) as $line) {
1088 $trimmedLine = trim($line);
1090 // Ignore empty lines and comments
1091 if ($trimmedLine === '
' || strpos($trimmedLine, '#
') === 0) {
1095 // Match a new package entry (e.g., "- modulename: 'helloasso
'") - Found a break in file.
1097 if (preg_match('/^\s*-\s*modulename:\s*[
"\']?(.*?)["\
']?$/', $trimmedLine, $matches)) {
1098 if ($currentPackage !==
null) {
1100 if (!empty($currentPackage[
'status']) && in_array($currentPackage[
'status'], array(
'enabled',
'soon'))) {
1101 $data[] = $currentPackage;
1104 $currentPackage = [
'modulename' => $matches[1]];
1105 $currentSection =
null;
1110 if (!preg_match(
'/^\s*(fr|en|es|it|de):\s*["\']?(.*?)["\']?$/', $trimmedLine)) {
1111 $currentSection =
null;
1115 if (preg_match(
'/^(\w[\w-]*):\s*["\']?(.*?)["\']?$/', $trimmedLine, $matches)) {
1116 if ($currentPackage !==
null) {
1117 if ($currentSection) {
1119 $currentPackage[$currentSection][$matches[1]] = $matches[2] ===
'' ? null : $matches[2];
1122 $currentPackage[$matches[1]] = $matches[2] ===
'' ? null : $matches[2];
1127 if (preg_match(
'/^\s*(label|description):\s*$/', $trimmedLine, $matches)) {
1128 $currentSection = $matches[1];
1129 $currentPackage[$currentSection] = [];
1137 if ($currentPackage !==
null) {
1138 if (!empty($currentPackage[
'status']) && in_array($currentPackage[
'status'], array(
'enabled',
'soon'))) {
1139 $data[] = $currentPackage;