diff --git a/.github/workflows/windows-ci.yaml b/.github/workflows/windows-ci.yaml index c0e44dbb74f..f8441c438fc 100644 --- a/.github/workflows/windows-ci.yaml +++ b/.github/workflows/windows-ci.yaml @@ -114,7 +114,7 @@ jobs: curl "http://${{ env.PHPSERVER_DOMAIN_PORT }}" shell: powershell - name: Run PHPUnit tests - continue-on-error: true + # continue-on-error: true shell: cmd # setting up php.ini, starting the php server are currently in this step run: |- @@ -143,7 +143,7 @@ jobs: cat htdocs/conf/conf.php curl "http://${{ env.PHPSERVER_DOMAIN_PORT }}" REM 'DOSKEY' USED to recover error code (no pipefile equivalent in windows?) - ( php "%PHPROOT%\phpunit" -d memory_limit=-1 -c %CD%\test\phpunit\phpunittest.xml "test\phpunit\AllTests.php" & call doskey /exename=err err=%%^^errorlevel%% ) | "${{ env.TEE }}" "${{ env.PHPUNIT_LOG }}" + ( php "%PHPROOT%\phpunit" -d memory_limit=-1 -c %CD%\test\phpunit\phpunittest.xml "test\phpunit\AllTests.php" --exclude-group WindowsWaitingForFix & call doskey /exename=err err=%%^^errorlevel%% ) | "${{ env.TEE }}" "${{ env.PHPUNIT_LOG }}" for /f "tokens=2 delims==" %%A in ('doskey /m:err') do EXIT /B %%A - name: Convert Raw Log to Annotations uses: mdeweerd/logToCheckStyle@v2024.2.9 diff --git a/dev/tools/github_commits_byversion.sh b/dev/tools/github_commits_byversion.sh index bd3405376cf..eafca6b2e01 100755 --- a/dev/tools/github_commits_byversion.sh +++ b/dev/tools/github_commits_byversion.sh @@ -4,7 +4,7 @@ # # shellcheck disable=1113,2002,2006,2086,2164,2219 -Releases=("3.9" "4.0" "5.0" "6.0" "7.0" "8.0" "9.0" "10.0" "11.0" "12.0" "13.0" "14.0" "15.0" "16.0" "17.0" "18.0" "develop") +Releases=("3.9" "4.0" "5.0" "6.0" "7.0" "8.0" "9.0" "10.0" "11.0" "12.0" "13.0" "14.0" "15.0" "16.0" "17.0" "18.0" "19.0" "develop") let "counter = 0" echo "Copy script into /tmp/github_commits_byversion.sh" @@ -49,4 +49,3 @@ do echo let "counter +=1" done - diff --git a/dev/tools/phan/config.php b/dev/tools/phan/config.php index 0bb240128f8..ea20cb9d1e4 100644 --- a/dev/tools/phan/config.php +++ b/dev/tools/phan/config.php @@ -1,5 +1,6 @@ + * Copyright (C) 2024 Frédéric France */ define('DOL_PROJECT_ROOT', __DIR__.'/../../..'); define('DOL_DOCUMENT_ROOT', DOL_PROJECT_ROOT.'/htdocs'); @@ -475,7 +476,7 @@ return [ // 'PhanPluginUnknownFunctionReturnType', // 'PhanPluginDescriptionlessCommentOnProtectedProperty', 'PhanPluginRedundantAssignmentInGlobalScope', - 'PhanTypeMismatchDeclaredParamNullable', + // 'PhanTypeMismatchDeclaredParamNullable', 'PhanTypeInvalidRightOperandOfAdd', // 'PhanPluginDescriptionlessCommentOnPrivateProperty', // 'PhanUndeclaredVariableDim', // Array initialisation on undeclared var: $abc['x']='ab' @@ -493,7 +494,7 @@ return [ 'PhanRedefineClass', 'PhanRedefineFunction', 'PhanTypeInvalidLeftOperandOfBitwiseOp', - 'PhanTypeMismatchDimAssignment', + // 'PhanTypeMismatchDimAssignment', // 'PhanPluginDescriptionlessCommentOnProtectedMethod', 'PhanPluginPrintfIncompatibleArgumentTypeWeak', 'PhanUndeclaredVariableAssignOp', @@ -535,7 +536,8 @@ return [ // 'PhanTypeInvalidThrowsIsInterface', // 'PhanPluginRedundantAssignmentInLoop', // 'PhanInvalidCommentForDeclarationType', - //'PhanParamSignatureMismatchInternal', + // 'PhanParamSignatureMismatchInternal', + // 'PhanParamSignatureMismatch', // 'PhanPluginEmptyStatementForeachLoop', // 'PhanCompatibleDimAlternativeSyntax', 'PhanInvalidFQSENInClasslike', @@ -656,5 +658,4 @@ return [ 'sockets' => PHAN_DIR . '/stubs/sockets.phan_php', 'zip' => PHAN_DIR . '/stubs/zip.phan_php', ], - ]; diff --git a/dev/tools/phan/config_fixer.php b/dev/tools/phan/config_fixer.php index a588483e3bf..8033331240b 100644 --- a/dev/tools/phan/config_fixer.php +++ b/dev/tools/phan/config_fixer.php @@ -1,4 +1,7 @@ + * Copyright (C) 2024 Frédéric France + */ // Uncomment require_once to enable corresponding fixer @@ -7,8 +10,6 @@ //require_once __DIR__.'/plugins/UrlEncodeStringifyFixer.php'; require_once __DIR__.'/plugins/SelectDateFixer.php'; -/* Copyright (C) 2024 MDW - */ define('DOL_PROJECT_ROOT', __DIR__.'/../../..'); define('DOL_DOCUMENT_ROOT', DOL_PROJECT_ROOT.'/htdocs'); define('PHAN_DIR', __DIR__); diff --git a/htdocs/adherents/class/adherent.class.php b/htdocs/adherents/class/adherent.class.php index 531c089e1f1..3c810864506 100644 --- a/htdocs/adherents/class/adherent.class.php +++ b/htdocs/adherents/class/adherent.class.php @@ -490,7 +490,7 @@ class Adherent extends CommonObject */ public function makeSubstitution($text) { - global $conf, $langs; + global $langs; $birthday = dol_print_date($this->birth, 'day'); diff --git a/htdocs/admin/mails.php b/htdocs/admin/mails.php index e224b75a986..1856e89f875 100644 --- a/htdocs/admin/mails.php +++ b/htdocs/admin/mails.php @@ -273,9 +273,9 @@ if ($action == 'edit') { jQuery("#MAIN_MAIL_SMTP_SERVER").show(); jQuery("#MAIN_MAIL_SMTP_PORT").show(); jQuery("#smtp_server_mess").hide(); - jQuery("#smtp_port_mess").hide(); + jQuery("#smtp_port_mess").hide(); jQuery(".smtp_method").show(); - jQuery(".dkim").hide(); + jQuery(".dkim").hide(); jQuery(".smtp_auth_method").show(); } if (jQuery("#MAIN_MAIL_SENDMODE").val()==\'swiftmailer\') @@ -302,9 +302,9 @@ if ($action == 'edit') { jQuery("#MAIN_MAIL_SMTP_PORT").show(); jQuery("#smtp_server_mess").hide(); jQuery("#smtp_port_mess").hide(); - jQuery(".smtp_method").show(); + jQuery(".smtp_method").show(); jQuery(".dkim").show(); - jQuery(".smtp_auth_method").show(); + jQuery(".smtp_auth_method").show(); } } function change_smtp_auth_method() { @@ -1109,7 +1109,7 @@ if ($action == 'edit') { $formmail->withtopicreadonly = 0; $formmail->withfile = 2; $formmail->withlayout = 1; - $formmail->withaiprompt = 1; + $formmail->withaiprompt = ($action == 'testhtml' ? 'html' : 'text'); $formmail->withbody = (GETPOSTISSET('message') ? GETPOST('message', 'restricthtml') : ($action == 'testhtml' ? $langs->transnoentities("PredefinedMailTestHtml") : $langs->transnoentities("PredefinedMailTest"))); $formmail->withbodyreadonly = 0; $formmail->withcancel = 1; diff --git a/htdocs/admin/modulehelp.php b/htdocs/admin/modulehelp.php index 50e83f65fcb..e076187e563 100644 --- a/htdocs/admin/modulehelp.php +++ b/htdocs/admin/modulehelp.php @@ -2,6 +2,7 @@ /* Copyright (C) 2017 Laurent Destailleur * Copyright (C) 2017 Regis Houssin * Copyright (C) 2022 Charlene Benke + * Copyright (C) 2024 MDW * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -167,7 +168,10 @@ foreach ($modulesdir as $dir) { $familyinfo = array_merge($familyinfo, $objMod->familyinfo); $familykey = key($objMod->familyinfo); } else { - $familykey = empty($objMod->family) ? 'other' : $objMod->family; + $familykey = $objMod->family; + } + if (empty($familykey) || $familykey === null) { + $familykey = 'other'; } $moduleposition = ($objMod->module_position ? $objMod->module_position : '50'); diff --git a/htdocs/ai/ajax/generate_content.php b/htdocs/ai/ajax/generate_content.php index 9b1adf13b84..2d65256ad01 100644 --- a/htdocs/ai/ajax/generate_content.php +++ b/htdocs/ai/ajax/generate_content.php @@ -61,13 +61,20 @@ if (is_null($jsonData)) { } $ai = new Ai($db); -$instructions = dol_string_nohtmltag($jsonData['instructions'], 1, 'UTF-8'); $function = 'textgeneration'; +$instructions = dol_string_nohtmltag($jsonData['instructions'], 1, 'UTF-8'); +$format = empty($jsonData['instructions']) ? '' : $jsonData['instructions']; -$generatedContent = $ai->generateContent($instructions, 'auto', $function); +$generatedContent = $ai->generateContent($instructions, 'auto', $function, $format); if (is_array($generatedContent) && $generatedContent['error']) { - print "Error : " . $generatedContent['message']; + // client errors + if ($generatedContent['code'] >= 400) { + print "Error : " . $generatedContent['message']; + print '
'.$langs->trans('Check Config of Module').''; + } else { + print "Error returned by API call: " . $generatedContent['message']; + } } else { print $generatedContent; } diff --git a/htdocs/ai/class/ai.class.php b/htdocs/ai/class/ai.class.php index 399b3adb1ee..68002b62bb8 100644 --- a/htdocs/ai/class/ai.class.php +++ b/htdocs/ai/class/ai.class.php @@ -1,9 +1,5 @@ - * Copyright (C) 2005-2016 Regis Houssin - * Copyright (C) 2012 J. Fernando Lagrange - * Copyright (C) 2015 Raphaël Doursenaud - * Copyright (C) 2023 Eric Seigne +/* Copyright (C) 2024 Laurent Destailleur * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,13 +16,14 @@ * or see https://www.gnu.org/ */ require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php"; +require_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php'; + /** * Class for AI */ class Ai { - /** * @var DoliDB $db Database object */ @@ -57,13 +54,18 @@ class Ai /** * Generate response of instructions * - * @param string $instructions instruction for generate content - * @param string $model model name ('gpt-3.5-turbo') - * @param string $function code of the feature we want to use ('emailing', 'transcription', 'audiotext', 'imagegeneration', 'translation') + * @param string $instructions Instruction to generate content + * @param string $model Model name ('gpt-3.5-turbo', 'gpt-4-turbo', 'dall-e-3', ...) + * @param string $function Code of the feature we want to use ('textgeneration', 'transcription', 'audiotext', 'imagegeneration', 'translation') + * @param string $format Format for output ('', 'html', ...) * @return mixed $response */ - public function generateContent($instructions, $model = 'auto', $function = 'textgeneration') + public function generateContent($instructions, $model = 'auto', $function = 'textgeneration', $format = '') { + if (empty($this->apiKey)) { + return array('error' => true, 'message' => 'API key is no defined'); + } + if (empty($this->apiEndpoint)) { if ($function == 'textgeneration') { $this->apiEndpoint = 'https://api.openai.com/v1/chat/completions'; @@ -97,6 +99,8 @@ class Ai } } + dol_syslog("Call API for apiEndpoint=".$this->apiEndpoint." apiKey=".substr($this->apiKey, 0, 3).'***********, model='.$model); + try { $configurationsJson = getDolGlobalString('AI_CONFIGURATIONS_PROMPT'); $configurations = json_decode($configurationsJson, true); @@ -115,40 +119,37 @@ class Ai } $fullInstructions = $prePrompt.' '.$instructions.' .'.$postPrompt; - // TODO Replace this with a simple call of getDolURLContent(); - $ch = curl_init($this->apiEndpoint); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + + $payload = json_encode([ 'messages' => [ ['role' => 'user', 'content' => $fullInstructions] ], 'model' => $model - ])); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ + ]); + + $headers = ([ 'Authorization: Bearer ' . $this->apiKey, 'Content-Type: application/json' ]); + $response = getURLContent($this->apiEndpoint, 'POST', $payload, $headers); - $response = curl_exec($ch); - if (curl_errno($ch)) { - throw new Exception('cURL error: ' . curl_error($ch)); - } - - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($statusCode != 200) { - throw new Exception('API request failed with status code ' . $statusCode); + if ($response['http_code'] != 200) { + throw new Exception('API request failed with status code ' . $response['http_code']); } // Decode JSON response - $decodedResponse = json_decode($response, true); + $decodedResponse = json_decode($response['content'], true); // Extraction content $generatedEmailContent = $decodedResponse['choices'][0]['message']['content']; + // If content is not HTML, we convert it into HTML + if (!dol_textishtml($generatedEmailContent)) { + $generatedEmailContent = dol_nl2br($generatedEmailContent); + } + return $generatedEmailContent; } catch (Exception $e) { return array('error' => true, 'message' => $e->getMessage()); - } finally { - curl_close($ch); } } } diff --git a/htdocs/comm/mailing/card.php b/htdocs/comm/mailing/card.php index f44f53071bd..147415be9fa 100644 --- a/htdocs/comm/mailing/card.php +++ b/htdocs/comm/mailing/card.php @@ -845,6 +845,63 @@ if ($action == 'create') { print $htmlother->selectColor(GETPOST('bgcolor'), 'bgcolor', '', 0); print ''; + $formmail = new FormMail($db); + $formmail->withfckeditor = 1; + $formmail->withaiprompt = 'html'; + $formmail->withlayout = 1; + + print ''; + $out = ''; + // Add link to add layout + if ($formmail->withlayout && $formmail->withfckeditor) { + $out .= ''; + $out .= img_picto($langs->trans("FillMessageWithALayout"), 'layout', 'class="paddingrightonly"'); + $out .= $langs->trans("FillMessageWithALayout").'...'; + $out .= '     '; + + $out .= ' + '; + } + + // Add link to add AI content + if ($formmail->withaiprompt && isModEnabled('ai')) { + $out .= ''; + $out .= img_picto($langs->trans("FillMessageWithAIContent"), 'ai', 'class="paddingrightonly"'); + $out .= $langs->trans("FillMessageWithAIContent").'...'; + $out .= ''; + $out .= ''; + } + if ($formmail->withfckeditor) { + $out .= $formmail->getModelEmailTemplate(); + } + if ($formmail->withaiprompt && isModEnabled('ai')) { + $out .= $formmail->getSectionForAIPrompt(); + } + print $out; + print ''; print ''; print '
'; @@ -1195,7 +1252,7 @@ if ($action == 'create') { $formmail->withtopicreadonly = 1; $formmail->withfile = 0; $formmail->withlayout = 0; - $formmail->withaiprompt = 0; + $formmail->withaiprompt = ''; $formmail->withbody = 0; $formmail->withbodyreadonly = 1; $formmail->withcancel = 1; diff --git a/htdocs/comm/remx.php b/htdocs/comm/remx.php index 994e30fb9f1..f1584aa8b30 100644 --- a/htdocs/comm/remx.php +++ b/htdocs/comm/remx.php @@ -2,7 +2,7 @@ /* Copyright (C) 2001-2004 Rodolphe Quiedeville * Copyright (C) 2004-2019 Laurent Destailleur * Copyright (C) 2008 Raphael Bertrand (Resultic) - * Copyright (C) 2019 Frédéric France + * Copyright (C) 2019-2024 Frédéric France * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -45,6 +45,7 @@ $backtopage = GETPOST('backtopage', 'alpha'); // Security check $socid = GETPOSTINT('id') ? GETPOSTINT('id') : GETPOSTINT('socid'); +/** @var User $user */ if ($user->socid > 0) { $socid = $user->socid; } @@ -55,7 +56,7 @@ if ($user->socid > 0) { } $result = restrictedArea($user, 'societe', $id, '&societe', '', 'fk_soc', 'rowid', 0); -$permissiontocreate = ($user->rights->societe->creer || $user->rights->facture->creer); +$permissiontocreate = ($user->hasRight('societe', 'creer') || $user->hasRight('facture', 'creer')); diff --git a/htdocs/core/class/commonnumrefgenerator.class.php b/htdocs/core/class/commonnumrefgenerator.class.php index c4edb202031..60104d09cd7 100644 --- a/htdocs/core/class/commonnumrefgenerator.class.php +++ b/htdocs/core/class/commonnumrefgenerator.class.php @@ -132,7 +132,7 @@ abstract class CommonNumRefGenerator * Checks if the numbers already in the database do not * cause conflicts that would prevent this numbering working. * - * @param Object $object Object we need next value for + * @param CommonObject $object Object we need next value for * @return boolean false if conflict, true if ok */ public function canBeActivated($object) diff --git a/htdocs/core/class/commonobject.class.php b/htdocs/core/class/commonobject.class.php index 549d76c1cd8..8b89f9f08c1 100644 --- a/htdocs/core/class/commonobject.class.php +++ b/htdocs/core/class/commonobject.class.php @@ -3280,8 +3280,8 @@ abstract class CommonObject if ($this->db->num_rows($resql) > 0) { while ($row = $this->db->fetch_row($resql)) { $rows[] = $row[0]; - if (!empty($includealltree)) { - $rows = array_merge($rows, $this->getChildrenOfLine($row[0]), $includealltree); + if ($includealltree) { + $rows = array_merge($rows, $this->getChildrenOfLine($row[0], $includealltree)); } } } @@ -7928,7 +7928,11 @@ abstract class CommonObject if (!empty($value)) { $checked = ' checked '; } - $value = ''; + if (getDolGlobalInt('MAIN_OPTIMIZEFORTEXTBROWSER') < 2) { + $value = ''; + } else { + $value = yn($value ? 1 : 0); + } } elseif ($type == 'mail' || $type == 'email') { $value = dol_print_email($value, 0, 0, 0, 64, 1, 1); } elseif ($type == 'url') { diff --git a/htdocs/core/class/conf.class.php b/htdocs/core/class/conf.class.php index 647354ef247..a8148548443 100644 --- a/htdocs/core/class/conf.class.php +++ b/htdocs/core/class/conf.class.php @@ -41,9 +41,14 @@ class Conf extends stdClass */ public $db; - //! To store properties found into database + /** + * @var Object To store global setup found into database + */ public $global; - //! To store browser info (->name, ->os, ->version, ->ua, ->layout, ...) + + /** + * @var Object To store browser info (->name, ->os, ->version, ->ua, ->layout, ...) + */ public $browser; //! To store some setup of generic modules diff --git a/htdocs/core/class/extrafields.class.php b/htdocs/core/class/extrafields.class.php index f849094195b..9a8e1ea8a0f 100644 --- a/htdocs/core/class/extrafields.class.php +++ b/htdocs/core/class/extrafields.class.php @@ -1121,7 +1121,6 @@ class ExtraFields } else { $checked = ' value="1" '; } - $out = ''; } else { $out = $form->selectyesno($keyprefix.$key.$keysuffix, $value, 1, false, 1); @@ -1691,7 +1690,11 @@ class ExtraFields if (!empty($value)) { $checked = ' checked '; } - $value = ''; + if (getDolGlobalInt('MAIN_OPTIMIZEFORTEXTBROWSER') < 2) { + $value = ''; + } else { + $value = yn($value ? 1 : 0); + } } elseif ($type == 'mail') { $value = dol_print_email($value, 0, 0, 0, 64, 1, 1); } elseif ($type == 'ip') { diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php index 51324da5753..014ec5486ae 100644 --- a/htdocs/core/class/html.form.class.php +++ b/htdocs/core/class/html.form.class.php @@ -9681,8 +9681,8 @@ class Form $stringforfirstkey .= ' CTL +'; } - $previous_ref = $object->ref_previous ? '' : ''; - $next_ref = $object->ref_next ? '' : ''; + $previous_ref = $object->ref_previous ? '' : ''; + $next_ref = $object->ref_next ? '' : ''; } //print "xx".$previous_ref."x".$next_ref; @@ -9696,8 +9696,8 @@ class Form if ($previous_ref || $next_ref || $morehtml) { $ret .= '