16 Commits

Author SHA1 Message Date
f2a7e6d1fc upgrade 2025-08-14 22:51:34 +02:00
cc0600450b upgrade 2025-08-14 22:47:59 +02:00
59bd7154fb upgrade
Some checks failed
Behat tests 1.11.x 🐞 / PHP 7.4 Test on ubuntu-latest (push) Has been cancelled
PHP-CS-Fixer / composer_install (7.4) (push) Has been cancelled
2025-08-14 22:47:23 +02:00
0f518027b4 upgrade 2025-08-14 22:46:17 +02:00
791cb748ab upgrade 2025-08-14 22:44:47 +02:00
8ce45119b6 upgrade 2025-08-14 22:41:49 +02:00
2de81ccc46 upgrade 2025-08-14 22:40:35 +02:00
5403f346e3 upgrade 2025-08-14 22:39:38 +02:00
3641e93527 upgrade 2025-08-14 22:37:50 +02:00
fb6d5d5926 upgrade 2025-08-14 22:37:11 +02:00
1c9e806972 upgrade 2025-08-14 22:36:35 +02:00
f00a2c32bb upgrade 2025-08-14 22:35:49 +02:00
03baefa35c bin upgrade 2025-08-14 22:34:55 +02:00
2fe1c43b3e upgrade app 2025-08-14 22:33:03 +02:00
d862a535e5 limpieza antes de upgrade 2025-08-14 22:25:42 +02:00
ca1a529e28 Actualización 2025-04-10 13:59:00 +02:00
1051 changed files with 34870 additions and 9260 deletions

View File

@@ -28,30 +28,28 @@ RewriteRule ^courses/([^/]+)/index.php$ main/course_home/course_home.php?cDir=$1
# Rewrite everything in the scorm folder of a course to the download script
# except JS, CSS and some image files, which can be served directly
RewriteRule ^courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*)$ main/document/download_scorm.php?doc_url=/$2&cDir=$1 [QSA,L]
# Rewrite everything in the document folder of a course to the download script
# Except certificate resources, which might need to be accessible publicly to all
RewriteRule ^courses/([^/]+)/document/certificates/(.*)$ app/courses/$1/document/certificates/$2 [QSA,L]
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that made redirection with space not to work.
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that broke redirections with spaces.
# To fix this issue we did not have a common syntaxis but it work with one of those 2 options :
# changing at the end of the following line [QSA,L] for [QSA,L,B=\x20?] or for "[QSA,L,B= ?,BNP]"
RewriteRule ^courses/([^/]+)/document/(.*)$ main/document/download.php?doc_url=/$2&cDir=$1 [QSA,L]
# changing at the end of the following line [QSA,L] for [QSA,L,B=\x20?] or for "[QSA,L,B= ?,BNP]" (with the quotes)
# We have opted to use the latter by default. If you are encountering "Not found" issues when entering course homepages,
# you might want to try either of the other 2 forms.
RewriteRule ^courses/([^/]+)/document/(.*)$ main/document/download.php?doc_url=/$2&cDir=$1 "[QSA,L,B= ?,BNP]"
# Optimize load of custom per-course icons in courses (avoid download_uploaded_files.php)
RewriteRule ^courses/([^/]+)/upload/course_home_icons/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/upload/course_home_icons/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/course_home_icons/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/upload/course_home_icons/$2 [QSA,L]
# Course upload files
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that made redirection with space not to work.
# To fix this issue we did not have a common syntaxis but it work with one of those 2 options :
# changing at the end of the following line [QSA,L] for [QSA,L,B=\x20?] or for "[QSA,L,B= ?,BNP]"
RewriteRule ^courses/([^/]+)/upload/([^/]+)/(.*)$ main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 [QSA,L]
# See note on line 37
RewriteRule ^courses/([^/]+)/upload/([^/]+)/(.*)$ main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 "[QSA,L,B= ?,BNP]"
# Rewrite everything in the work folder
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that made redirection with space not to work.
# To fix this issue we did not have a common syntaxis but it work with one of those 2 options :
# changing at the end of the following line [QSA,L] for [QSA,L,B=\x20?] or for "[QSA,L,B= ?,BNP]"
RewriteRule ^courses/([^/]+)/work/(.*)$ main/work/download.php?file=work/$2&cDir=$1 [QSA,L]
# See note on line 37
RewriteRule ^courses/([^/]+)/work/(.*)$ main/work/download.php?file=work/$2&cDir=$1 "[QSA,L,B= ?,BNP]"
RewriteRule ^courses/([^/]+)/course-pic85x85.png$ main/inc/ajax/course.ajax.php?a=get_course_image&code=$1&image=course_image_source [QSA,L]
RewriteRule ^courses/([^/]+)/course-pic.png$ main/inc/ajax/course.ajax.php?a=get_course_image&code=$1&image=course_image_large_source [QSA,L]
@@ -87,10 +85,8 @@ RewriteRule ^service/(\d{1,})$ plugin/buycourses/src/service_information.php?ser
RewriteRule ^lti/os$ plugin/ims_lti/outcome_service.php [L]
# Deny direct access to user my files
# Note : since version 2.4.38-3 of Apache a security fix had a side effect that made redirection with space not to work.
# To fix this issue we did not have a common syntaxis but it work with one of those 2 options :
# changing at the end of the following line [QSA,L] for [QSA,L,B=\x20?] or for "[QSA,L,B= ?,BNP]"
RewriteRule ^app/upload/users/([^/]+)/([^/]+)/my_files/(.*)$ main/social/download_my_files.php?user_id=$2&file=$3 [QSA,L]
# See note on line 37
RewriteRule ^app/upload/users/([^/]+)/([^/]+)/my_files/(.*)$ main/social/download_my_files.php?user_id=$2&file=$3 "[QSA,L,B= ?,BNP]"
# Deny access
RewriteRule ^(tests|.git) - [F,L,NC]

View File

@@ -204,4 +204,4 @@ In short, we ask you to send us Pull Requests based on a branch that you create
with this purpose into your repository forked from the original Chamilo repository.
# Documentation
For more information on Chamilo, visit https://1.11.chamilo.org/documentation/index.html
For more information on Chamilo, visit https://11.chamilo.org/documentation/index.html

View File

@@ -9615,6 +9615,23 @@ ul.dropdown-menu.inner > li > a {
padding-bottom: 133.33%;
}
.dropdown-menu li a + .dropdown-menu {
display: block;
left: 0;
top: 0;
box-shadow: none;
margin-top: 0;
border: 0;
position: relative;
padding: 0;
}
.dropdown-menu li a + .dropdown-menu li a {
padding-top: 6px;
padding-bottom: 6px;
padding-right: 9px;
}
/* CSS Responsive */
@media (min-width: 1025px) and (max-width: 1200px) {
.sidebar-scorm {

View File

@@ -48,7 +48,6 @@
"ext-zlib": "*",
"angelfqc/vimeo-api": "2.0.6",
"apereo/phpcas": "^1.6",
"brumann/polyfill-unserialize": "^1.0",
"chamilo/pclzip": "~2.8",
"clue/graph": "~0.9.0",
"culqi/culqi-php": "1.3.4",
@@ -62,7 +61,7 @@
"enshrined/svg-sanitize": "^0.16.0",
"essence/essence": "2.6.1",
"ezyang/htmlpurifier": "~4.9",
"facebook/php-sdk-v4": "~5.0",
"facebook/graph-sdk": "^5.7",
"firebase/php-jwt": "~5.0",
"gedmo/doctrine-extensions": "~2.3",
"graphp/algorithms": "~0.8.0",

477
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -110,6 +110,372 @@
</table>
<div class="version" aria-label="1.11.32">
<a id="1.11.32"></a>
<h1>Chamilo 1.11.32 - Tikal, 27/06/2025</h1>
<h3>Release notes - summary</h3>
<p>Chamilo 1.11.32 is a micro corrective release on top of 1.11.30. For any significant change, please check the 1.11.30 release notes.</p>
<h3>Release name</h3>
<p><a href="https://en.wikipedia.org/wiki/Huaral">Huaral</a> a city in Peru, capital of the Huaral Province in the Department of Lima, on the path to the ancient city of Caral. 1.11.32 being a minor corrective release on top of 1.11.30 (called "Caral"), relating to a small city geographically close to Caral is a subtle wink to the Chamilo community there.</p>
<h3>Security fixes</h3>
<ul aria-live="off">
<li>No new vulnerability detected in this version</li>
</ul>
<h3>Important note</h3>
<p>Chamilo 1.11.30 comes with subtle changes in the root .htaccess file which could affect your system (for example by triggering "Not Found" errors on course homepages) if you use Apache &lt; 2.4.38-3. Please check line 37 of /.htaccess for more info.</p>
<h3>Notable new Features</h3>
<h4>For end-users, teachers and Chamilo admins</h4>
These features are immediately available to users through the web interface.<br />
<ul aria-live="off">
<li>No notable change</li>
</ul>
<h4>For developers and sysadmins</h4>
Although most features here will be used by teachers or Chamilo admins, they require sysadmin privileges to enable them on the server.
<ul aria-live="off">
<li>No notable change</li>
</ul>
<h3>Improvements (minor features) and debug</h3>
In reverse chronological order...
<ul aria-live="off">
<li>[2025-06-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/a039923b7065fb00d2b5b1cfd56e7d4fb2703dbd">a039923b</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6400">GH#6400</a>) Internal: Remove potential double conversion of quotes in api_htmlentites() (change introduced in 1.11.30 to increase support for PHP 8.3) - loosely refs</li>
<li>[2025-06-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/ef7c8b5eb65eb6ffeaa4422fbfbb690e920b28e9">ef7c8b5e</a>) Plugin: OnlyOffice: Bump version to 1.5.0 and merge with exercise-specific code + fix issues saving document details</li>
</ul>
<h3>Stylesheets and theming</h3>
<ul aria-live="off">
<li>No notable style change</li>
</ul>
<h3>Web services</h3>
<ul aria-live="off">
<li>[2025-06-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/ccaf0f6629e77b6ab5dc3e2ca068ec6e5dd0e7ab">ccaf0f66</a> - <a href="https://task.beeznest.com/issues/22611">BT#22611</a>) Webservice: Allow receiving body content in JSON format -</li>
</ul>
<h3>Removals</h3>
<ul aria-live="off">
<li>No notable removal</li>
</ul>
<h3>Known issues</h3>
<ul aria-live="off">
<li>No notable known issue</li>
</ul>
</div>
<div class="version" aria-label="1.11.30">
<a id="1.11.30"></a>
<h1>Chamilo 1.11.30 - Caral, 25/06/2025</h1>
<h3>Release notes - summary</h3>
<p>Chamilo 1.11.30 is a patch release on top of 1.11.28.</p>
<h3>Release name</h3>
<p><a href="https://en.wikipedia.org/wiki/Caral">Caral</a> is an archaeological site in Peru where the remains of the main city of the Caral civilization are found. It is attributed an antiquity of 5,000 years and it is considered the oldest city in the Americas and one of the oldest in the world. We found this reference ironically related to Chamilo 1.11.30 as we are closing towards a 2.0 release.</p>
<h3>Security fixes</h3>
<ul aria-live="off">
<li>[2024-09-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/387808b5e23963cea126414cf068b05e4c31ede8">387808b5</a>) Security: Social: Add sec_token when commenting posts Fix GHSA-33gm-vrgh-m239</li>
<li>[2024-09-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/ad03014235c6f55a4b5d1d9c56e203af525a2280">ad030142</a>) Security: Social: Add sec_token when accepting a friend request Fix GHSA-33gm-vrgh-m239</li>
<li>[2024-09-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/0c4dae40c0b126d77930543fa03ac081d6f38693">0c4dae40</a>) Security: Social: Add sec_token when denying a friend request Fix GHSA-33gm-vrgh-m239</li>
<li>[2024-09-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/5fadf075429329aff59133e833fae715b6de6b4e">5fadf075</a>) Security: Social: Add sec_token when deleting friend Fix GHSA-33gm-vrgh-m239</li>
<li>[2024-10-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/8ff67c3ad239b1668279e0be3f334b333927a445">8ff67c3a</a>) Security: Glossary: Remove XSS Fix GHSA-4wcp-3rh3-7wm4 advisory</li>
<li>[2024-12-02] (<a href="https://github.com/chamilo/chamilo-lms/commit/f9150075246df4ed9755a4a150e25edb468767be">f9150075</a>) Security: Confirm delete action with modal instead of alert Fix advisory GHSA-gw58-89f7-4xgj</li>
<li>[2024-12-02] (<a href="https://github.com/chamilo/chamilo-lms/commit/82cc07edd8ef316e6b36da7c501120d5c0aeb151">82cc07ed</a>) Security: Remove on* attributes through new filter of HTML Purifier Fix advisory GHSA-gw58-89f7-4xgj</li>
<li>[2024-12-02] (<a href="https://github.com/chamilo/chamilo-lms/commit/241c569dde0ad0e34d558ae51271f70438189b0e">241c569d</a>) Security: Remove on* attributes for input text fields Fix advisory GHSA-gw58-89f7-4xgj</li>
<li>[2024-12-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/7212fb2a1e00d68a35c89527569beb91c8433daa">7212fb2a</a> - <a href="https://task.beeznest.com/issues/22273">BT#22273</a>) Work: Security: Sanitize file name that could import document with special characters</li>
<li>[2024-12-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/3075eeba7a3482dc19b7025a317221e4aaa27f6e">3075eeba</a> - <a href="https://task.beeznest.com/issues/22273">BT#22273</a>) Dropbox: Security: Sanitize file name that could import document with special characters</li>
<li>[2024-12-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/822ae55513dce5c7dfa42850c0466ff5ed37355d">822ae555</a>) Security: Plugin: OnlyOffice: Add filtering to new filenames created through the plugin</li>
<li>[2025-01-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/15023ce6301a8b623d789e8e788b2db9bf470547">15023ce6</a>) Display: Sanitize attributes for anchor tag in Display::url function Refs advisory GHSA-gw58-89f7-4xgj</li>
<li>[2025-01-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/89d2026720a9cf65275ac03ea467e09a22370316">89d20267</a>) Glossary: Use entity to save glossary Refs advisory GHSA-gw58-89f7-4xgj</li>
<li>[2025-01-16] (<a href="https://github.com/chamilo/chamilo-lms/commit/beb07770d674fcc9db6df0e59aab107678c28682">beb07770</a>) Security: Remove on-attributes when showing an HTML editor in forms Refs advisory GHSA-24cc-9jp9-rxx6</li>
<li>[2025-01-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/d5c29cf39ac30d7364a52bba4036c3e870412066">d5c29cf3</a>) Security: Prevent XSS when setting image alt text by using the ckeditor image plugin Refs advisory GHSA-24cc-9jp9-rxx6</li>
<li>[2025-02-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/8c4e6436379c31d88acc30cfd1095feed6d43e96">8c4e6436</a> - <a href="https://task.beeznest.com/issues/22421">BT#22421</a>) Security: Fix pattern to remove on* attributes from HTML tags</li>
<li>[2025-04-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/fd0f2a95b21fa28da47d785c88b879a77951b0f7">fd0f2a95</a>) Security: Fix SQL injection vulnerability by escaping dates in SOAP registration script</li>
<li>[2025-04-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/06155f128395acc6745af826150a0bb2b4a68496">06155f12</a>) Security: Fix SQL injection vulnerability by escaping filename when uploading hot potatoes image</li>
<li>[2025-04-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/613eb19a0fac6dd49c233f32ad5428cd29d5a468">613eb19a</a>) Security: Fix SQL injection vulnerability by escaping assoc_handle in openid login</li> </ul>
<li>[2025-04-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/22bb81df8f7062da20a2f6248789f47b221ca705">22bb81df</a>) Security: Improve database query handling in CourseSelectForm by using parameterized queries and simplifying SQL logic</li>
<li>[2025-04-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/4d07349340543b9c913e76f8cb436a4570500ce2">4d073493</a>) Security: Sanitize English name input in sub_language_add.php to prevent dangerous characters</li>
<li>[2025-04-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/f35acf013b72656ac33ac273a9ed579a5d3af3d4">f35acf01</a>) Security: Use FormValidator::addText and FormValidator::addCheckBox instead addElement method</li>
<li>[2025-04-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/afdbd4bb9a9ea17b7740559dd4e05aa13b16480d">afdbd4bb</a>) Security: Plugin: VChamilo: Add clearDatabaseName method to sanitize database names and update usages</li>
<li>[2025-04-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/e1c7879d63172cd7e5e47c9a03ade0e32023e134">e1c7879d</a>) Security: Sanitize language variable inputs and improve variable handling in multiple files</li>
<li>[2025-04-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/e343c764493b457c82d1e85b91a3655b093767ed">e343c764</a>) Security: Sanitize main database name in Virtual.php to prevent unsafe inputs</li>
<li>[2025-04-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/89c67e6630852cf94d288499e2cd6f6a06f9c6f1">89c67e66</a>) Security: Add MIME type validation for image uploads in ck_uploadimage action</li>
<li>[2025-04-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/07f7954f2dd18c4f5a307b2a6fa802d9ce36b827">07f7954f</a>) Security: Plugin: VChamilo: Update import logic to handle 'phar://' paths to import config</li>
<li>[2025-04-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/349062d31533d464feea78d41996bc0857f90f61">349062d3</a>) Security: Refactor hidden input generation using Display::input Replaced manual HTML string construction with the Display::input method for hidden inputs. This improves code readability, maintainability, and aligns with existing utility usage. Security measures with Security::remove_XSS remain intact. See advisory GHSA-7p5f-34rx-49h8</li>
<li>[2025-04-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/aaf975467213bbd340fc7493513fd9570eb5cf9f">aaf97546</a>) Security: Remove unused 'page' parameter from session user forms. See advisory GHSA-h3m8-53j3-xjx8</li>
<li>[2025-04-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/083b1d2b0c29b0cc0313a28165ad47bebae9dcb2">083b1d2b</a>) Security: Add a whitelist of allowed help topics</li>
<li>[2025-05-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/39e0fa88a2ba5dd197e0d8ce7335730b666992a6">39e0fa88</a>) Security: Messages: Ensure accepted friends have invitations sent See advisory GHSA-m5xj-5xf3-rqch</li>
<li>[2025-05-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/790ef513aceacae6fe5b6641145901f04c7992dd">790ef513</a>) Security: Apply XSS removal when importing users See advisory GHSA-hc3c-8p55-xh4r</li>
<li>[2025-05-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/ba7e15d8cfefcd451de939e98d461b17e72eb627">ba7e15d8</a>) Security: Fix case sensitivity in phar file validation Adjusted the `str_starts_with` checks to be case-insensitive by converting paths to lowercase. Also refactored the logic to separate `isPharFile` handling from writable checks, improving readability and error handling with better feedback messages.</li>
<li>[2025-05-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/9fef8f30b41d586cb4f4fc823906c16a12ae0ff4">9fef8f30</a>) Security: Sanitize uploaded file names in a user import process See advisory GHSA-wrx6-5v5r-mmgx</li>
<li>[2025-05-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/4eb2ffed674eedc2e5e524b7a5a66801b20f28ca">4eb2ffed</a>) Security: Initialize the variable to avoid a wrong result by old value See GHSA-qv5j-rq2p-q5w5</li>
<li>[2025-05-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/c3afce072806198c39e36de7b0b72be01ca7c6c2">c3afce07</a>) Security: Initialize variable to avoid wrong result by old value See GHSA-qv5j-rq2p-q5w5</li>
<li>[2025-06-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/a02c1cd04018fc883054a9cbc32024c4c07d7dc6">a02c1cd0</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5972">GH#5972</a>) Documentation: Add security documentation about CSP (Content Security Policy) headers needing to allow unsafe-inline and unsafe-eval for the inline editor to work</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/ead79db4eb034b8c11a3d6036759d083de37530c">ead79db4</a>) Security: Fix XSS in session category See advisory GHSA-p4m6-gwhg-x89f</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/d672d11871628a68f9948b40d0a5ed1a87013bfa">d672d118</a>) Security: Set token validation to set a student as tutor</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/1aaa53da9bc358c5493c5e6de71be825fd8de4f8">1aaa53da</a>) Security: Exercise: Remove XSS when displaying fill in blanks results</li>
</ul>
<h3>Important note</h3>
<p>Chamilo 1.11.30 comes with subtle changes in the root .htaccess file which could affect your system (for example by triggering "Not Found" errors on course homepages) if you use Apache &lt; 2.4.38-3. Please check line 37 of /.htaccess for more info.</p>
<h3>Notable new Features</h3>
<h4>For end-users, teachers and Chamilo admins</h4>
These features are immediately available to users through the web interface.<br />
<ul aria-live="off">
<li>[2024-10-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/775a5452f10c8084e3f4c0e4302b3895a5a06050">775a5452</a> - <a href="https://task.beeznest.com/issues/20900">BT#20900</a>) Plugin: Exercise monitoring and mouse focus tracking allowing for better control of remote exams, akin to some proctoring services</li>
<li>[2024-11-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/8bcf1c1678b37efc01c7776e679856c7dbd0871a">8bcf1c16</a> - <a href="https://task.beeznest.com/issues/22195">BT#22195</a>) Learnpath: Plugin: OnlyOffice: Allow showing PDF files directly in the learning path when ONLYOFFICE plugin is enabled</li>
<li>[2024-12-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/ab1f124dfdf3efe812c5ebe2f58a02d0c591f2ec">ab1f124d</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5395">GH#5395</a>) Plugin: OnlyOffice: Add OnlyOffice viewer by default for corresponding extensions in documents tool when the plugin is enabled</li>
<li>[2024-12-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/918416a8443f1a7861f94be09fc93f76e27129a1">918416a8</a> - <a href="https://task.beeznest.com/issues/22232">BT#22232</a>) Portfolio: List base-course content in sessions when setting 'portfolio_show_base_course_post_in_sessions' is enabled</li>
<li>[2025-01-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/6954d866c7513bcad937215f9061232ca11e8739">6954d866</a>) Plugin: AI Helper: Add DeepSeek support</li>
<li>[2025-02-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/7d9da9c491e255f02c6637829d25ebe9b2c3f9d3">7d9da9c4</a> - <a href="https://task.beeznest.com/issues/22305">BT#22305</a>) User: Add extra fields filter to advanced user search in administration</li>
<li>[2025-02-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/df715cb4b043c88e69fe8e05156cef8b0678cbef">df715cb4</a> - <a href="https://task.beeznest.com/issues/22403">BT#22403</a>) Exercise: Add configuration setting 'exercise_subscribe_session_when_finished_failure' to allow subscribing a user to a specific session when the user has failed a given test</li>
<li>[2025-02-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/4f5406bed5638268f06b55ce78159406a1fb7b3e">4f5406be</a> - <a href="https://task.beeznest.com/issues/22403">BT#22403</a>) Exercise: Add configuration setting 'exercise_text_when_finished_failure' to allow displaying a text when the user has failed</li>
<li>[2025-02-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/905ea485af6821a7cca6561a903114eac0d1990b">905ea485</a> - <a href="https://task.beeznest.com/issues/22396">BT#22396</a>) Tracking: Show progress based on visibles learning paths only (was previously showing global progress based on all learning paths including hidden ones, which made no visual sense)</li>
<li>[2025-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/07072db9fc44277287ed816009ce9fe50a452e75">07072db9</a>) Plugin: H5pImport: Add hook to create course tool in new courses</li>
<li>[2025-03-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/6bc5b60f6fed810a811859e50edcb9a463911100">6bc5b60f</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Add OnlyOffice question type with document editing support</li>
<li>[2025-04-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/7805cd609c425f20fa37153207a51aa170ed1ad1">7805cd60</a> - <a href="https://task.beeznest.com/issues/22464">BT#22464</a>) Calendar: Add option for course agenda to define default participants</li>
<li>[2025-05-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/5012f87432650e06dcf6f59b73871bb806219ebc">5012f874</a> - <a href="https://task.beeznest.com/issues/22617">BT#22617</a>) Document: Add option to define an access start date for any given document</li>
<li>[2025-06-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/92d370cd12eb43f548878188b8171fa80a0b5ecd">92d370cd</a>) Language: Mongolian language added. Requires following query to manually enable in 1.11.30: "INSERT INTO language (original_name, english_name, isocode, dokeos_folder, available) VALUES ('&#1052;&#1086;&#1085;&#1075;&#1086;&#1083; &#1093;&#1101;&#1083;','mongolian','mn','mongolian',1);" - Contributed by Tsogtsaikhan Byambaa</li>
</ul>
<h4>For developers and sysadmins</h4>
Although most features here will be used by teachers or Chamilo admins, they require sysadmin privileges to enable them on the server.
<ul aria-live="off">
<li>[2024-11-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/286b61d167d1ca0b67b09c71511871e64768d36a">286b61d1</a> - <a href="https://task.beeznest.com/issues/21930">BT#21930</a>) Plugin: Azure: Add option to use delta queries when syncing</li>
<li>[2024-12-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/e661cbb2932ba0f8b5324eac4c9330b523994140">e661cbb2</a> - <a href="https://task.beeznest.com/issues/22232">BT#22232</a>) Portfolio: Add configuration setting 'portfolio_show_base_course_post_in_sessions'</li>
<li>[2025-01-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/c1aa647717d20874b7b61dd4f6baab33ee7e0f71">c1aa6477</a>) Session: Enable user session duration manual extension</li>
<li>[2025-01-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/055273bd2444079660436ec10267b50ac58513c6">055273bd</a>) Internal: Set Apache redirect rules in .htaccess to defaults that work with most Apache >2.4.38-3.</li>
<li>[2025-02-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/44f063a249a6cea2b351a15181dc6ed8e634ba10">44f063a2</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Extra Fields: Add support for attendance extra field</li>
<li>[2025-05-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/358089b19a644ca1c900b359e5f7a479432b68c8">358089b1</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Scripts: Add script to change course code. Replaces strings in all HTML files and exercises content</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/85febd572d37916037e86cdea3a304db89fb03c6">85febd57</a> - <a href="https://task.beeznest.com/issues/22320">BT#22320</a>) Cron: Add users import from XLSX cron script with configurable input fields and comparison mechanisms for a first sync</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/493303827207a1c5fc50d8e405c735afcfb2b993">49330382</a> - <a href="https://task.beeznest.com/issues/22676">BT#22676</a>) Display: Add configuration setting 'display_menu_use_course_categories' to use course categories as top horizontal drop-down menu</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/205484522c2555251852f7dfddf48310c56724f9">20548452</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6013">GH#6013</a>) Session: Add configuration setting 'scheduled_announcements_use_base_progress' for scheduled session announcement - by progress</li>
<li>[2025-06-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/e69282cded7b999398a6ebfd769e21d9d219a4eb">e69282cd</a>) Admin: Add configuration setting 'session_import_drh_hide_old_relationships_check_box' to hide the old relationships checkbox on session import view for drh users</li>
<li>[2025-06-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/4fd8e7d607043030f65cf20df583780304734371">4fd8e7d6</a> - <a href="https://task.beeznest.com/issues/22725">BT#22725</a>) Ticket: Add ticket deletion feature</li>
</ul>
<h3>Improvements (minor features) and debug</h3>
In reverse chronological order...
<ul aria-live="off">
<li>[2025-06-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/1d47856bcae8cb8e992b577a4f7ed8dc17b9b755">1d47856b</a> - <a href="https://task.beeznest.com/issues/22725">BT#22725</a>) Language: Ticket: #add partial translation in ES, FR, EN for ticket deletion functionnality</li>
<li>[2025-06-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/735267ddace929bc99e58c0af424e172096a8717">735267dd</a>) Internal: Fix deprecated calls to mb_convert_encoding() to encode to HTML entities. Helps support PHP 8.3</li
<li>[2025-06-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/735267ddace929bc99e58c0af424e172096a8717">735267dd</a>) Internal: Fix deprecated calls to mb_convert_encoding() to encode to HTML entities. Helps support PHP 8.3</li>
<li>[2025-06-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/be94e40ff58eed62e05f369298c950fa7d1297e3">be94e40f</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5021">GH#5021</a>) Internal: Kses: Fix issue preventing loading $kses_allowedentitynames from global scope in PHP 8+</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/0712adb23e29a873cd87f903501dd94dc45089e2">0712adb2</a>) CI: Fix issues setting up test environment for behat</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/50a181d14350a6c91d12c0da074d6983776f28d7">50a181d1</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6384">GH#6384</a>) Ticket: Add option to give session admin the same right as admin, an … (#6384) Author: @yverhenne</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/c3ef857a22c615b637fd0c6a594c5d2ea05ee3a2">c3ef857a</a>) Use null coalescing operator for cleaner code in fill_blanks and question classes</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/62d37abfa192e5c92fd7b629ddf58e491ee1bedd">62d37abf</a>) CI: Test updated version of Chrome install</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/77c1b6cb75751b197c5241f6d337acaf671a1632">77c1b6cb</a>) CI: Force use of PHP 7.4 in automated tests building sequence</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/655cf868fadbe541065a0e6d6cae13ab3baf85ee">655cf868</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5138">GH#5138</a>) Plugin: before_login: Fix error due to (apparently) rogue copy-paste in plugin "Before login". Add information about config and theming to README.md</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/ea2e427e88252c0d66eacbf48bc9dd1c0fede4af">ea2e427e</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5138">GH#5138</a>) Plugin: before_login: Fix error due to (apparently) rogue copy-paste in plugin "Before login". Add information about config and theming to README.md</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/7d21eb8a20da129a0aae4ff01d2b984a2ae8a526">7d21eb8a</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5538">GH#5538</a>) Admin: Modify default expiration date behaviour to avoid adding default expiration period when selecting "Never expires". Affects CSV import and user creation/edition forms</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/056f3fa2204a4f0707b7b2a43d8498cd616df737">056f3fa2</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5581">GH#5581</a>) Plugin: BuyCourses: Fix error in coupon processing</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/3caf822dd5b287c353528cc7f7423a8b19e90e11">3caf822d</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5637">GH#5637</a>) Documentation: Fix RewriteRule for optimization of files access</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/b3e7531115467a2d02a3c66f0536a29b07ce7566">b3e75311</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5637">GH#5637</a>) Internal: Fix .htaccess rules for scorm content and course home icons</li>
<li>[2025-06-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/221e6d80ab21a77839af690dbe3a81728eb48133">221e6d80</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Course: Improve export of quiz questions, assignments in LP, and images in introduction page with mzb format</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/992c6158d16ad090bad08be08fe77c894c3cc8ec">992c6158</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5673">GH#5673</a>) Internal: Fix support for X-Sendfile headers</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/e797ca057c53c631560ffde5e7d79506e3bc004d">e797ca05</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5717">GH#5717</a>) Plugin: Add courseToolDefaultVisibility property to Plugin class to enable course plugins that are not visible by default (set it from the plugin with $this->setCourseToolDefaultVisibility(false); before install_course_fields_in_all_courses()). Enabled on positioning plugin</li>
<li>[2025-06-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/9533472f70cfb58d26c6fd578858ca9678f12458">9533472f</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6185">GH#6185</a>) Internal: Trim spaces around passwords in mail to user on user creation</li>
<li>[2025-06-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/3de62682f90d33efe772cb1f4745e9051dec8ea0">3de62682</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Course: Export HTML documents as Moodle page activities</li>
<li>[2025-06-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/f9726b414556b8268f94c190630c7d2e00b50a5c">f9726b41</a>) Documentation: Add info about Mongolian language to changelog & estimated release date for 1.11.30</li>
<li>[2025-06-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/c55d63dd1c8aea3e3d2a77181fc7b3cc450362c8">c55d63dd</a>) Language: Added comments in data.sql for the addition of Estionian and Mongolian languages in a future major version. The languages are there, but internal policy prevent us from adding them in a minor version (because if would make the database records different from the default of 1.11.0)</li>
<li>[2025-06-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/a2cc79241319626e03c17dc6f2ba9193677972f5">a2cc7924</a>) Language: Update translations</li>
<li>[2025-06-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/19327a6213b4adc1a6b10dd1da01d95c1879e74c">19327a62</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6218">GH#6218</a>) Session: Add send_subscription_notification field when copying sessions (#6218) Author: @nosolored</li>
<li>[2025-06-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/2373eed527b6bc0409ef994f2ac60fcb58028cc7">2373eed5</a>) Attendance: Add group ID to parameters when using tablet vue with signature Author: @TheTomcat14</li>
<li>[2025-06-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/98c42aaf628c2a18a94a81e3de8375f4e0ef9927">98c42aaf</a>) Admin: ensure "login_as" flag is off anytime we logout or login with username/password directly</li>
<li>[2025-06-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/d808c28a9430333f0fed73d33d4f5c3aab044ac8">d808c28a</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6012">GH#6012</a>) Session: Enable user session duration extension days (#6012)</li>
<li>[2025-06-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/6a26e4ab1329093ff88cb21e208e99a1461803a4">6a26e4ab</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6012">GH#6012</a>) Session: Add avatar display in session_user_edit.php for sessions with duration</li>
<li>[2025-06-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/e16b30320c0c0c48803a97b41c2adcf850ad2b03">e16b3032</a> - <a href="https://task.beeznest.com/issues/22714">BT#22714</a>) Learnpath: fix Language variable to reuse an existing one</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/d08274508892d2bb63aca04cd4e8a14187c268ef">d0827450</a> - <a href="https://task.beeznest.com/issues/22175">BT#22175</a>) WYSIWYG: Allow Genially iframes in HTMLPurifier filter</li>
<li>[2025-06-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/18dc815b7022ab203de0b5b35aeb8a0a0d6b618e">18dc815b</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6013">GH#6013</a>) Session: Fix label in schedule announcements for 'progress' type</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/e630edee37d9899566899916fc1bb26727483dc3">e630edee</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5504">GH#5504</a>) Webservice: Update example WS call for add_courses_session</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/1b7e0030d1c347abd72a12715da10cf89a1475f0">1b7e0030</a> - <a href="https://task.beeznest.com/issues/22714">BT#22714</a>) Internal: Add labels to addmultiselect element for formvalidator</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/874f59ceb7f7670aea1b938025449cc00c10ee21">874f59ce</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6030">GH#6030</a>) Setitngs: Language: Fix PlatformLanguage visualization when on multi-URL configuration</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/24367776fd66ff28b136ac4bc6386b16b5af968c">24367776</a> - <a href="https://task.beeznest.com/issues/22710">BT#22710</a>) Portfolio: Keep user seleccion context when going back to post list from post view</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/995c4fb025569a2475b3fe0bad480a30a25b0a76">995c4fb0</a> - <a href="https://task.beeznest.com/issues/22711">BT#22711</a>) Portfolio: Fix encoding error to show all post by alphabetical order</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/d27e1216fc741120360d2bcc9a2dd878423bc31f">d27e1216</a> - <a href="https://task.beeznest.com/issues/22711">BT#22711</a>) Portfolio: Fix encoding error to show all post by alphabetical order</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/23c53c9ad6fe2ea95a3636b9ad4dba40eb177d9b">23c53c9a</a> - <a href="https://task.beeznest.com/issues/22711">BT#22711</a>) Portfolio: Fix option to show all post by alphabetical order and set only one link</li>
<li>[2025-06-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/2d0b264931942061baedf0e11249556deb7e7a0c">2d0b2649</a> - <a href="https://task.beeznest.com/issues/22711">BT#22711</a>) Portfolio: Add option to show all post by alphabetical order</li>
<li>[2025-06-16] (<a href="https://github.com/chamilo/chamilo-lms/commit/b66bb19a92cefe68dcca7c2cf66f0331ba0c436c">b66bb19a</a> - <a href="https://task.beeznest.com/issues/22688">BT#22688</a>) Ticket: Fix advanced ticket search to select all type of user since tickets can be assign to all type of user</li>
<li>[2025-06-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/340deb6298899a9d061be219ebdc33e060ce942b">340deb62</a>) Plugin: Azure: Update resource for auth code with new Microsoft Graph API</li>
<li>[2025-06-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/70d71006bd8f73a87ca0ca95159b4929b6f49900">70d71006</a> - <a href="https://task.beeznest.com/issues/22691">BT#22691</a>) Session: #fix session per duration visibility management for user_portal page</li>
<li>[2025-06-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/e1b3af971b9f67c0b2f49a7dc06aed8f5559bb51">e1b3af97</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Scripts: Replace course code in cidReq param</li>
<li>[2025-06-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/ee656f0f3ac27310028136e8cc18899c198ad056">ee656f0f</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Scripts: Fix tables fields to replace course code</li>
<li>[2025-06-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/d1bf59068f0a0fb612a655874e9fcbf6ef446399">d1bf5906</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Scripts: Use the appropriate directory name when replacing the course in replace_course_code.php</li>
<li>[2025-06-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/4e8be04358ed0e115293077aba849b7bb389b1fa">4e8be043</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Simplify variable initialization using null coalescing operator</li>
<li>[2025-06-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/3917784c1daed663c2e4df0cc5cee238b2f04173">3917784c</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Scripts: Improve action messages in replace_course_code</li>
<li>[2025-06-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/94127eecb4175173c16361c853687c2e09109db9">94127eec</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Scripts: Add missing tables to replace course code</li>
<li>[2025-06-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/84d4f67eb52765bf8e8aa3d17b3851f5c0a103e4">84d4f67e</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Refactoring ExerciseLib::replaceTermsInContent function to avoid repeat code</li>
<li>[2025-06-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/cf21ea40654b5753583948bff57d9584c90920d2">cf21ea40</a>) Internal: Fix fclose() call to avoid undetected error</li>
<li>[2025-05-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/982bbfaa7db30c2f80a571ba6fb2b1f2cd3a5844">982bbfaa</a> - <a href="https://task.beeznest.com/issues/22634">BT#22634</a>) Ticket: #fix advanced search to give result</li>
<li>[2025-05-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/c4d98f1dcedcdfb4b566ac7eeb4568896d3e7b84">c4d98f1d</a> - <a href="https://task.beeznest.com/issues/22647">BT#22647</a>) Plugin: H5P: Add missing translations for config option</li>
<li>[2025-05-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/8887610a86a0b6be2c6dd1097da66e15a9b48047">8887610a</a> - <a href="https://task.beeznest.com/issues/22647">BT#22647</a>) Plugin: H5P: Fix typo to include missing jquery-ui</li>
<li>[2025-05-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/d103759bcfe6760823cdd46ea810c0e4d340f1a6">d103759b</a> - <a href="https://task.beeznest.com/issues/22618">BT#22618</a>) Language: Quiz: Add partial update in FR, ES, EN for translation of parameter HideComment</li>
<li>[2025-05-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/543ccb4e5d47d298ff9bd455cdb3cea15ae14bd7">543ccb4e</a> - <a href="https://task.beeznest.com/issues/22618">BT#22618</a>) Exercise: Add new option 'Hide comment' to the result page configuration extra parameter</li>
<li>[2025-05-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/378899f9718683cb7bed6c1a7302244c0dc15509">378899f9</a> - <a href="https://task.beeznest.com/issues/22393">BT#22393</a>) Plugin: H5P Import: Fix class declaration to be compatible with H5P library update</li>
<li>[2025-05-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/7f4bd80cafa8663e42a080490ac5789f70d0ee52">7f4bd80c</a> - <a href="https://task.beeznest.com/issues/22393">BT#22393</a>) Internal: Fix stricter functions declarations to improve PHPDoc from commit #ea334a3f6f because the return can be null when there is no token in session</li>
<li>[2025-05-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/e2d89c5f1205974cf9e17a55b16c7cccc3ac0787">e2d89c5f</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5393">GH#5393</a>) Tracking: Fix to adapt app/cache/.htaccess regeneration on archive cleanup to include the fix for the issue displaying the pChart-generated charts due to Apache 2.4 syntax change</li>
<li>[2025-05-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/0c4d67c8f5107f69dd41fdbdcd4b5fb3231f54ec">0c4d67c8</a>) Document: [Minor] fix typo in a comment from previous commit</li>
<li>[2025-05-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/d204845d3345e23b4e1c0e8213f26966fe596b0c">d204845d</a> - <a href="https://task.beeznest.com/issues/22464">BT#22464</a>) Session: Add use of session's position on myStudent page when session_list_order is activated</li>
<li>[2025-05-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/42d0e44e68da2d6e94f579ec9fb4af8c7d06819b">42d0e44e</a> - <a href="https://task.beeznest.com/issues/22464">BT#22464</a>) Session: Fix ambigous position error when activation session_list_order</li>
<li>[2025-05-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/ea248be77c23f83761ec338b2692c410a09e46ca">ea248be7</a> - <a href="https://task.beeznest.com/issues/22515">BT#22515</a>) Session: Fix on course inclusion in session with gradebook copy set generate certificate and is requirement</li>
<li>[2025-05-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/6f3d730bfa4e55bf8abc1690528369b11b449179">6f3d730b</a> - <a href="https://task.beeznest.com/issues/22464">BT#22464</a>) Ticket: Fix form validate redirection and return icon to keep projet_id context</li>
<li>[2025-05-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/ed06cdbd29f46e1174678fdfb23a7cba4e56d552">ed06cdbd</a> - <a href="https://task.beeznest.com/issues/22502">BT#22502</a>) Session: Use config-defined headers for base user and session fields for Excel export</li>
<li>[2025-05-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/83dc6e3d7a10f660337d71fee1723bc2fdc52cab">83dc6e3d</a>) CI: Update GitHub workflows to use ubuntu-24.04</li>
<li>[2025-05-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/2b202b09a66fd449eea62bb7b04fc1c469f6f32c">2b202b09</a> - <a href="https://task.beeznest.com/issues/22562">BT#22562</a>) Document: # fix total document tool size usage calculation to avoid documents that have many registries in c_item_property</li>
<li>[2025-05-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/d005bcdd7ada112596b95a81be35f741ac4b7f38">d005bcdd</a> - <a href="https://task.beeznest.com/issues/22562">BT#22562</a>) Document: # fix total document tool size usage calculation to avoid DELETED documents that have many registries in c_item_property</li>
<li>[2025-05-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/5b3755be0af03eb7e418aa882128a4df79b7b3d4">5b3755be</a> - <a href="https://task.beeznest.com/issues/22562">BT#22562</a>) Document: # fix folder size calculation to avoid DELETED documents that have many registries in c_item_property</li>
<li>[2025-05-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/2b7f62660839239445e6ea28cee0ffcbd654d005">2b7f6266</a> - <a href="https://task.beeznest.com/issues/22502">BT#22502</a>) Session: Fix configuration variable format to correspond to adaptation of report generation</li>
<li>[2025-05-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/13ae07525a8212220d432aba38374795c24f2714">13ae0752</a> - <a href="https://task.beeznest.com/issues/22533">BT#22533</a>) Session: Use config-defined headers and order for Excel export</li>
<li>[2025-05-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/e4f9239e2ea4e5a7b7c5db7d307e3ad337e5a806">e4f9239e</a>) Course settings: Remove e=1 from direct course access link to be consistent with the explanation text</li>
<li>[2025-05-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/db1a302f90cd49193fd5e572574fb5559e79758c">db1a302f</a> - <a href="https://task.beeznest.com/issues/22502">BT#22502</a>) Extra Fieldss: Fix default value to be used for text type extra fields only when there is no value and it is not null</li>
<li>[2025-05-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/1f38d4e8ea987805747496978348354ce078a094">1f38d4e8</a> - <a href="https://task.beeznest.com/issues/22393">BT#22393</a>) Learnpath: Fix session's lp copy</li>
<li>[2025-04-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/58f0faa4d89fe48109f697a5aac26f37edb4d6a1">58f0faa4</a> - <a href="https://task.beeznest.com/issues/22502">BT#22502</a>) Extra Fieldss: Fix default value to be used for text type extra fields only when there is no value</li>
<li>[2025-04-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/c599103d129f13b5494efacfb89c7ded508f6bcc">c599103d</a> - <a href="https://task.beeznest.com/issues/22502">BT#22502</a>) Extra Fieldss: Add default value to be used for text type extra fields</li>
<li>[2025-04-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/f5acfda2a46b316c876db421cb5ddbb5678cbd2c">f5acfda2</a> - <a href="https://task.beeznest.com/issues/22502">BT#22502</a>) Session: Fix Excel export show display text for user's headers and fix spanish translation</li>
<li>[2025-04-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/31747dbf3f132296723a07c30ae227ca96881e1c">31747dbf</a> - <a href="https://task.beeznest.com/issues/22485">BT#22485</a>) Language: Portfolio: Add partial update in FR, ES, EN for translation related to new alphabetical ordering option</li>
<li>[2025-04-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/78bf8d9ebee534e10bea2c74ada7531673a3d4c3">78bf8d9e</a> - <a href="https://task.beeznest.com/issues/22485">BT#22485</a>) Portfolio: Add option to order post by title alphabetical order</li>
<li>[2025-04-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/b63e5856084193259f312dfe22db74301289e0b8">b63e5856</a> - <a href="https://task.beeznest.com/issues/22502">BT#22502</a>) Session: Fix Excel export do not put headers in upper case and remove official code column</li>
<li>[2025-04-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/04742fa697189ecc9e423c55c41b9123768b7549">04742fa6</a> - <a href="https://task.beeznest.com/issues/22502">BT#22502</a>) Language: partial update in FR, ES, EN for translation related to specific feature</li>
<li>[2025-04-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/1ee2d8bb61b67e08946cd80b1a9b92c1a9959c7b">1ee2d8bb</a>) Standardize header titles for tools help</li>
<li>[2025-04-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/284a7502b54ae2d431f3d0170c4e942b49a16c3b">284a7502</a>) Internal: Fix E_NOTICE by undefined variable</li>
<li>[2025-04-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/e3a37889fe4e8e47518941452d76340cf2dac661">e3a37889</a> - <a href="https://task.beeznest.com/issues/22577">BT#22577</a>) Plugin: BBB: fix video download link to correspond to new path in BBB</li>
<li>[2025-04-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/8bd86913a89fec084053e2c2916df19f15d03d95">8bd86913</a>) Internal: Refactor file upload error handling for early exits</li>
<li>[2025-04-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/3608709731f714b92d2b306030fb64fe7928c05e">36087097</a> - <a href="https://task.beeznest.com/issues/22344">BT#22344</a>) Attendance: Fix escaping of language var in JS context preventing comments to be added in attendance in courses in French</li>
<li>[2025-04-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/dc4d7c8611ea3526a85d885c14beefae19a5f971">dc4d7c86</a> - <a href="https://task.beeznest.com/issues/22563">BT#22563</a>) Learnpath: Remove uploaded SCORM/AICC file immediately after treatment</li>
<li>[2025-04-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/ac908246c59121ddc976cd00e960aca5a6eba971">ac908246</a> - <a href="https://task.beeznest.com/issues/22551">BT#22551</a>) Exercise: #fix notice appearing in exercise list and exercise rendering</li>
<li>[2025-04-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/80f631dbffa13906a482385fa7f0884a1ca84250">80f631db</a> - <a href="https://task.beeznest.com/issues/22023">BT#22023</a>) Work #fix compilatio error on pending work when compilatio is not activated</li>
<li>[2025-04-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/7903cef2eb41817c11a52ba6ac34a1d454bc5ef7">7903cef2</a>) Internal: Refactor CourseSelectForm to simplify conditional logic and improve readability</li>
<li>[2025-04-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/75ab03c938adc48a3cd8234d98fc340e1998aa81">75ab03c9</a>) Refactor CourseSelectForm to simplify conditional logic and improve readability</li>
<li>[2025-04-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/07ce29dcfbf5a34a317eb2b26a642c69ad4598c7">07ce29dc</a>) Plugin: Azure: User the id property instead of objectId from resource</li>
<li>[2025-04-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/5455c42d23f5af59c4adb1ce2158f836d1ae988b">5455c42d</a>) Plugin: Azure: Refactor provider to enhance user data retrieval with new Microsoft Graph API</li>
<li>[2025-04-02] (<a href="https://github.com/chamilo/chamilo-lms/commit/d193237235c8eb7f73aac443573579b21cf4852f">d1932372</a> - <a href="https://task.beeznest.com/issues/22533">BT#22533</a>) Session: Add Excel export of certified users in course session with extra fields</li>
<li>[2025-04-02] (<a href="https://github.com/chamilo/chamilo-lms/commit/6079094bb2d7c3ac35619c36fa625a251b7b52bb">6079094b</a> - <a href="https://task.beeznest.com/issues/22531">BT#22531</a>) Internals: return the users last registered first when password lost in case of many occurences</li>
<li>[2025-03-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/680d765bcd8ffcbe2f98ea79fc9ca4325b268833">680d765b</a> - <a href="https://task.beeznest.com/issues/22402">BT#22402</a>) Tracking: Fix session admin permissions in statistics module</li>
<li>[2025-03-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/4b377c5d37e94c9174f08978bf1b9d862aedea8f">4b377c5d</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Improve document reload in result view</li>
<li>[2025-03-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/b789f216ad8b95d2f6ca1011c93e90d7ee3c1b3c">b789f216</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Prevent cached document in results page by adding timestamp to iframe</li>
<li>[2025-03-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/524a1655c52dfe2b2de500d41854a7c59dfb5170">524a1655</a>) Documentation: Update link to public internal documentation in README.md Author: @Kaneda-1</li>
<li>[2025-03-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/d31bce53c42a6c9650300f3b108678bcfb9b05a3">d31bce53</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Auto-refresh OnlyOffice iframe</li>
<li>[2025-03-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/c486375d5233b52301c8800f750f8ac7a499dd6d">c486375d</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Auto-refresh OnlyOffice iframe to show results</li>
<li>[2025-03-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/5bc1912d70107207fc1508d9a605aae06d84fdcf">5bc1912d</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Auto-refresh OnlyOffice iframe to show latest edits</li>
<li>[2025-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/be554b60a2178e62eb6f38f5a60ff63abd017dc9">be554b60</a>) Language: Update language terms</li>
<li>[2025-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/d2c57d6f42df0acbf48fcc36edbf7e46a38cf049">d2c57d6f</a> - <a href="https://task.beeznest.com/issues/22404">BT#22404</a>) Exercise: Add validation to enforce correct answer and positive score</li>
<li>[2025-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/232aa648d21f98fe6c1ffe3ae14d1c358b488806">232aa648</a>) Language: Update language terms</li>
<li>[2025-03-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/58d7dc8267b14fb446f3a966ab83ad9f428c1e7f">58d7dc82</a> - <a href="https://task.beeznest.com/issues/22503">BT#22503</a>) Admin: remove restriction due to experimental feature on access to entry to main/admin/user_move_stats.php in the administration menu</li>
<li>[2025-03-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/98cf6fe60a6cd61792b8823579a9a119bcabf045">98cf6fe6</a>) Exercise: Fix student id to display onlyoffice answer</li>
<li>[2025-03-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/1a5bc078217e2529c29e974b37fb5ff34b47c9f8">1a5bc078</a>) Exercise: Fix student edit permissions & finalization view in OnlyOffice</li>
<li>[2025-03-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/00b080cc239760df72f3784b944339d08adc1429">00b080cc</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Internal: Improvements and structure adjustments for moodle import</li>
<li>[2025-03-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/e34e1a5cd2ac71a175193c90df83e7be65c8f380">e34e1a5c</a> - <a href="https://task.beeznest.com/issues/22335">BT#22335</a>) Plugin: Azure: Fix session redirect after login from custom page</li>
<li>[2025-03-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/2f7f85c102f101a1c8ee88dbf64c54bee73adff7">2f7f85c1</a> - <a href="https://task.beeznest.com/issues/22455">BT#22455</a>) Exercise: Add automatic feedback comments to email notification</li>
<li>[2025-03-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/07d3aedbdaece9706709c6d11aee3730573787e6">07d3aedb</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Add readonly to editor onlyoffice in results</li>
<li>[2025-03-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/5fb30f2c9d0848d7166ac6a2013bcf6f89eeefa0">5fb30f2c</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Improve display onlyoffice doc editor</li>
<li>[2025-03-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/bb4ed6d55cbe21bffce1e3dcd1236ee9d740637c">bb4ed6d5</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Fix evaluation of Answer in Office doc question</li>
<li>[2025-03-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/b088041f134e658fd3fefc4a7111e4bc19b46c19">b088041f</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Improve file handling with onlyoffice and exercise tracking</li>
<li>[2025-03-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/e10f338782d5622db57323d840083809eedfffe1">e10f3387</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Add missing parameters for docInfo onlyoffice</li>
<li>[2025-03-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/45a6736ec1e6e9d4d4724990ff2e4967dd4b86bc">45a6736e</a> - <a href="https://task.beeznest.com/issues/22451">BT#22451</a>) Learnpath: Add direct lessons list access button & hide header in reduced mode</li>
<li>[2025-03-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/a60bfcb3148c369d74806d499609d06876aafc3e">a60bfcb3</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Improve OnlyOffice integration with dynamic return URLs</li>
<li>[2025-03-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/4d82256e54916b067caee42d8fc9b7382592fcbe">4d82256e</a> - <a href="https://task.beeznest.com/issues/22370">BT#22370</a>) Exercise: Improved OnlyOffice integration and URL handling</li>
<li>[2025-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/43a28402f95c93c44c0774eaa8fa60c7a9ecfa95">43a28402</a> - <a href="https://task.beeznest.com/issues/21500">BT#21500</a>) User: Language: #fix syntax from previous commit about plateformLanguage by default when updating a user instead of english</li>
<li>[2025-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/2faa79eb9ab18ed40314dfc39b28fbc6669edd6a">2faa79eb</a> - <a href="https://task.beeznest.com/issues/21500">BT#21500</a>) User: Language: #change set platformLanguage by default when updating a user instead of english</li>
<li>[2025-02-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/9853e6ede71bf7d6e17877d1cb0b94b832c7a01c">9853e6ed</a> - <a href="https://task.beeznest.com/issues/22048">BT#22048</a>) Internal: #fix commit de5623b2740be to show last 10 registered users in user group and session pages to show only user from the current URL</li>
<li>[2025-02-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/44adc09a3c2744d55620430874651d4072981039">44adc09a</a> - <a href="https://task.beeznest.com/issues/22396">BT#22396</a>) Tracking: improve display on comment to indicate progress based on visibles LPs only</li>
<li>[2025-02-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/cbc77b6996b9871a14bfaadc12d9f6c17c83c178">cbc77b69</a> - <a href="https://task.beeznest.com/issues/22396">BT#22396</a>) Learnpath: Fix LP visibility to review registry in base course if nothing set in session</li>
<li>[2025-02-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/e23191212cca37076442f90d821f09961ec55bbe">e2319121</a> - <a href="https://task.beeznest.com/issues/22396">BT#22396</a>) Tracking: improve display on comment to indicate progress based on visibles LPs only</li>
<li>[2025-02-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/e95dceb9218a698ae89374ee8625a52da353054e">e95dceb9</a> - <a href="https://task.beeznest.com/issues/22426">BT#22426</a>) Document: LP: fix access to document in LP for sessionAdmin when session_admins_access_all_content is true</li>
<li>[2025-02-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/45e7d2922aafb8a0562ef25409157b745d9c2e0b">45e7d292</a> - <a href="https://task.beeznest.com/issues/22423">BT#22423</a>) Survey: Fix survey publication form blank block and htmlspecialchars() TypeError</li>
<li>[2025-02-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/16beb27b2a6379d569961f4da78ba8ce9ee5e5a2">16beb27b</a> - <a href="https://task.beeznest.com/issues/22396">BT#22396</a>) Language: Update language terms for progress calculation and others</li>
<li>[2025-02-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/9d2a2171d125cfe3e7775cd87965934d1e4dfb2e">9d2a2171</a> - <a href="https://task.beeznest.com/issues/22407">BT#22407</a>) Skill: Minor: adapt skill block to be visible to all on the index page as it is on the user_portal page</li>
<li>[2025-02-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/070094f2298793603705950f3c399be4328859c9">070094f2</a> - <a href="https://task.beeznest.com/issues/22305">BT#22305</a>) User: Fix filters & editable columns in extra fields for advanced edit</li>
<li>[2025-02-16] (<a href="https://github.com/chamilo/chamilo-lms/commit/4f9d10530367a6c590f2a53d26eb9b3f835d41f8">4f9d1053</a> - <a href="https://task.beeznest.com/issues/22305">BT#22305</a>) Admin: Filter extra fields from GET in user_advanced_edit.php and allow filtering on fields as admin (do not take into account modifyability of field)</li>
<li>[2025-02-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/cb34d089a783079c0cf536b6190bab404c1bac6e">cb34d089</a> - <a href="https://task.beeznest.com/issues/22390">BT#22390</a>) Gradebook: Fix total average weight calculation</li>
<li>[2025-02-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/68ad7d96a1794c65f9ae9ad74552340996077a08">68ad7d96</a> - <a href="https://task.beeznest.com/issues/22304">BT#22304</a>) Tracking: Fix column position for email in report</li>
<li>[2025-02-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/6e9152b71bc0ba03f23b0b39fc46dc9355ff7527">6e9152b7</a> - <a href="https://task.beeznest.com/issues/22318">BT#22318</a>) Plagiarism: Compilatio: fix analyses API query to work with all services and contracts for compilatio</li>
<li>[2025-02-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/aacc534fe6f0b078243865a944e6a4eecfe659c8">aacc534f</a> - <a href="https://task.beeznest.com/issues/22305">BT#22305</a>) Language: Update language terms</li>
<li>[2025-02-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/6f9c01a120b9c5b88a360ea0927aabe0dd03da74">6f9c01a1</a> - <a href="https://task.beeznest.com/issues/22305">BT#22305</a>) User: add advanced user edition with bulk and ajax updates</li>
<li>[2025-02-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/c19b19d626801e2a525302ee37c65858579e5a66">c19b19d6</a>) Plugin: BuyCourses: Fix bug in learning path end page/final item when plugin is enabled. End page could not load because of buggy query in services feature of plugin.</li>
<li>[2025-02-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/6e975be050d7dd64b62e0b799ffbcdabd9e10200">6e975be0</a> - <a href="https://task.beeznest.com/issues/22390">BT#22390</a>) Gradebook: Fix pre-loading of stats with the allow_gradebook_stats setting</li>
<li>[2025-02-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/72aa66a1e9216dfb6f929896563aeba3f95f061c">72aa66a1</a>) Documentation: Add PHPDoc block to getAllValuesByItem() to explain it only returns fields with filter=1</li>
<li>[2025-02-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/b7ceb1456e349c41f273ff45bcde673a1938b48a">b7ceb145</a>) Tracking: fix average total time spent on platform calculation and use See dfca26416355703049309d2b69b38b15669a0e42</li>
<li>[2025-02-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/981d7ceac74bf19e55dbaec57d565c5374ce69f7">981d7cea</a> - <a href="https://task.beeznest.com/issues/22341">BT#22341</a>) Documentation: Add index on c_quiz_question.type in optimization guide</li>
<li>[2025-01-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/ea499b8920138a8b3c0db5b16e3aea04c368980c">ea499b89</a> - <a href="https://task.beeznest.com/issues/22303">BT#22303</a>) Statistics: Add user extra fields to export users in course session</li>
<li>[2025-01-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/1cf3d21d57a1a0c34e33bb5f55db175f84227619">1cf3d21d</a>) Plugin: Azure: Fix create/drop of azure_ad_sync_state table on install/uninstall</li>
<li>[2025-01-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/6b2da59aef019b923a0c60053d21337ece180da2">6b2da59a</a> - <a href="https://task.beeznest.com/issues/22306">BT#22306</a>) Exercise: set tolerance to pointer for draggable question</li>
<li>[2025-01-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/12e827aefe49c286bc85d0ab8302c4d10aa497f1">12e827ae</a>) Documentation: Add changelog entry for 1.11.30 with note on the .htaccess rule change.</li>
<li>[2025-01-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/46f11d3e28bedd22bc7972f61397fe135ac83137">46f11d3e</a> - <a href="https://task.beeznest.com/issues/22308">BT#22308</a>) Exercise: Refactor answer handling in exercise_show_functions.lib.php for oral expression questions</li>
<li>[2025-01-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/7638f49ec1a79defc6ed8170049f3ee60471012c">7638f49e</a> - <a href="https://task.beeznest.com/issues/22308">BT#22308</a>) Exercise: Simplify ternary operator</li>
<li>[2025-01-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/aa72a3beb83a7867d4183de1b6e4b564b7ed5b4d">aa72a3be</a>) Plugin: AI Helper: Rename Url class to DeepSeekUrl to resolve conflict</li>
<li>[2025-01-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/57b0fe66a2ac5abf1e9baf2597ec9125d4294abe">57b0fe66</a>) Plugin: AI Helper: Fix params of generateDeepSeekQuestions()</li>
<li>[2025-01-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/fe4564802c7a35c1ec5c6ef8381610c3d12e0a6b">fe456480</a>) Plugin: AI Helper: Fix the DeepSeek prompt (use same as for OpenAI by default)</li>
<li>[2025-01-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/a9d0097466e5112d7fbf592a15247ce5bb44a9ae">a9d00974</a>) Link: Avoid double htmlspecialchars for links using Display::url See 15023ce6301a8b623d789e8e788b2db9bf470547</li>
<li>[2025-01-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/32d3d5de8e10f1956e95e81af012267dc0d5e8c4">32d3d5de</a>) Internal: Use $.getJson instead of fetch</li>
<li>[2025-01-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/7f75d7066dfcfc345f84727f297068ddb2f83b3d">7f75d706</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5977">GH#5977</a>) Calendar: Allow to delete registry in agenda_event_invitee when deleting user</li>
<li>[2025-01-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/7ea28f8df60db59d46778bace2f7cc7a1f592776">7ea28f8d</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5977">GH#5977</a>) Calendar: Add property types</li>
<li>[2025-01-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/e0507216f0871dd22cad4ce461f657f969056f04">e0507216</a> - <a href="https://task.beeznest.com/issues/22323">BT#22323</a>) Internal: Avoid unnecessary loop condition in duplicate links deletion script</li>
<li>[2025-01-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/b7513f284f95ea1dac752bd1acb191f60c45c1e3">b7513f28</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6030">GH#6030</a>) Settings: Fix PlatformLanguage setting to support multi-URL configurations</li>
<li>[2025-01-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/2eb0030eedffdd5c9c206635bc981e261fb7703b">2eb0030e</a> - <a href="https://task.beeznest.com/issues/22323">BT#22323</a>) Internal: Fix duplicate links handling with improved LP checks and deletion logic</li>
<li>[2025-01-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/8c9caf6ad3ac2a97cf1dd0dd93293989b4621edc">8c9caf6a</a>) Language: Don't load i18n files for datepicker and timepicker when language ISO code is EN</li>
<li>[2025-01-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/0a91306a2c8a3db973d32da3d895bfd6fbcda983">0a91306a</a> - <a href="https://task.beeznest.com/issues/22335">BT#22335</a>) User: Fix session redirect after login from custom page</li>
<li>[2025-01-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/cfa1f18c4f24652fddb6a5869f182bbc982c318e">cfa1f18c</a> - <a href="https://task.beeznest.com/issues/22335">BT#22335</a>) User: Improve layout of custom login page</li>
<li>[2025-01-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/27b344eeee5f45eab373ceaa419ccf7a35e66c8e">27b344ee</a>) Internal: Fix params and return types</li>
<li>[2025-01-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/4d3c2a9533b54eb309a28e061f51d556b1cf3ee3">4d3c2a95</a>) Auth: Fix locale field when logging with facebook</li>
<li>[2025-01-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/b7accd6d69c85027844bfec2c366fbed2c10ec69">b7accd6d</a> - <a href="https://task.beeznest.com/issues/22335">BT#22335</a>) User: Add custom login template support for session expiration page</li>
<li>[2025-01-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/8f51bd8f118801a25cb32051425a9fbbe7712243">8f51bd8f</a> - <a href="https://task.beeznest.com/issues/22330">BT#22330</a>) Session: Add dynamic sorting for users table by name and date</li>
<li>[2025-01-02] (<a href="https://github.com/chamilo/chamilo-lms/commit/135e61ebf5505028bf46ab37df97d0f821bdf274">135e61eb</a>) Skill: Display: Use badge.design instead of openbadges.me/designer</li>
<li>[2024-12-31] (<a href="https://github.com/chamilo/chamilo-lms/commit/060e433498387b38fbed8d48a9ef98e48655db9a">060e4334</a>) Internal: Add return and params types + format code</li>
<li>[2024-12-31] (<a href="https://github.com/chamilo/chamilo-lms/commit/c343e89a8823a707af73d324e26bcaf33cfa0831">c343e89a</a>) Vendor: Use facebook/graph-sdk instead of facebook/php-sdk-v4</li>
<li>[2024-12-31] (<a href="https://github.com/chamilo/chamilo-lms/commit/f6a79759095ee55c1b123c3686599c936a4bc040">f6a79759</a>) Internal: Fix script to upload video to Vimeo</li>
<li>[2024-12-31] (<a href="https://github.com/chamilo/chamilo-lms/commit/18b2131d7518baa635f08af6ffb2a2ee2245495d">18b2131d</a> - <a href="https://task.beeznest.com/issues/22318">BT#22318</a>) Plagiarism: Compilatio: fix analyses API query to work with all services and contracts for compilatio</li>
<li>[2024-12-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/8f92e154b7f3c7c731880bb895d85606aee11bc6">8f92e154</a> - <a href="https://task.beeznest.com/issues/22235">BT#22235</a>) Gossary: complement to Fix load glossary from base course if not found in session</li>
<li>[2024-12-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/3d19c0014744ea219b8a7790b80e190a5737edc7">3d19c001</a> - <a href="https://task.beeznest.com/issues/22319">BT#22319</a>) Internal: Fix pagination issue in SortableTable session handling</li>
<li>[2024-12-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/38d8bce275164684e824e4f9b7b2d50c6f07c728">38d8bce2</a> - <a href="https://task.beeznest.com/issues/21930">BT#21930</a>) Plugin: Azure: Add option to filter groups by display name</li>
<li>[2024-12-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/10b6d6125dfdb23d765d694eaede0ae4e22ef6c0">10b6d612</a>) Internal: Fix undefined index in request</li>
<li>[2024-12-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/49816b6855d03cbea4898533169675ff3dd91e44">49816b68</a> - <a href="https://task.beeznest.com/issues/21354">BT#21354</a>) Display: Load missing i18n files for date and time pickers</li>
<li>[2024-12-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/8c16c19299b01ea1321b4d721efdbf67c174b5c0">8c16c192</a> - <a href="https://task.beeznest.com/issues/22321">BT#22321</a>) Internal: Revert PHP 8 compatibility improvement in api_htmlentities() as it causes issues filtering HTML tags in HTML titles</li>
<li>[2024-12-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/c6880d3554918bba237e2c2e6aad4708acf61218">c6880d35</a>) Vendor: Remove brumann/polyfill-unserialize > In case you are using PHP 7.0+ the original unserialize() will be used instead.</li>
<li>[2024-12-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/a025de461aa7447446a9eab77f30a2a48ef8c642">a025de46</a> - <a href="https://task.beeznest.com/issues/22232">BT#22232</a>) Portfolio: Duplicate post in session when commenting if portfolio_show_base_course_post_in_sessions is enabled</li>
<li>[2024-12-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/542765cb6335eefe34f9dafee81beb8b35bd9396">542765cb</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Course: Fix export mbz validation, root-only resources, skip empty folders</li>
<li>[2024-12-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/34975cd966974a821471c92f39c1ecf286ea1c30">34975cd9</a> - <a href="https://task.beeznest.com/issues/22232">BT#22232</a>) Internal: Portfolio: Add types to entity properties</li>
<li>[2024-12-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/c60a4e23f5b64bf12385fb123a066259a96a9fa1">c60a4e23</a>) Plugin: OnlyOffice: Fix E_NOTICEs when api_get_setting is not returning an array value</li>
<li>[2024-12-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/2d12af3491a2742fb3b3ef2f4f773fe57f69a587">2d12af34</a>) Internal: Remove return type mixed The mixed return type is available from php8.0</li>
<li>[2024-12-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/d617e04be734ec096e0681bd92a5bdc57e8836d1">d617e04b</a>) Internal: Fix ErrorCorrectionLevel::MEDIUM constant usage</li>
<li>[2024-12-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/e117d3f69f9f831c12240f3a932685108518a239">e117d3f6</a>) Add missing use of ReflectionClass</li>
<li>[2024-12-16] (<a href="https://github.com/chamilo/chamilo-lms/commit/600d8a1a9269b21c4d5ba78f04c2a34e60fd2688">600d8a1a</a>) Internal: Fix various PHP8 compatibility issues to reduce error messages</li>
<li>[2024-12-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/3439ff1d714f18594a6d9db3d8d344e8b02cb4b7">3439ff1d</a>) Documentation: Add precision bout the syntax for new RewriteRule with Apache > 2.4.38-3</li>
<li>[2024-12-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/b21a8a27fbb14dfa5fda603523727e7d02d34883">b21a8a27</a> - <a href="https://task.beeznest.com/issues/22272">BT#22272</a>) Exercise: fix delete attempt from pending exercice page</li>
<li>[2024-12-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/b590a170d69195e1baca37f0e02abcf1771d8537">b590a170</a>) Internal: Fix warning and error with php8</li>
<li>[2024-12-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/ca83c918ea43df18933a68dd519f4f7c312fb3bc">ca83c918</a> - <a href="https://task.beeznest.com/issues/22235">BT#22235</a>) Gossary: Fix load glossary from base course if not found in session</li>
<li>[2024-12-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/f846c254038550c3b86d5ab485107df3a7088a15">f846c254</a> - <a href="https://task.beeznest.com/issues/22234">BT#22234</a>) Session: Fix session course position handling in CSV export/import</li>
<li>[2024-12-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/e11bb258e5bf8d3bbaa3bd0fb05c7a530a340395">e11bb258</a>) Plugin: OnlyOffice: Add checked vendor/ to git</li>
<li>[2024-12-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/406e0dd5f47366c609a9bc0878edd3763aa4f7d2">406e0dd5</a> - <a href="https://task.beeznest.com/issues/22231">BT#22231</a>) Gradebook: Fix theme image paths in certificates</li>
<li>[2024-11-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/5e1031678798f1229acb4315e07df261cbbbcab8">5e103167</a> - <a href="https://task.beeznest.com/issues/21930">BT#21930</a>) Plugin: Azure: Bump version to v2.5</li>
<li>[2024-11-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/2c779cbc56c20d0b88a1a748a75f773a00411fc4">2c779cbc</a> - <a href="https://task.beeznest.com/issues/21969">BT#21969</a>) Exercise: Fix hide_expected_answer exercise option for fill in the blank question type</li>
<li>[2024-11-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/54d55b59b46e727aeafbe2243bcf85db09d09289">54d55b59</a> - <a href="https://task.beeznest.com/issues/22195">BT#22195</a>) Learnpath: Avoid showing PDF files in embed view</li>
<li>[2024-11-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/a0322486a563a2ae3029314916d714fbad4c374e">a0322486</a> - <a href="https://task.beeznest.com/issues/22195">BT#22195</a>) Group: Fix advanced search when adding user to class</li>
<li>[2024-11-13] (<a href="https://github.com/chamilo/chamilo-lms/commit/d12798db17f6e0297c9f6be307069686996af640">d12798db</a> - <a href="https://task.beeznest.com/issues/22195">BT#22195</a>) Remove E_NOTICE about undefined variable</li>
<li>[2024-11-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/da839c3f8cb0bd465a2e87c5a34088d3e1e78732">da839c3f</a> - <a href="https://task.beeznest.com/issues/22175">BT#22175</a>) WYSIWYG: Allow all domain in iframes insertion if setting to allow iframe is true in HTMLPurifier filter</li>
<li>[2024-11-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/a591c25b577131dbeb76794454c998076872672d">a591c25b</a> - <a href="https://task.beeznest.com/issues/21436">BT#21436</a>) Userportal: Fix coach url when it is null</li>
<li>[2024-10-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/e63569abb7f26d52412a9709b18b485b4e21dff6">e63569ab</a> - <a href="https://task.beeznest.com/issues/20849">BT#20849</a>) LDAP: Fix encrypted password generation to use the api function which is updated and not the local function that ended being different</li>
<li>[2024-10-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/23d210040f61ee3a096c1d8ccda1fcde38165813">23d21004</a> - <a href="https://task.beeznest.com/issues/22159">BT#22159</a>) Admin: Fix download of existing CSS by platform admin</li>
<li>[2024-10-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/3e2582f64f3017b0af0f515ce5200e209a9e4478">3e2582f6</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5887">GH#5887</a>) Internal: Fix filter for "on" attributes was too wide and replaced normal text containing " on"</li>
<li>[2024-10-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/00cdb857e034ed688ef8a141dc5d8317d1eb4ccd">00cdb857</a>) Documentation: Update upgrade guide from other minor versions regarding cache and CSS directories</li>
<li>[2024-10-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/368dfaf932bd9084f60088b24495ed3aa8c6becc">368dfaf9</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5887">GH#5887</a>) Internal: Fix df47eac9b93700bdf3a73e2596e956e14ab1e4f2 where the filter for "on" attributes was too wide and replaced normal text containing " on"</li>
</ul>
<h3>Stylesheets and theming</h3>
<ul aria-live="off">
<li>No notable style change</li>
</ul>
<h3>Web services</h3>
<ul aria-live="off">
<li>[2025-02-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/bc2e5fde6e46a224c92a9d244dd64c15aff138d6">bc2e5fde</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Webservice: Add get_session_info_from_extra_field WS</li>
<li>[2025-02-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/be0cceabfa43509b90b86c83607aa36799638c49">be0cceab</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Webservice: Add get_extra_fields option to get_sessions WS</li>
<li>[2025-02-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/f4f6e1ee9383538308fcd82c5eb51562f2e6abab">f4f6e1ee</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Webservice: Only return relevant extra field properties in getSessionInfoFromExtraField()</li>
<li>[2025-02-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/c8ff566ff0e6c385e5fe70a789b26ea6e5e94a14">c8ff566f</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6078">GH#6078</a>) Webservice: Add get_user_info_from_username WS (rename from get_user_from_username)</li>
<li>[2025-02-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/0786df76a53455b0712a4605df11b1de27e6379e">0786df76</a> - <a href="https://task.beeznest.com/issues/22409">BT#22409</a>) Webservice: Message: #add a new only_local option to save_user_message</li>
<li>[2025-03-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/1ec72a419f48a8e5c27d2095c83ea0ffee2e043c">1ec72a41</a> - <a href="https://task.beeznest.com/issues/22409">BT#22409</a>) Webservice: #add a new get_user_progress_and_time_in_session ws</li>
<li>[2025-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/0dd652811e00108115811e5f4e540470db3ffe62">0dd65281</a> - <a href="https://task.beeznest.com/issues/22409">BT#22409</a>) Webservice: #add official_code parameter to the save_user ws</li>
</ul>
<h3>Removals</h3>
<ul aria-live="off">
<li>No notable removal</li>
</ul>
<h3>Known issues</h3>
<ul aria-live="off">
<li>No notable known issue</li>
</ul>
</div>
<div class="version" aria-label="1.11.28">
<a id="1.11.28"></a>
<h1>Chamilo 1.11.28 - Alcatraz, 21/10/2024</h1>

View File

@@ -395,7 +395,7 @@ some manual work, but the upgrade procedure will not touch sublanguages directly
<br />
<span class="text-muted">* Styles and images are located in the main/css or main/img
<span class="text-muted">* Styles and images are located in the main/img/ or app/Resources/public/css/themes/
directories. You can still recover them from your backup if you have made it.
Any modified style or image that uses the default style/image name will be
overwritten by the next step. To avoid loosing your customisations, always
@@ -674,7 +674,23 @@ Chamilo LMS 1.9.8 (and following versions) supports the X-Sendfile headers, but
<pre>
$_configuration['enable_x_sendfile_headers'] = true;
</pre>
If you have issues with files taking a long time to download, make sure you reconfigure your webserver and add this line. You should see a notable difference in download time.
If you have issues with files taking a long time to download, make sure you reconfigure your webserver and add this line. You should see a notable difference in download time.<br>
Your webserver must be configured to support X-Sendfile, both by enabling the module and setting options in your web server's configuration.<br>
If you are running Apache 2.4 on Ubuntu, for example, this would mean installing and enabling the module:
<pre>
sudo apt install libapache2-mod-xsendfile
sudo a2enmod xsendfile
</pre>
Then in your active vhost for Chamilo, you would have to add the following lines, where the path is the same as your DocumentRoot (we use /var/www/chamilo in this example):
<pre>
XSendFile On
XSendFilePath /var/www/chamilo
</pre>
Then restart Apache:
<pre>
sudo systemctl restart apache2
</pre>
<hr style="width: 100%; height: 2px;" />
<h2><a id="15._Videoconference"></a>13. Videoconference</h2>
<p>
@@ -735,11 +751,11 @@ If you have issues with files taking a long time to download, make sure you reco
RewriteRule ^certificates/$ certificates/index.php?id=%1 [L]
RewriteRule ^courses/([^/]+)/?$ main/course_home/course_home.php?cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/index.php$ main/course_home/course_home.php?cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*)$ main/document/download_scorm.php?doc_url=/$2&cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/document/certificates/(.*)$ app/courses/$1/document/certificates/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/document/(.*)$ main/document/download.php?doc_url=/$2&cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/course_home_icons/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/upload/course_home_icons/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/course_home_icons/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/upload/course_home_icons/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/([^/]+)/(.*)$ main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 [QSA,L]
RewriteRule ^courses/([^/]+)/work/(.*)$ main/work/download.php?file=work/$2&cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/course-pic85x85.png$ main/inc/ajax/course.ajax.php?a=get_course_image&code=$1&image=course_image_source [QSA,L]
@@ -806,7 +822,7 @@ If you have issues with files taking a long time to download, make sure you reco
rewrite ^certificates/$ certificates/index.php last;
rewrite ^/courses/([^/]+)/$ /main/course_home/course_home.php?cDir=$1 last;
rewrite ^/courses/([^/]+)/index.php$ /main/course_home/course_home.php?cDir=$1 last;
rewrite ^/courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/scorm/$2 last;
rewrite ^/courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/scorm/$2 last;
rewrite ^/courses/([^/]+)/scorm/(.*)$ /main/document/download_scorm.php?doc_url=/$2&cDir=$1 last;
# Alternatively, you can choose to give direct access to all SCORM files, which is much faster but less secure
# rewrite "^/courses/([^/]+)/scorm/(.*)$" /app/courses/$1/scorm/$2 break;
@@ -815,7 +831,7 @@ If you have issues with files taking a long time to download, make sure you reco
rewrite ^/courses/([^/]+)/document/(.*)$ /main/document/download.php?doc_url=/$2&cDir=$1 last;
rewrite ^/courses/([^/]+)/upload/([^/]+)/(.*)$ /main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 last;
rewrite ^/courses/([^/]+)/work/(.*)$ /main/work/download.php?file=work/$2&cDir=$1 last;
rewrite ^/courses/([^/]+)/upload/course_home_icons/(.*([\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/upload/course_home_icons/$2 last;
rewrite ^/courses/([^/]+)/upload/course_home_icons/(.*\.(png|jpg|jpeg|gif))$ app/courses/$1/upload/course_home_icons/$2 last;
rewrite ^/courses/([^/]+)/(.*)$ /app/courses/$1/$2 last;
rewrite ^/session/([^/]+)/about/?$ /main/session/about.php?session_id=$1 last;
rewrite ^/course/([^/]+)/about/?$ /main/course_info/about.php?course_id=$1 last;
@@ -915,7 +931,7 @@ If you have issues with files taking a long time to download, make sure you reco
&lt;action type="Rewrite" url="main/course_home/course_home.php?cDir={R:1}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule 4v" stopProcessing="true"&gt;
&lt;match url="^courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$" /&gt;
&lt;match url="^courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$" /&gt;
&lt;action type="Rewrite" url="app/courses/{R:1}/scorm/{R:2}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule 5v" stopProcessing="true"&gt;
@@ -931,7 +947,7 @@ If you have issues with files taking a long time to download, make sure you reco
&lt;action type="Rewrite" url="main/document/download.php?doc_url=/{R:2}&amp;cDir={R:1}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule v8" stopProcessing="true"&gt;
&lt;match url="^courses/([^/]+)/upload/course_home_icons/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$" /&gt;
&lt;match url="^courses/([^/]+)/upload/course_home_icons/(.*\.(js|css|png|jpg|jpeg|gif))$" /&gt;
&lt;action type="Rewrite" url="app/courses/{R:1}/upload/course_home_icons/{R:2}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule v9" stopProcessing="true"&gt;

View File

@@ -525,7 +525,7 @@ Este script se proporciona sin garantía. Por favor * siempre * realice una copi
</li>
</ul>
<span class="text-muted">
* Los estilos e imágenes están ubicados en el directorio main/css o main/img. Usted puede recuperarlos desde la
* Los estilos e imágenes están ubicados en el directorio main/img o app/Resources/public/css/themes. Usted puede recuperarlos desde la
copia de seguridad en el caso de que usted hya tenido la precaución de realizarla. Cualquier estilo o imagen
modificada que use el nombre predeterminado style/image será sobrescrita en el siguiente paso. Para evitar
perder cualquier personalización, siempre asegúrese de copiar styles/images bajo un nuevo nombre y use y
@@ -796,6 +796,23 @@ específica de configuración para ser agregado a configuration.php:
<pre>$_configuration['enable_x_sendfile_headers'] = true;</pre>
Si tiene problemas con los archivos que tardan mucho tiempo en descargarse, asegúrese de reconfigurar su
servidor web y agregar esta línea. Debería ver una diferencia notable en el tiempo de descarga.
El servidor web tiene que ser configurado para soportar X-Sendfile, por activar el módulo y por configurar opciones en su configuración.<br>
Si tiene Apache 2.4 en Ubuntu, por ejemplo, tendría que ser algo así:
<pre>
sudo apt install libapache2-mod-xsendfile
sudo a2enmod xsendfile
</pre>
De ahí, en su vhost para Chamilo, tendría que añadir las líneas siguientes, donde el path es el mismo que su DocumentRoot (usamos /var/www/chamilo en este ejemplo):
<pre>
XSendFile On
XSendFilePath /var/www/chamilo
</pre>
Luego reiniciar Apache:
<pre>
sudo systemctl restart apache2
</pre>
<hr style="width: 100%; height: 2px;" />
<h2><a id="15._Videoconference"></a>13. Videoconferencia</h2>
@@ -867,11 +884,11 @@ servidor web y agregar esta línea. Debería ver una diferencia notable en el ti
RewriteRule ^certificates/$ certificates/index.php?id=%1 [L]
RewriteRule ^courses/([^/]+)/?$ main/course_home/course_home.php?cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/index.php$ main/course_home/course_home.php?cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*)$ main/document/download_scorm.php?doc_url=/$2&cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/document/certificates/(.*)$ app/courses/$1/document/certificates/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/document/(.*)$ main/document/download.php?doc_url=/$2&cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/course_home_icons/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/upload/course_home_icons/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/course_home_icons/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/upload/course_home_icons/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/([^/]+)/(.*)$ main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 [QSA,L]
RewriteRule ^courses/([^/]+)/work/(.*)$ main/work/download.php?file=work/$2&cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/course-pic85x85.png$ main/inc/ajax/course.ajax.php?a=get_course_image&code=$1&image=course_image_source [QSA,L]
@@ -926,7 +943,7 @@ que se colocarán dentro de un bloque de servidor {}, ya que otras configuracion
rewrite ^certificates/$ certificates/index.php last;
rewrite ^/courses/([^/]+)/$ /main/course_home/course_home.php?cDir=$1 last;
rewrite ^/courses/([^/]+)/index.php$ /main/course_home/course_home.php?cDir=$1 last;
rewrite ^/courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/scorm/$2 last;
rewrite ^/courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/scorm/$2 last;
rewrite ^/courses/([^/]+)/scorm/(.*)$ /main/document/download_scorm.php?doc_url=/$2&cDir=$1 last;
# Alternatively, you can choose to give direct access to all SCORM files, which is much faster but less secure
# rewrite "^/courses/([^/]+)/scorm/(.*)$" /app/courses/$1/scorm/$2 break;
@@ -935,7 +952,7 @@ que se colocarán dentro de un bloque de servidor {}, ya que otras configuracion
rewrite ^/courses/([^/]+)/document/(.*)$ /main/document/download.php?doc_url=/$2&cDir=$1 last;
rewrite ^/courses/([^/]+)/upload/([^/]+)/(.*)$ /main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 last;
rewrite ^/courses/([^/]+)/work/(.*)$ /main/work/download.php?file=work/$2&cDir=$1 last;
rewrite ^/courses/([^/]+)/upload/course_home_icons/(.*([\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/upload/course_home_icons/$2 last;
rewrite ^/courses/([^/]+)/upload/course_home_icons/(.*\.(png|jpg|jpeg|gif))$ app/courses/$1/upload/course_home_icons/$2 last;
rewrite ^/courses/([^/]+)/(.*)$ /app/courses/$1/$2 last;
rewrite ^/session/([^/]+)/about/?$ /main/session/about.php?session_id=$1 last;
rewrite ^/course/([^/]+)/about/?$ /main/course_info/about.php?course_id=$1 last;
@@ -1033,7 +1050,7 @@ que se colocarán dentro de un bloque de servidor {}, ya que otras configuracion
&lt;action type="Rewrite" url="main/course_home/course_home.php?cDir={R:1}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule 4v" stopProcessing="true"&gt;
&lt;match url="^courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$" /&gt;
&lt;match url="^courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$" /&gt;
&lt;action type="Rewrite" url="app/courses/{R:1}/scorm/{R:2}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule 5v" stopProcessing="true"&gt;
@@ -1049,7 +1066,7 @@ que se colocarán dentro de un bloque de servidor {}, ya que otras configuracion
&lt;action type="Rewrite" url="main/document/download.php?doc_url=/{R:2}&amp;cDir={R:1}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule v8" stopProcessing="true"&gt;
&lt;match url="^courses/([^/]+)/upload/course_home_icons/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$" /&gt;
&lt;match url="^courses/([^/]+)/upload/course_home_icons/(.*\.(js|css|png|jpg|jpeg|gif))$" /&gt;
&lt;action type="Rewrite" url="app/courses/{R:1}/upload/course_home_icons/{R:2}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule v9" stopProcessing="true"&gt;

View File

@@ -342,7 +342,7 @@
<li>php-pcre: L'extension pcre partagée pour php</li>
<li>php-xml</li>
<li>php-json</li>
<li>php-iconv ou php5-mbstring (au choix)</li>
<li>php-iconv ou php-mbstring (au choix)</li>
<li>php-gd L'extension de manipulation d'images pour PHP</li>
<li>php-intl L'extension pour la gestion de l'internationalisation pour PHP</li>
</ul>
@@ -411,20 +411,20 @@
quelconque version 1.11.*, les seuls pas à suivre sont:
<ul>
<li>
vérifier que vous n'avez pas créé une version modifiée d'une feuille de
Vérifiez que vous n'avez pas créé une version modifiée d'une feuille de
style en utilisant un répertoire css existant <span class="text-muted">(si c'est le cas, elle sera
écrasée par la mise à jour. Gardez-en une copie que vous renommerez au
moment de la replacer dans le répertoire main/css/)</span>
moment de la replacer dans le répertoire app/Resources/public/css/themes/)</span>
</li>
<li> téléchargez le paquet d'installation de Chamilo 1.11 depuis la
<li> Téléchargez le paquet d'installation de Chamilo 1.11 depuis la
<a href="https://chamilo.org/download">page de téléchargement de Chamilo</a></li>
<li> dézippez les nouveaux fichiers de Chamilo 1.11 par dessus les fichiers de
<li> Dézippez les nouveaux fichiers de Chamilo 1.11 par dessus les fichiers de
l'ancienne version (ou dézippez les fichiers dans un répertoire et copiez-les ensuite
là où se trouve votre version actuelle de Chamilo)</li>
<li> éditez le fichier app/config/configuration.php : localisez le numéro de version antérieur dans le paramètre <i>$_configuration['system_version']</i> (p.ex. '1.11.18') et remplacez-le
par le numéro de la nouvelle version (p.ex. '1.11.26')</li>
<li> videz votre dossier archive/ : faites une copie temporaire de index.html, supprimez touss les contenus *dans* ce dossier (ne supprimer pas le dossier, juste ses contenus). Vous pouvez également vider ce dossier depuis le lien "Vidange du répertoire archive" du bloc "Système" de l'interface d'administration.</li>
<li> c'est fini! Il n'y a pas d'autre étape requise.</li>
là où se trouve votre version actuelle de Chamilo, le zip contenant un sous-répertoire qui ne porte peut-être pas le même nom que votre installation)</li>
<li> Éditez le fichier app/config/configuration.php : localisez le numéro de version antérieur dans le paramètre <i>$_configuration['system_version']</i> (p.ex. '1.11.18') et remplacez-le
par le numéro de la nouvelle version (p.ex. '1.11.30')</li>
<li> Videz votre dossier app/cache/twig/ : supprimez tous les contenus *dans* ce dossier (ne supprimez pas le dossier, juste ses contenus). Vous pouvez également vider ce dossier depuis le lien "Vidange du cache et des fichiers temporaires" du bloc "Système" de l'interface d'administration.</li>
<li> C'est fini! Il n'y a pas d'autre étape requise.</li>
</ul>
<br />
@@ -436,15 +436,15 @@
<li> télécharger le paquet Chamilo 1.11 depuis la <a href="https://chamilo.org/download">page de téléchargement de Chamilo</a></li>
<li> décompressez les nouveaux fichiers de Chamilo 1.11 sur les fichiers de votre ancienne installation (ou décompressez les dans un nouveau dossier et copier les fichiers extraits sur les anciens fichiers).</li>
<li> vérifiez que le fichier .htaccess de la version 1.11 a bien été copié</li>
<li> vérifiez que "AllowOverride All" est présent dans votre configuration Apache, car l'interprétation du fichier .htaccess est capitale pour le fonctionnement de Chamilo</li>
<li> vérifiez que "AllowOverride All" ou "Require all granted" (selon la version d'Apache) est présent dans votre configuration Apache, car l'interprétation du fichier .htaccess est capitale pour le fonctionnement de Chamilo</li>
<li> accéder à l'adresse de votre portail URL + main/install </li>
<li> Choisissez votre langue et cliquez sur <span style="font-style: italic;">Mettre à jour depuis la version 1.8.x / 1.9.x</span></li>
<li> Videz votre dossier archive/ : faites une copie temporaire de index.html, supprimez touss les contenus *dans* ce dossier (ne supprimer pas le dossier, juste ses contenus). Vous pouvez également vider ce dossier depuis le lien "Vidange du répertoire archive" du bloc "Système" de l'interface d'administration.</li>
<li> Videz votre dossier app/cache/ : faites une copie temporaire de index.html, supprimez touss les contenus *dans* ce dossier (ne supprimer pas le dossier, juste ses contenus). Vous pouvez également vider ce dossier depuis le lien "Vidange du répertoire archive" du bloc "Système" de l'interface d'administration.</li>
</ul>
<br />
<span class="text-muted">* Les styles et images se trouvent dans les répertoires main/css ou main/img.
<span class="text-muted">* Les styles et images se trouvent dans les répertoires main/img et app/Resources/public/css/themes/.
Vous pouvez les récupérer depuis la copie de sauvegarde (que vous n'avez pas manqué de prendre).
Toute image ou style modifié qui utilise le nom de l'image/style par défaut sera
écrasé lors de l'étape suivante. Pour éviter la perte de customisations, assurez-vous toujours
@@ -720,14 +720,30 @@ Please note that, although Chamilo allows you to define its position, the "title
<hr style="width: 100%; height: 2px;" />
<h2><a id="14._Improving_files_download"></a>12. Améliorer la performance des téléchargements de fichiers</h2>
<div>
File download can be very slow when passing through a PHP script to control permissions. One solution to this
is to use the X-Sendfile header, which depends on a module on the webserver. <a href="https://stackoverflow.com/a/3731639/1406662">Check https://stackoverflow.com/a/3731639/1406662 for more details on implementing Sendfile</a>.
Chamilo LMS 1.9.8 (and following versions) supports the X-Sendfile headers, but requires a specific line of configuration to be
added to configuration.php:
Le téléchargement de fichiers peut &ecirc;tre très lent quand il passe au travers d'un script PHP qui contr&ocirc;le les permissions. One solution to this
Une solution pour cela est d'utiliser les en-t&ecirc;tes X-Sendfile, qui dépendent d'un module du serveur web. <a href="https://stackoverflow.com/a/3731639/1406662">Lisez https://stackoverflow.com/a/3731639/1406662 pour plus de d&eacute;tails concernant son impl&eacut;mentation</a>.
Chamilo LMS 1.9.8 (et les versions suivantes) supporte les en-t&ecirc;tes X-Sendfile, mais exige une ligne de configuration spéciale à ajouter à configuration.php :
<pre>
$_configuration['enable_x_sendfile_headers'] = true;
</pre>
If you have issues with files taking a long time to download, make sure you reconfigure your webserver and add this line. You should see a notable difference in download time.
Si vous avez des soucis avec le téléchargement trop lent de fichiers, essayez cette option. Vous devriez observer une différence sensible dans les temps de téléchargement.
Votre serveur web doit &ecirc;tre configuré pour supporter X-Sendfile, en activant le moédule correspondant ainsi qu'une configuration de votre vhost.<br>
Si vous utilisez Apache 2.4 sur Ubuntu, par exemple, ça voudrait dire installer et activer le module X-Sendfile :
<pre>
sudo apt install libapache2-mod-xsendfile
sudo a2enmod xsendfile
</pre>
Ensuite il faut configurer le vhost actif de Chamilo en ajoutant les lignes suivantes, où le path est le m&ecirc;me que votre DocumentRoot (nous utilisons /var/www/chamilo dans cet exemple) :
<pre>
XSendFile On
XSendFilePath /var/www/chamilo
</pre>
Puis red&eacute;marrer Apache :
<pre>
sudo systemctl restart apache2
</pre>
</div>
<hr style="width: 100%; height: 2px;" />
<h2><a id="15._Videoconference"></a>13. Vidéo-conférence</h2>
@@ -787,11 +803,11 @@ ou, si vous travaillez avec Apache 2.4, la syntaxe est légèrement différente
RewriteRule ^certificates/$ certificates/index.php?id=%1 [L]
RewriteRule ^courses/([^/]+)/?$ main/course_home/course_home.php?cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/index.php$ main/course_home/course_home.php?cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*)$ main/document/download_scorm.php?doc_url=/$2&cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/document/certificates/(.*)$ app/courses/$1/document/certificates/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/document/(.*)$ main/document/download.php?doc_url=/$2&cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/course_home_icons/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/upload/course_home_icons/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/course_home_icons/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/upload/course_home_icons/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/upload/([^/]+)/(.*)$ main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 [QSA,L]
RewriteRule ^courses/([^/]+)/work/(.*)$ main/work/download.php?file=work/$2&cDir=$1 [QSA,L]
RewriteRule ^courses/([^/]+)/course-pic85x85.png$ main/inc/ajax/course.ajax.php?a=get_course_image&code=$1&image=course_image_source [QSA,L]
@@ -842,7 +858,7 @@ Ce sont uniquement les redirections à placer dans un bloc server{}, comme les a
rewrite ^certificates/$ certificates/index.php last;
rewrite ^/courses/([^/]+)/$ /main/course_home/course_home.php?cDir=$1 last;
rewrite ^/courses/([^/]+)/index.php$ /main/course_home/course_home.php?cDir=$1 last;
rewrite ^/courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/scorm/$2 last;
rewrite ^/courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/scorm/$2 last;
rewrite ^/courses/([^/]+)/scorm/(.*)$ /main/document/download_scorm.php?doc_url=/$2&cDir=$1 last;
# Alternatively, you can choose to give direct access to all SCORM files, which is much faster but less secure
# rewrite "^/courses/([^/]+)/scorm/(.*)$" /app/courses/$1/scorm/$2 break;
@@ -851,7 +867,7 @@ Ce sont uniquement les redirections à placer dans un bloc server{}, comme les a
rewrite ^/courses/([^/]+)/document/(.*)$ /main/document/download.php?doc_url=/$2&cDir=$1 last;
rewrite ^/courses/([^/]+)/upload/([^/]+)/(.*)$ /main/document/download_uploaded_files.php?code=$1&type=$2&file=$3 last;
rewrite ^/courses/([^/]+)/work/(.*)$ /main/work/download.php?file=work/$2&cDir=$1 last;
rewrite ^/courses/([^/]+)/upload/course_home_icons/(.*([\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/upload/course_home_icons/$2 last;
rewrite ^/courses/([^/]+)/upload/course_home_icons/(.*\.(png|jpg|jpeg|gif))$ app/courses/$1/upload/course_home_icons/$2 last;
rewrite ^/courses/([^/]+)/(.*)$ /app/courses/$1/$2 last;
rewrite ^/session/([^/]+)/about/?$ /main/session/about.php?session_id=$1 last;
rewrite ^/course/([^/]+)/about/?$ /main/course_info/about.php?course_id=$1 last;
@@ -935,7 +951,7 @@ Ce sont uniquement les redirections à placer dans un bloc server{}, comme les a
&lt;action type="Rewrite" url="main/course_home/course_home.php?cDir={R:1}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule 4v" stopProcessing="true"&gt;
&lt;match url="^courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$" /&gt;
&lt;match url="^courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$" /&gt;
&lt;action type="Rewrite" url="app/courses/{R:1}/scorm/{R:2}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule 5v" stopProcessing="true"&gt;
@@ -951,7 +967,7 @@ Ce sont uniquement les redirections à placer dans un bloc server{}, comme les a
&lt;action type="Rewrite" url="main/document/download.php?doc_url=/{R:2}&amp;cDir={R:1}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule v8" stopProcessing="true"&gt;
&lt;match url="^courses/([^/]+)/upload/course_home_icons/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$" /&gt;
&lt;match url="^courses/([^/]+)/upload/course_home_icons/(.*\.(js|css|png|jpg|jpeg|gif))$" /&gt;
&lt;action type="Rewrite" url="app/courses/{R:1}/upload/course_home_icons/{R:2}" /&gt;
&lt;/rule&gt;
&lt;rule name="rule v9" stopProcessing="true"&gt;

View File

@@ -122,7 +122,7 @@
<p class="p2"><span class="s1">Opzionalmente puoi fare lo stesso alle seguenti cartelle se vuoi consentire il caricamento dei pacchetti di stile CSS (CSS styles package) e la definizione di sotto-linguaggi (sub-language definition):</span></p>
<p class="p3"><span class="s1"></span><br></p>
<ul>
<li class="li2"><span class="s1">[chamilo]/main/css/</span></li>
<li class="li2"><span class="s1">[chamilo]/app/Resources/public/css/themes/</span></li>
<li class="li2"><span class="s1">[chamilo]/main/lang/</span></li>
</ul>
<p class="p3"><span class="s1"></span><br></p>
@@ -252,7 +252,7 @@
<p class="p3"><span class="s1"></span><br></p>
<p class="p2"><span class="s1">Alcuni amministratori di Chamilo hanno riportato alcuni problemi minori nella migrazione tra versioni molto diverse tra loro (per esempio nel passaggio da DokΩos a Chamilo). Questi includono la perdita di alcuni esercizi assegnati o post dei forum. Per evitare qualsiasi spiacevole effetto nei confronti dei tuoi utenti ti raccomandiamo di definire una checklist di tutti i contenuti che sono critici per te e di mantenere attiva una copia funzionante del tuo sito durante la migrazione. In questo modo sarà più semplice gestire la transizione permettendo ai tuoi utenti di accedere ai propri contenuti dalla precedente versione del sito e permettendo a te di effettuare una facile comparazione delle versioni dei contenuti. Se incontri difficoltà considera la possibilità di chiedere aiuto ad uno sviluppatore PHP o chiedere a qualcuno dei provider ufficiali di Chamilo. Essi ti daranno il supporto migliore per assicurarti una corretta migrazione di Chamilo.</span></p>
<p class="p3"><span class="s1"></span><br></p>
<p class="p2"><span class="s1">*Gli Style e le immagini sono localizzate nella cartella main/css e main/img. Puoi comunque recuperarle dal tuo backup se lo hai fatto. Qualsiasi style o image che utilizza il nome di default di style o image verrà sovrascritto nel passaggio successivo a questa fase della migrazione. Per evitare di perdere la tua personalizzazione, ricordati sempre di fare una copia degli styles e images con un nuovo nome, utilizzando e modificando questo file copia, mai loriginale. Loriginale viene sempre sovrascritto nella nuova versione. In Do€os 1.8.5 abbiamo cambiato il nome di molti temi CSS. La retro compatibilità è assicurata dal fatto che un aggiornamento aggiunge solamente nuovi temi, ma dovresti provare ed utilizzare i nuovi temi piuttosto che continuare ad utilizzare i vecchi che verranno deprecati rapidamente (ovvero non più mantenuti).</span></p>
<p class="p2"><span class="s1">*Gli Style e le immagini sono localizzate nella cartella main/img/ e app/Resources/public/css/themes/. Puoi comunque recuperarle dal tuo backup se lo hai fatto. Qualsiasi style o image che utilizza il nome di default di style o image verrà sovrascritto nel passaggio successivo a questa fase della migrazione. Per evitare di perdere la tua personalizzazione, ricordati sempre di fare una copia degli styles e images con un nuovo nome, utilizzando e modificando questo file copia, mai loriginale. Loriginale viene sempre sovrascritto nella nuova versione. In Do€os 1.8.5 abbiamo cambiato il nome di molti temi CSS. La retro compatibilità è assicurata dal fatto che un aggiornamento aggiunge solamente nuovi temi, ma dovresti provare ed utilizzare i nuovi temi piuttosto che continuare ad utilizzare i vecchi che verranno deprecati rapidamente (ovvero non più mantenuti).</span></p>
<p class="p3"><span class="s1"></span><br></p>
<p class="p3"><span class="s1"></span><br></p>
<p class="p2"><span class="s1"><b>3.4 Aggiornare da Dok€os 1.6.x</b></span></p>

View File

@@ -257,6 +257,7 @@ ALTER TABLE c_lp_item_view ADD INDEX idx_clpiv_c_i_v (c_id, id, view_count);
ALTER TABLE c_chat_connected ADD INDEX idx_course_group(c_id, to_group_id);
-- For big online exams, this index reduces the load considerably
ALTER TABLE c_quiz_question ADD INDEX idx_cqq_cidid (c_id, id);
ALTER TABLE c_quiz_question ADD INDEX idx_cqq_type (type);
-- If you have many messages with attachments
ALTER TABLE message_attachment ADD index idx_msgat_msgid (message_id);
-- If you query audit logs from track_e_default on huge tables
@@ -505,10 +506,10 @@ RewriteRule (\.(html|gif|jpg|jpeg|png|js|pdf|ico|icon|css|swf|avi|mp3|ogg|wav|tt
<p>
Since version 1.11.10, the .htaccess has been modified to do this by default with media files, and a change post-1.11.10 also does it for documents that are not in a SCORM folder. These changes will improve speed considerably but will lower the security on media files, as a direct link could be used to open the file with no validation. As such, you can comment those lines with media files to ensure increased security, at the cost of performance. These are the two lines (followed by their more wide-ranging rule) that have to be present in .htaccess for maximum efficiency.
<pre>
RewriteRule ^courses/([^/]+)/scorm/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif]))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*\.(js|css|png|jpg|jpeg|gif))$ app/courses/$1/scorm/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/scorm/(.*)$ main/document/download_scorm.php?doc_url=/$2&cDir=$1 [QSA,L]
[...]
RewriteRule ^courses/([^/]+)/document/(.*([\.js|\.css|\.png|\.jpg|\.jpeg|\.gif|\.mp4|\.webm|\.avi|\.mpeg|\.mp3|\.wav|\.ogg]))$ app/courses/$1/document/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/document/(.*\.(js|css|png|jpg|jpeg|gif|mp4|webm|avi|mpeg|mp3|wav|ogg]))$ app/courses/$1/document/$2 [QSA,L]
RewriteRule ^courses/([^/]+)/document/(.*)$ main/document/download.php?doc_url=/$2&cDir=$1 [QSA,L]
</pre>
</p>

View File

@@ -184,6 +184,11 @@ This will prevent direct access to your settings and make it seem totally the sa
but dangerous if you cannot trust people using these features. These features cannot be used by anonymous
users, but portals allowing for open registration could be particularly vulnerable.
</p>
<p>The inline editor in Chamilo 1.11 requires
the Content Security Policy (CSP) headers to include 'unsafe-inline' and 'unsafe-eval'. This is because the
editor includes a number of complex features that require those accesses. This means that your Chamilo install
will probably not validate as an "A+" score in standard CSP tests. We have no solution for this except to
prevent users from using the inline editor, which would remove a considerable level of usability from Chamilo.</p>
<br />
<hr />
<h2><a id="7.Direct-web-access">Direct web access to files</a></h2>

View File

@@ -195,9 +195,8 @@ $controller->tpl->assign('navigation_links', $controller->return_navigation_link
$controller->tpl->assign('notice_block', $controller->return_notice());
$controller->tpl->assign('help_block', $controller->return_help());
$controller->tpl->assign('student_publication_block', $controller->studentPublicationBlock());
if (api_is_platform_admin() || api_is_drh()) {
$controller->tpl->assign('skills_block', $controller->returnSkillLinks());
}
$controller->tpl->assign('skills_block', $controller->returnSkillLinks());
if (api_is_anonymous()) {
$controller->tpl->setLoginBodyClass();
}

View File

@@ -175,11 +175,16 @@ if (ChamiloApi::isAjaxRequest() && $_SERVER['REQUEST_METHOD'] === 'POST' && isse
$excludedUsers = isset($_POST['excludedUsers']) ? $_POST['excludedUsers'] : [];
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
$accessUrlId = api_get_current_access_url_id();
$excludedIds = !empty($excludedUsers) ? implode(",", array_map('intval', $excludedUsers)) : '0';
$sql = 'SELECT id, username, firstname, lastname
FROM user
WHERE status != '.ANONYMOUS.'
AND id NOT IN ('.$excludedIds.')
AND u.id IN (
SELECT user_id
FROM access_url_rel_user
WHERE access_url_id ='.$accessUrlId.')
ORDER BY id DESC
LIMIT 10';
@@ -203,8 +208,8 @@ if (ChamiloApi::isAjaxRequest() && $_SERVER['REQUEST_METHOD'] === 'POST' && isse
$first_letter_user = '';
if ((isset($_POST['form_sent']) && $_POST['form_sent']) || isset($_REQUEST['firstLetterUser'])) {
$form_sent = $_POST['form_sent'];
$elements_posted = $_POST['elements_in_name'] ?? null;
$form_sent = $_POST['form_sent'] ?? 0;
$elements_posted = $_POST['elements_in_name'] ?? [];
$first_letter_user = Security::remove_XSS($_REQUEST['firstLetterUser']);
if (!is_array($elements_posted)) {
@@ -326,6 +331,7 @@ if (1 === $activeUser) {
$filterData = [];
if ($searchForm->validate()) {
$showAllStudentByDefault = true;
$filterData = $searchForm->getSubmitValues();
foreach ($filters as $filter) {

View File

@@ -49,16 +49,20 @@ if ($form->validate()) {
$htaccess = <<<TEXT
<IfModule mod_authz_core.c>
Require all denied
# pChart generated files should be allowed
<FilesMatch "^[0-9a-f]+$">
require all granted
</FilesMatch>
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
# pChart generated files should be allowed
<FilesMatch "^[0-9a-f]+$">
# pChart generated files should be allowed
<FilesMatch "^[0-9a-f]+$">
order allow,deny
allow from all
</FilesMatch>
</FilesMatch>
</IfModule>
php_flag engine off
TEXT;

View File

@@ -147,6 +147,11 @@ if (api_is_platform_admin()) {
'url' => 'usergroups.php',
'label' => get_lang('Classes'),
];
$items[] = [
'class' => 'item-user-advanced_edit',
'url' => 'user_advanced_edit.php',
'label' => get_lang('AdvancedUserEdition'),
];
if (api_get_configuration_value('show_link_request_hrm_user')) {
$items[] = [
'class' => 'item-user-linking-requests',
@@ -177,6 +182,11 @@ if (api_is_platform_admin()) {
'label' => get_lang('Classes'),
],
];
$items[] = [
'class' => 'item-user-advanced_edit',
'url' => 'user_advanced_edit.php',
'label' => get_lang('AdvancedUserEdition'),
];
if (api_is_session_admin()) {
if ('true' === api_get_setting('limit_session_admin_role')) {
@@ -531,14 +541,20 @@ if (api_is_platform_admin()) {
}
$blockPlatform['items'] = $items;
} elseif (api_is_session_admin() && api_get_configuration_value('session_admin_access_system_announcement')) {
} elseif (api_is_session_admin()) {
$items = [];
$items[] = [
'class' => 'item-stats',
'url' => 'statistics/index.php',
'label' => get_lang('Statistics'),
];
if (api_get_configuration_value('session_admin_access_system_announcement')) {
$items[] = [
'class' => 'item-global-announcement',
'url' => 'system_announcements.php',
'label' => get_lang('SystemAnnouncements'),
];
}
$blockPlatform['items'] = $items;
}
@@ -629,14 +645,11 @@ $items[] = [
$allowCareer = api_get_configuration_value('allow_session_admin_read_careers');
if (api_is_platform_admin() || ($allowCareer && api_is_session_admin())) {
// option only visible in development mode. Enable through code if required
if (is_dir(api_get_path(SYS_TEST_PATH).'datafiller/')) {
$items[] = [
'class' => 'item-session-user-move-stats',
'url' => 'user_move_stats.php',
'label' => get_lang('MoveUserStats'),
];
}
$items[] = [
'class' => 'item-session-user-move',

View File

@@ -204,7 +204,7 @@ if (isset($_POST['Submit']) && $_POST['Submit']) {
);
// changing the Platform language
if (isset($_POST['platformlanguage']) && $_POST['platformlanguage'] != '') {
api_set_setting('platformLanguage', $_POST['platformlanguage'], null, null, $_configuration['access_url']);
api_set_setting('platformLanguage', $_POST['platformlanguage'], null, null, api_get_current_access_url_id());
}
} elseif (isset($_POST['action'])) {
switch ($_POST['action']) {
@@ -263,9 +263,8 @@ echo '<a
$sql_select = "SELECT * FROM $tbl_admin_languages";
$result_select = Database::query($sql_select);
$sql_select_lang = "SELECT * FROM $tbl_settings_current WHERE category='Languages'";
$result_select_lang = Database::query($sql_select_lang);
$row_lang = Database::fetch_array($result_select_lang);
$current_access_url = api_get_current_access_url_id();
$platformLanguage = api_get_setting('platformLanguage');
// the table data
$language_data = [];
@@ -275,7 +274,7 @@ while ($row = Database::fetch_array($result_select)) {
// the first column is the original name of the language OR a form containing the original name
if ($action == 'edit' and $row['id'] == $_GET['id']) {
$checked = '';
if ($row['english_name'] == api_get_setting('platformLanguage')) {
if ($row['english_name'] == $platformLanguage) {
$checked = ' checked="checked" ';
}
@@ -291,7 +290,7 @@ while ($row = Database::fetch_array($result_select)) {
// the third column
$row_td[] = $row['dokeos_folder'];
if ($row['english_name'] == $row_lang['selected_value']) {
if ($row['english_name'] == $platformLanguage) {
$setplatformlanguage = Display::return_icon('languages.png', get_lang('CurrentLanguagesPortal'), '', ICON_SIZE_SMALL);
} else {
$setplatformlanguage = "<a href=\"javascript:if (confirm('".addslashes(get_lang('AreYouSureYouWantToSetThisLanguageAsThePortalDefault'))."')) { location.href='".api_get_self()."?action=setplatformlanguage&id=".$row['id']."'; }\">".Display::return_icon('languages_na.png', get_lang('SetLanguageAsDefault'), '', ICON_SIZE_SMALL)."</a>";
@@ -322,7 +321,7 @@ while ($row = Database::fetch_array($result_select)) {
$allow_add_term_sub_language = '';
}
if ($row['english_name'] == $row_lang['selected_value']) {
if ($row['english_name'] == $platformLanguage) {
$row_td[] = Display::return_icon('visible.png', get_lang('Visible'))."<a href='".api_get_self()."?action=edit&id=".$row['id']."#value'>".Display::return_icon('edit.png', get_lang('Edit'), '', ICON_SIZE_SMALL)."</a>
&nbsp;".$setplatformlanguage.$allow_use_sub_language.$allow_add_term_sub_language.$allow_delete_sub_language;
} else {

View File

@@ -8,7 +8,7 @@
$cidReset = true;
require_once __DIR__.'/../../inc/global.inc.php';
api_protect_admin_script();
api_protect_admin_script(true);
$interbreadcrumb[] = ['url' => '../index.php', 'name' => get_lang('PlatformAdmin')];
@@ -18,6 +18,72 @@ $validated = false;
$sessionStatusAllowed = api_get_configuration_value('allow_session_status');
$invoicingMonth = isset($_GET['invoicing_month']) ? (int) $_GET['invoicing_month'] : '';
$invoicingYear = isset($_GET['invoicing_year']) ? (int) $_GET['invoicing_year'] : '';
$tool_name = get_lang('Statistics');
if (api_is_platform_admin()) {
$tools = [
get_lang('Courses') => [
'report=courses' => get_lang('CountCours'),
'report=tools' => get_lang('PlatformToolAccess'),
'report=courselastvisit' => get_lang('LastAccess'),
'report=coursebylanguage' => get_lang('CountCourseByLanguage'),
],
get_lang('Users') => [
'report=users' => get_lang('CountUsers'),
'report=recentlogins' => get_lang('Logins'),
'report=logins&amp;type=month' => get_lang('Logins').' ('.get_lang('PeriodMonth').')',
'report=logins&amp;type=day' => get_lang('Logins').' ('.get_lang('PeriodDay').')',
'report=logins&amp;type=hour' => get_lang('Logins').' ('.get_lang('PeriodHour').')',
'report=pictures' => get_lang('CountUsers').' ('.get_lang('UserPicture').')',
'report=logins_by_date' => get_lang('LoginsByDate'),
'report=no_login_users' => get_lang('StatsUsersDidNotLoginInLastPeriods'),
'report=zombies' => get_lang('Zombies'),
'report=users_active' => get_lang('UserStats'),
'report=users_online' => get_lang('UsersOnline'),
'report=invoicing' => get_lang('InvoicingByAccessUrl'),
'report=duplicated_users' => get_lang('DuplicatedUsers'),
'report=duplicated_users_by_mail' => get_lang('DuplicatedUsersByMail'),
],
get_lang('System') => [
'report=activities' => get_lang('ImportantActivities'),
'report=user_session' => get_lang('PortalUserSessionStats'),
'report=courses_usage' => get_lang('CoursesUsage'),
'report=quarterly_report' => get_lang('QuarterlyReport'),
],
get_lang('Social') => [
'report=messagereceived' => get_lang('MessagesReceived'),
'report=messagesent' => get_lang('MessagesSent'),
'report=friends' => get_lang('CountFriends'),
],
get_lang('Session') => [
'report=session_by_date' => get_lang('SessionsByDate'),
],
];
if ('true' === api_get_plugin_setting('lti_provider', 'enabled')) {
$tools[get_lang('Users')]['report=lti_tool_lp'] = get_lang('LearningPathLTI');
}
} elseif (api_is_session_admin()) {
$tools = [
get_lang('Session') => [
'report=session_by_date' => get_lang('SessionsByDate'),
],
];
}
// Get list of allowed reports based on role
$allowedReports = [];
foreach ($tools as $section => $items) {
foreach ($items as $key => $label) {
if (preg_match('/report=([a-zA-Z0-9_]+)/', $key, $matches)) {
$allowedReports[] = $matches[1];
}
}
}
// Ensure current report is valid for this user, or default to first available
if (!in_array($report, $allowedReports)) {
$report = reset($allowedReports);
}
if (
in_array(
@@ -334,50 +400,6 @@ if (isset($_GET['export'])) {
ob_start();
}
$tool_name = get_lang('Statistics');
$tools = [
get_lang('Courses') => [
'report=courses' => get_lang('CountCours'),
'report=tools' => get_lang('PlatformToolAccess'),
'report=courselastvisit' => get_lang('LastAccess'),
'report=coursebylanguage' => get_lang('CountCourseByLanguage'),
],
get_lang('Users') => [
'report=users' => get_lang('CountUsers'),
'report=recentlogins' => get_lang('Logins'),
'report=logins&amp;type=month' => get_lang('Logins').' ('.get_lang('PeriodMonth').')',
'report=logins&amp;type=day' => get_lang('Logins').' ('.get_lang('PeriodDay').')',
'report=logins&amp;type=hour' => get_lang('Logins').' ('.get_lang('PeriodHour').')',
'report=pictures' => get_lang('CountUsers').' ('.get_lang('UserPicture').')',
'report=logins_by_date' => get_lang('LoginsByDate'),
'report=no_login_users' => get_lang('StatsUsersDidNotLoginInLastPeriods'),
'report=zombies' => get_lang('Zombies'),
'report=users_active' => get_lang('UserStats'),
'report=users_online' => get_lang('UsersOnline'),
'report=invoicing' => get_lang('InvoicingByAccessUrl'),
'report=duplicated_users' => get_lang('DuplicatedUsers'),
'report=duplicated_users_by_mail' => get_lang('DuplicatedUsersByMail'),
],
get_lang('System') => [
'report=activities' => get_lang('ImportantActivities'),
'report=user_session' => get_lang('PortalUserSessionStats'),
'report=courses_usage' => get_lang('CoursesUsage'),
'report=quarterly_report' => get_lang('QuarterlyReport'),
],
get_lang('Social') => [
'report=messagereceived' => get_lang('MessagesReceived'),
'report=messagesent' => get_lang('MessagesSent'),
'report=friends' => get_lang('CountFriends'),
],
get_lang('Session') => [
'report=session_by_date' => get_lang('SessionsByDate'),
],
];
if ('true' === api_get_plugin_setting('lti_provider', 'enabled')) {
$tools[get_lang('Users')]['report=lti_tool_lp'] = get_lang('LearningPathLTI');
}
$course_categories = Statistics::getCourseCategories();
$content = '';

View File

@@ -209,9 +209,8 @@ $msg = '';
if (isset($_POST['SubmitAddNewLanguage'])) {
$original_name = $_POST['original_name'];
$english_name = $_POST['english_name'];
$isocode = $_POST['isocode'];
$english_name = str_replace(' ', '_', $english_name);
$english_name = api_replace_dangerous_char($_POST['english_name']);
$isocode = str_replace(' ', '_', $isocode);
$sublanguage_available = $_POST['sub_language_is_visible'];
@@ -298,14 +297,11 @@ if (isset($_GET['action']) && $_GET['action'] == 'definenewsublanguage') {
);
$class = 'add';
$form->addElement('header', '', $text);
$form->addElement('text', 'original_name', get_lang('OriginalName'), 'class="input_titles"');
$form->addRule('original_name', get_lang('ThisFieldIsRequired'), 'required');
$form->addElement('text', 'english_name', get_lang('EnglishName'), 'class="input_titles"');
$form->addRule('english_name', get_lang('ThisFieldIsRequired'), 'required');
$form->addElement('text', 'isocode', get_lang('ISOCode'), 'class="input_titles"');
$form->addRule('isocode', get_lang('ThisFieldIsRequired'), 'required');
$form->addText('original_name', get_lang('OriginalName'));
$form->addText('english_name', get_lang('EnglishName'));
$form->addText('isocode', get_lang('ISOCode'));
$form->addElement('static', null, '&nbsp;', '<i>en, es, fr</i>');
$form->addElement('checkbox', 'sub_language_is_visible', '', get_lang('Visibility'));
$form->addCheckBox('sub_language_is_visible', '', get_lang('Visibility'));
$form->addButtonCreate(get_lang('CreateSubLanguage'), 'SubmitAddNewLanguage');
//$values['original_name'] = $language_details['original_name'].'...'; -> cannot be used because of quickform filtering (freeze)
$values['english_name'] = $language_details['english_name'].'2';

View File

@@ -14,10 +14,15 @@ require_once __DIR__.'/../inc/global.inc.php';
api_protect_admin_script();
$new_language = Security::remove_XSS($_REQUEST['new_language']);
$language_variable = Security::remove_XSS($_REQUEST['variable_language']);
$language_variable = ltrim(
Security::remove_XSS($_REQUEST['variable_language']),
'$'
);
$file_id = intval($_REQUEST['file_id']);
if (isset($new_language) && isset($language_variable) && isset($file_id)) {
$variableIsValid = isset($language_variable) && preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $language_variable);
if (isset($new_language) && $variableIsValid && isset($file_id)) {
$file_language = $language_files_to_load[$file_id].'.inc.php';
$id_language = intval($_REQUEST['id']);
$sub_language_id = intval($_REQUEST['sub']);
@@ -27,12 +32,7 @@ if (isset($new_language) && isset($language_variable) && isset($file_id)) {
$all_file_of_directory = SubLanguageManager::get_all_language_variable_in_file($path_folder);
$return_value = SubLanguageManager::add_file_in_language_directory($path_folder);
//update variable language
// Replace double quotes to avoid parse errors
$new_language = str_replace('"', '\"', $new_language);
// Replace new line signs to avoid parse errors - see #6773
$new_language = str_replace("\n", "\\n", $new_language);
$all_file_of_directory[$language_variable] = "\"".$new_language."\";";
$all_file_of_directory[$language_variable] = $new_language;
$result_array = [];
foreach ($all_file_of_directory as $key_value => $value_info) {

View File

@@ -397,7 +397,13 @@ if ($form->validate()) {
}
if ($user['radio_expiration_date'] == '1') {
if (!empty($user['expiration_date'])) {
$expiration_date = $user['expiration_date'];
} else {
if (!empty($days)) {
$expiration_date = api_get_local_time('+'.$days.' day');
}
}
} else {
$expiration_date = null;
}

View File

@@ -0,0 +1,388 @@
<?php
/* For licensing terms, see /license.txt */
$cidReset = true;
require_once __DIR__.'/../inc/global.inc.php';
api_protect_admin_script(true);
$this_section = SECTION_PLATFORM_ADMIN;
$tool_name = get_lang('AdvancedUserEdition');
$message = '';
// Secure GET parameters
$parameters = [];
if (!empty($_GET)) {
foreach ($_GET as $key => $value) {
$parameters[$key] = Security::remove_XSS($value);
}
}
$interbreadcrumb[] = ['url' => 'index.php', 'name' => get_lang('PlatformAdmin')];
// Toolbar actions
$toolbarActions = '';
// Filter GET params
$keywordUsername = !empty($_GET['keywordUsername']) ? Security::remove_XSS($_GET['keywordUsername']) : '';
$keywordEmail = !empty($_GET['keywordEmail']) ? Security::remove_XSS($_GET['keywordEmail']) : '';
$keywordFirstname = !empty($_GET['keywordFirstname']) ? Security::remove_XSS($_GET['keywordFirstname']) : '';
$keywordLastname = !empty($_GET['keywordLastname']) ? Security::remove_XSS($_GET['keywordLastname']) : '';
$keywordOfficialCode = !empty($_GET['keywordOfficialCode']) ? Security::remove_XSS($_GET['keywordOfficialCode']) : '';
$keywordStatus = !empty($_GET['keywordStatus']) ? Security::remove_XSS($_GET['keywordStatus']) : '';
// Advanced search form
$form = new FormValidator('advancedSearch', 'get', '', '', [], FormValidator::LAYOUT_HORIZONTAL);
$form->addElement('header', '', get_lang('AdvancedSearch'));
$form->addText('keywordUsername', get_lang('LoginName'), false);
$form->addText('keywordEmail', get_lang('Email'), false);
$form->addText('keywordFirstname', get_lang('FirstName'), false);
$form->addText('keywordLastname', get_lang('LastName'), false);
$form->addText('keywordOfficialCode', get_lang('OfficialCode'), false);
$statusOptions = [
'%' => get_lang('All'),
STUDENT => get_lang('Student'),
COURSEMANAGER => get_lang('Teacher'),
DRH => get_lang('Drh'),
SESSIONADMIN => get_lang('SessionsAdmin'),
PLATFORM_ADMIN => get_lang('Administrator'),
];
$form->addElement('select', 'keywordStatus', get_lang('Profile'), $statusOptions);
$form->setDefaults(
[
'keywordUsername' => $keywordUsername,
'keywordEmail' => $keywordEmail,
'keywordFirstname' => $keywordFirstname,
'keywordLastname' => $keywordLastname,
'keywordOfficialCode' => $keywordOfficialCode,
'keywordStatus' => $keywordStatus,
]
);
$activeGroup = [];
$activeGroup[] = $form->createElement('checkbox', 'keywordActive', '', get_lang('Active'), ['checked' => isset($_GET['keywordActive'])]);
$activeGroup[] = $form->createElement('checkbox', 'keywordInactive', '', get_lang('Inactive'), ['checked' => isset($_GET['keywordInactive'])]);
$form->addGroup($activeGroup, '', get_lang('ActiveAccount'), null, false);
$parameters = array_map(function ($value) {
return Security::remove_XSS($value);
}, $_GET);
$extraUserField = new ExtraField('user');
$returnParams = $extraUserField->addElements(
$form,
0,
[],
true,
false,
[],
[],
$_REQUEST,
false,
true
);
$htmlHeadXtra[] = '<script>
$(function () {
'.$returnParams['jquery_ready_content'].'
})
</script>';
$form->addButtonSearch(get_lang('SearchUsers'), 'filter');
$users = [];
if (isset($_GET['filter'])) {
$users = UserManager::searchUsers($parameters);
}
$fieldSelector = '';
$jqueryReadyContent = '';
if (!empty($users)) {
$extraFields = $extraUserField->get_all(['filter = ?' => 1], 'option_order');
$editableFields = [
'firstname' => get_lang('FirstName'),
'lastname' => get_lang('LastName'),
'email' => get_lang('Email'),
'phone' => get_lang('PhoneNumber'),
'official_code' => get_lang('OfficialCode'),
'status' => get_lang('Profile'),
'active' => get_lang('ActiveAccount'),
'password' => get_lang('Password'),
];
foreach ($extraFields as $field) {
$editableFields[$field['variable']] = ucfirst($field['variable']);
}
$form->addElement('select', 'editableFields', get_lang('FieldsToEdit'), $editableFields, [
'multiple' => 'multiple',
'size' => 7,
]);
$form->addElement('submit', 'filter', get_lang('View'));
}
$tableResult = '';
if (!empty($users)) {
foreach ($users as &$user) {
$userData = api_get_user_info($user['id']);
if ($userData) {
$user = array_merge($user, $userData);
}
$extraFieldValues = new ExtraFieldValue('user');
$userExtraFields = $extraFieldValues->getAllValuesByItem($user['id']);
$formattedExtraFields = [];
foreach ($userExtraFields as $extraField) {
$formattedExtraFields[$extraField['variable']] = $extraField['value'];
}
$user['extra_fields'] = $formattedExtraFields;
}
unset($user);
$selectedFields = $_GET['editableFields'] ?? [];
$filtersUsed = [
'keywordUsername' => 'username',
'keywordEmail' => 'email',
'keywordFirstname' => 'firstname',
'keywordLastname' => 'lastname',
'keywordOfficialCode' => 'official_code',
'keywordStatus' => 'status',
];
foreach ($filtersUsed as $filterKey => $fieldName) {
$getFilterKey = Security::remove_XSS($_GET[$filterKey]);
if (!empty($getFilterKey) && !in_array($fieldName, $selectedFields)) {
$selectedFields[] = $fieldName;
}
}
foreach ($extraFields as $field) {
$extraVariable = Security::remove_XSS($_GET['extra_'.$field['variable']]);
if (is_array($extraVariable)) {
$extraVariable = array_filter($extraVariable, function ($v) {
return $v !== null && $v !== '';
});
}
if (!empty($extraVariable) && !in_array($field['variable'], $selectedFields)) {
$selectedFields[] = $field['variable'];
}
}
$parameters = array_diff_key($parameters, array_flip(['users_direction', 'users_column']));
$userTable = new SortableTable('users', null, null, 0, count($users));
$userTable->set_additional_parameters($parameters);
$userTable->setTotalNumberOfItems(count($users));
$userTable->set_header(0, get_lang('ID'));
$userTable->set_header(1, get_lang('Username'));
$columnIndex = 2;
foreach ($selectedFields as $field) {
$userTable->set_header($columnIndex, ucfirst($field));
$columnIndex++;
}
$userTable->set_header($columnIndex, get_lang('Actions'));
$userTable->addRow([]);
foreach ($users as $user) {
$row = [$user['id'], $user['username']];
foreach ($selectedFields as $field) {
$value = isset($user[$field]) ? htmlspecialchars($user[$field]) : '';
$extraFieldTypes = [];
foreach ($extraFields as $extraField) {
$extraFieldTypes[$extraField['variable']] = $extraField['field_type'];
}
if (isset($user['extra_fields'][$field])) {
$fieldType = $extraFieldTypes[$field] ?? ExtraField::FIELD_TYPE_TEXT;
$value = htmlspecialchars($user['extra_fields'][$field]);
switch ($fieldType) {
case ExtraField::FIELD_TYPE_TEXTAREA:
$row[] = '<textarea name="extra_'.$field.'['.$user['id'].']" class="form-control">'.$value.'</textarea>';
break;
case ExtraField::FIELD_TYPE_SELECT:
$fieldHtml = '<select name="extra_'.$field.'['.$user['id'].']" class="form-control">';
foreach ($extraField['options'] as $option) {
$selected = ($option['option_value'] == $value) ? 'selected' : '';
$fieldHtml .= '<option value="'.$option['option_value'].'" '.$selected.'>'.$option['display_text'].'</option>';
}
$fieldHtml .= '</select>';
$row[] = $fieldHtml;
break;
case ExtraField::FIELD_TYPE_CHECKBOX:
$checked = ($value == '1') ? 'checked' : '';
$row[] = '<input type="checkbox" name="extra_'.$field.'['.$user['id'].']" value="1" '.$checked.'>';
break;
case ExtraField::FIELD_TYPE_RADIO:
$fieldHtml = '';
foreach ($extraField['options'] as $option) {
$checked = ($option['option_value'] == $value) ? 'checked' : '';
$fieldHtml .= '<label><input type="radio" name="extra_'.$field.'['.$user['id'].']" value="'.$option['option_value'].'" '.$checked.'> '.$option['display_text'].'</label>';
}
$row[] = $fieldHtml;
break;
case ExtraField::FIELD_TYPE_TAG:
$extraTagField = $extraUserField->get_handler_field_info_by_field_variable($field);
$formattedValue = UserManager::get_user_tags_to_string(
$user['id'],
$extraTagField['id'],
false
);
$row[] = '<input type="text" name="extra_'.$field.'['.$user['id'].']" value="'.$formattedValue.'" class="form-control">'.
'<small>'.get_lang('KeywordTip').'</small>';
break;
case ExtraField::FIELD_TYPE_DOUBLE_SELECT:
if (is_array($value) && isset($value["extra_{$field}"]) && isset($value["extra_{$field}_second"])) {
$formattedValue = $value["extra_{$field}"].','.$value["extra_{$field}_second"];
} else {
$formattedValue = '';
}
$row[] = '<input type="text" name="extra_'.$field.'['.$user['id'].']" value="'.$formattedValue.'" class="form-control">'.
'<small>'.get_lang('KeywordTip').'</small>';
break;
default:
$row[] = '<input type="text" name="extra_'.$field.'['.$user['id'].']" value="'.$value.'" class="form-control">';
break;
}
} else {
if ($field === 'password') {
$row[] = '<input type="password" name="'.$field.'['.$user['id'].']" value="" class="form-control" placeholder="'.get_lang('Password').'">';
} elseif ($field === 'status') {
$statusOptions = [
STUDENT => get_lang('Student'),
COURSEMANAGER => get_lang('Teacher'),
DRH => get_lang('Drh'),
SESSIONADMIN => get_lang('SessionsAdmin'),
PLATFORM_ADMIN => get_lang('Administrator'),
];
$select = '<select name="status['.$user['id'].']" class="form-control">';
foreach ($statusOptions as $key => $label) {
$selected = ($key == $user['status']) ? 'selected' : '';
$select .= '<option value="'.$key.'" '.$selected.'>'.$label.'</option>';
}
$select .= '</select>';
$row[] = $select;
} elseif ($field === 'active') {
$checkedActive = ($user['active'] == 1) ? 'checked' : '';
$checkedInactive = ($user['active'] == 0) ? 'checked' : '';
$row[] = '<label><input type="radio" name="active['.$user['id'].']" value="1" '.$checkedActive.'> '.get_lang('Active').'</label>
<label><input type="radio" name="active['.$user['id'].']" value="0" '.$checkedInactive.'> '.get_lang('Inactive').'</label>';
} else {
$row[] = '<input type="text" name="'.$field.'['.$user['id'].']" value="'.$value.'" class="form-control">';
}
}
}
$row[] = '<button class="btn btn-primary saveUser" data-user-id="'.$user['id'].'">'.get_lang('SaveOne').'</button>';
$userTable->addRow($row);
}
$tableResult = $userTable->return_table();
}
$htmlHeadXtra[] = '<script>
$(document).ready(function() {
function getUserData(userId) {
let userData = { user_id: userId };
$("input[name$=\'[" + userId + "]\'], select[name$=\'[" + userId + "]\'], textarea[name$=\'[" + userId + "]\']").each(function() {
let fieldName = $(this).attr("name").replace("[" + userId + "]", "");
userData[fieldName] = $(this).val();
});
$("input[type=\'radio\'][name$=\'[" + userId + "]\']:checked").each(function() {
let fieldName = $(this).attr("name").replace("[" + userId + "]", "");
userData[fieldName] = $(this).val();
});
$("input[type=\'checkbox\'][name$=\'[" + userId + "]\']:checked").each(function() {
let fieldName = $(this).attr("name").replace("[" + userId + "]", "");
userData[fieldName] = "1";
});
$("input[name^=\'extra_[" + userId + "]\'], select[name^=\'extra_[" + userId + "]\'], textarea[name^=\'extra_[" + userId + "]\']").each(function() {
let fieldName = $(this).attr("name").replace("extra_[" + userId + "]", "extra_");
if ($(this).hasClass("tags-input")) {
userData[fieldName] = $(this).val().split(",");
}
else if ($(this).hasClass("doubleselect-input")) {
let values = $(this).val().split(",");
if (values.length === 2) {
userData[fieldName] = values[0];
userData[fieldName + "_second"] = values[1];
}
}
else {
userData[fieldName] = $(this).val();
}
});
return userData;
}
$(".saveUser").click(function() {
let userId = $(this).data("user-id");
if (!userId) {
return;
}
let userData = getUserData(userId);
$.post("'.api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php", {
a: "update_users",
users: JSON.stringify([userData])
}, function(response) {
alert(response.message);
}, "json");
});
$("#saveAll").click(function() {
let usersData = [];
$(".saveUser").each(function() {
let userId = $(this).data("user-id");
let userData = getUserData(userId);
if (userData) usersData.push(userData);
});
if (usersData.length === 0) {
return;
}
$.post("'.api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php", {
a: "update_users",
users: JSON.stringify(usersData)
}, function(response) {
alert(response.message);
}, "json");
});
});
</script>';
$formContent = $form->returnForm();
// Render page
$tpl = new Template($tool_name);
$tpl->assign('actions', $toolbarActions);
$tpl->assign('message', $message);
$tpl->assign('content', $formContent.$fieldSelector.$tableResult.(!empty($users) ? '<button class="btn btn-success" id="saveAll">'.get_lang('SaveAll').'</button>' : ''));
$tpl->display_one_col_template();

View File

@@ -421,7 +421,8 @@ $expiration_date = $user_data['expiration_date'];
if (empty($expiration_date)) {
$user_data['radio_expiration_date'] = 0;
$user_data['expiration_date'] = api_get_local_time();
$days = api_get_setting('account_valid_duration');
$user_data['expiration_date'] = api_get_local_time('+'.$days.' day');
} else {
$user_data['radio_expiration_date'] = 1;
$user_data['expiration_date'] = api_get_local_time($expiration_date);

View File

@@ -21,12 +21,8 @@ set_time_limit(0);
/**
* Create directories and subdirectories for backup import csv data.
*
* @param null $path
*
* @return string|null
*/
function createDirectory($path = null)
function createDirectory(?string $path = null): ?string
{
if ($path == null) {
return $path;
@@ -54,8 +50,10 @@ Options -Indexes';
$fp = fopen($data.'/.htaccess', 'w');
if ($fp) {
fwrite($fp, $block);
}
fclose($fp);
} else {
error_log("Failed to open .htaccess file in $data for writing.");
}
}
}
}
@@ -63,13 +61,7 @@ Options -Indexes';
return $data;
}
/**
* @param array $users
* @param bool $checkUniqueEmail
*
* @return array
*/
function validate_data($users, $checkUniqueEmail = false)
function validate_data(array $users, bool $checkUniqueEmail = false): array
{
global $defined_auth_sources;
$usernames = [];
@@ -94,7 +86,7 @@ function validate_data($users, $checkUniqueEmail = false)
}
}
$username = isset($user['UserName']) ? $user['UserName'] : '';
$username = $user['UserName'] ?? '';
// 2. Check username, first, check whether it is empty.
if (!UserManager::is_username_empty($username)) {
// 2.1. Check whether username is too long.
@@ -204,14 +196,12 @@ function validate_data($users, $checkUniqueEmail = false)
/**
* Add missing user-information (which isn't required, like password, username etc).
*
* @param array $user
*/
function complete_missing_data($user)
function complete_missing_data(array $user): array
{
global $purification_option_for_usernames;
$username = isset($user['UserName']) ? $user['UserName'] : '';
$username = $user['UserName'] ?? '';
// 1. Create a username if necessary.
if (UserManager::is_username_empty($username)) {
@@ -265,17 +255,14 @@ function complete_missing_data($user)
/**
* Save the imported data.
*
* @param array $users List of users
* @param bool $sendMail
*
* @uses \global variable $inserted_in_course, which returns the list of
* courses the user was inserted in
*/
function save_data(
$users,
$sendMail = false,
$targetFolder = null
) {
array $users,
bool $sendMail = false,
?string $targetFolder = null
): array {
global $inserted_in_course, $extra_fields;
// Not all scripts declare the $inserted_in_course array (although they should).
@@ -290,7 +277,6 @@ function save_data(
}
$usergroup = new UserGroup();
if (is_array($users)) {
$efo = new ExtraFieldOption('user');
$optionsByField = [];
@@ -303,7 +289,7 @@ function save_data(
$user = complete_missing_data($user);
$user['Status'] = api_status_key($user['Status']);
$redirection = isset($user['Redirection']) ? $user['Redirection'] : '';
$redirection = $user['Redirection'] ?? '';
$user_id = UserManager::create_user(
$user['FirstName'],
@@ -397,7 +383,6 @@ function save_data(
}
$user['message'] = $returnMessage;
}
}
// Save with success, error and warning users
if (!empty($targetFolder)) {
@@ -417,12 +402,12 @@ function save_data(
$csv_content[] = $csv_row;
foreach ($userSaved as $userItem) {
$csv_row = [];
$csv_row[] = isset($userItem['id']) ? $userItem['id'] : '';
$csv_row[] = isset($userItem['FirstName']) ? $userItem['FirstName'] : '';
$csv_row[] = isset($userItem['LastName']) ? $userItem['LastName'] : '';
$csv_row[] = isset($userItem['Status']) ? $userItem['Status'] : '';
$csv_row[] = isset($userItem['Email']) ? $userItem['Email'] : '';
$csv_row[] = isset($userItem['UserName']) ? $userItem['UserName'] : '';
$csv_row[] = $userItem['id'] ?? '';
$csv_row[] = $userItem['FirstName'] ?? '';
$csv_row[] = $userItem['LastName'] ?? '';
$csv_row[] = $userItem['Status'] ?? '';
$csv_row[] = $userItem['Email'] ?? '';
$csv_row[] = $userItem['UserName'] ?? '';
$csv_row[] = isset($userItem['message']) ? strip_tags($userItem['message']) : '';
$csv_content[] = $csv_row;
}
@@ -436,12 +421,12 @@ function save_data(
$csv_content[] = $csv_row;
foreach ($userError as $userItem) {
$csv_row = [];
$csv_row[] = isset($userItem['id']) ? $userItem['id'] : '';
$csv_row[] = isset($userItem['FirstName']) ? $userItem['FirstName'] : '';
$csv_row[] = isset($userItem['LastName']) ? $userItem['LastName'] : '';
$csv_row[] = isset($userItem['Status']) ? $userItem['Status'] : '-';
$csv_row[] = isset($userItem['Email']) ? $userItem['Email'] : '';
$csv_row[] = isset($userItem['UserName']) ? $userItem['UserName'] : '';
$csv_row[] = $userItem['id'] ?? '';
$csv_row[] = $userItem['FirstName'] ?? '';
$csv_row[] = $userItem['LastName'] ?? '';
$csv_row[] = $userItem['Status'] ?? '-';
$csv_row[] = $userItem['Email'] ?? '';
$csv_row[] = $userItem['UserName'] ?? '';
$csv_row[] = isset($userItem['message']) ? strip_tags($userItem['message']) : '';
$csv_content[] = $csv_row;
}
@@ -455,12 +440,12 @@ function save_data(
$csv_content[] = $csv_row;
foreach ($userWarning as $userItem) {
$csv_row = [];
$csv_row[] = isset($userItem['id']) ? $userItem['id'] : '';
$csv_row[] = isset($userItem['FirstName']) ? $userItem['FirstName'] : '';
$csv_row[] = isset($userItem['LastName']) ? $userItem['LastName'] : '';
$csv_row[] = isset($userItem['Status']) ? $userItem['Status'] : '';
$csv_row[] = isset($userItem['Email']) ? $userItem['Email'] : '';
$csv_row[] = isset($userItem['UserName']) ? $userItem['UserName'] : '';
$csv_row[] = $userItem['id'] ?? '';
$csv_row[] = $userItem['FirstName'] ?? '';
$csv_row[] = $userItem['LastName'] ?? '';
$csv_row[] = $userItem['Status'] ?? '';
$csv_row[] = $userItem['Email'] ?? '';
$csv_row[] = $userItem['UserName'] ?? '';
$csv_row[] = isset($userItem['message']) ? strip_tags($userItem['message']) : '';
$csv_content[] = $csv_row;
}
@@ -474,12 +459,8 @@ function save_data(
/**
* Save array to a specific file.
*
* @param array $data
* @param $file
* @param string $enclosure
*/
function saveCsvFile($data = [], $file = 'example', $enclosure = '"')
function saveCsvFile(array $data = [], string $file = 'example', string $enclosure = '"')
{
$filePath = $file.'.csv';
$stream = fopen($filePath, 'w');
@@ -495,21 +476,15 @@ function saveCsvFile($data = [], $file = 'example', $enclosure = '"')
$writer->writeItem($item);
}
$writer->finish();
return null;
}
/**
* @param array $users
* @param string $fileName
* @param int $sendEmail
* @param bool $checkUniqueEmail
* @param bool $resumeImport
*
* @return array
*/
function parse_csv_data($users, $fileName, $sendEmail = 0, $checkUniqueEmail = true, $resumeImport = false)
{
function parse_csv_data(
array $users,
string $fileName,
int $sendEmail = 0,
bool $checkUniqueEmail = true,
bool $resumeImport = false
): array {
$usersFromOrigin = $users;
$allowRandom = api_get_configuration_value('generate_random_login');
if ($allowRandom) {
@@ -559,13 +534,13 @@ function parse_csv_data($users, $fileName, $sendEmail = 0, $checkUniqueEmail = t
}
// Lastname is needed.
if (!isset($user['LastName']) || (isset($user['LastName']) && empty($user['LastName']))) {
if ((empty($user['LastName']))) {
unset($users[$index]);
continue;
}
// FirstName is needed.
if (!isset($user['FirstName']) || (isset($user['FirstName']) && empty($user['FirstName']))) {
if ((empty($user['FirstName']))) {
unset($users[$index]);
continue;
}
@@ -573,6 +548,11 @@ function parse_csv_data($users, $fileName, $sendEmail = 0, $checkUniqueEmail = t
$users[$index] = $user;
}
$users = array_map(
fn ($user) => Security::remove_XSS($user),
$users
);
$globalCounter = $counter;
if (!empty($importData)) {
$globalCounter = $importData['counter'] + $counter;
@@ -601,7 +581,7 @@ function parse_csv_data($users, $fileName, $sendEmail = 0, $checkUniqueEmail = t
*
* @return array All user information read from the file
*/
function parse_xml_data($file, $sendEmail = 0, $checkUniqueEmail = true)
function parse_xml_data(string $file, $sendEmail = 0, $checkUniqueEmail = true): array
{
$crawler = Import::xml($file);
$crawler = $crawler->filter('Contacts > Contact ');
@@ -631,12 +611,7 @@ function parse_xml_data($file, $sendEmail = 0, $checkUniqueEmail = true)
return $array;
}
/**
* @param array $users
* @param bool $sendMail
* @param string $targetFolder
*/
function processUsers(&$users, $sendMail, $targetFolder = null)
function processUsers(array &$users, bool $sendMail, ?string $targetFolder = null)
{
$users = save_data($users, $sendMail, $targetFolder);
@@ -703,10 +678,10 @@ if (isset($_POST['formSent']) && $_POST['formSent'] && $_FILES['import_file']['s
$allowed_file_mimetype = ['csv', 'xml'];
$error_kind_file = true;
$checkUniqueEmail = isset($_POST['check_unique_email']) ? $_POST['check_unique_email'] : null;
$sendMail = $_POST['sendMail'] ? true : false;
$resume = isset($_POST['resume_import']) ? true : false;
$askNewPassword = isset($_POST['ask_new_password']) ? true : false;
$checkUniqueEmail = $_POST['check_unique_email'] ?? false;
$sendMail = (bool) $_POST['sendMail'];
$resume = isset($_POST['resume_import']);
$askNewPassword = isset($_POST['ask_new_password']);
$uploadInfo = pathinfo($_FILES['import_file']['name']);
$ext_import_file = $uploadInfo['extension'];
$targetFolder = null;
@@ -722,7 +697,9 @@ if (isset($_POST['formSent']) && $_POST['formSent'] && $_FILES['import_file']['s
$targetFolder = api_get_configuration_value('root_sys').'app/cache/backup/import_users';
$targetFolder .= DIRECTORY_SEPARATOR.$userId.DIRECTORY_SEPARATOR.$today;
$targetFolder = createDirectory($targetFolder).DIRECTORY_SEPARATOR;
$originalFile = $targetFolder.$_FILES['import_file']['name'];
$cleanFileName = api_replace_dangerous_char($_FILES['import_file']['name']);
$cleanFileName = disable_dangerous_file($cleanFileName);
$originalFile = $targetFolder.$cleanFileName;
// save original file
if (!file_exists($originalFile)) {
touch($originalFile);
@@ -733,7 +710,7 @@ if (isset($_POST['formSent']) && $_POST['formSent'] && $_FILES['import_file']['s
$users = Import::csvToArray($_FILES['import_file']['tmp_name']);
$users = parse_csv_data(
$users,
$_FILES['import_file']['name'],
$cleanFileName,
$sendMail,
$checkUniqueEmail,
$resume
@@ -803,13 +780,13 @@ if (!empty($importData)) {
}
$formContinue->addHeader($label);
if (isset($importData['filename'])) {
$formContinue->addLabel(get_lang('File'), $importData['filename'] ?? '');
$formContinue->addLabel(get_lang('File'), $importData['filename'] ?: '');
}
$resumeStop = true;
if ($isResume) {
$totalUsers = isset($importData['complete_list']) ? count($importData['complete_list']) : 0;
$counter = isset($importData['counter']) ? $importData['counter'] : 0;
$counter = $importData['counter'] ?? 0;
$bar = '';
if (!empty($totalUsers)) {
$bar = Display::bar_progress($counter / $totalUsers * 100);
@@ -837,7 +814,7 @@ if (!empty($importData)) {
if ($isResume) {
$resumeStop = $importData['counter'] >= count($importData['complete_list']);
if ($resumeStop == false) {
if (!$resumeStop) {
$formContinue->addButtonImport(get_lang('ContinueImport'), 'import_continue');
}
}
@@ -911,7 +888,7 @@ $form->addElement(
get_lang('ResumeImport')
);
if (api_get_configuration_value('force_renew_password_at_first_login') == true) {
if (api_get_configuration_value('force_renew_password_at_first_login')) {
$form->addElement(
'checkbox',
'ask_new_password',
@@ -927,9 +904,7 @@ $defaults['sendMail'] = 0;
$defaults['file_type'] = 'csv';
$extraSettings = api_get_configuration_value('user_import_settings');
if (!empty($extraSettings) && isset($extraSettings['options']) &&
isset($extraSettings['options']['send_mail_default_option'])
) {
if (isset($extraSettings['options']['send_mail_default_option']) && !empty($extraSettings)) {
$defaults['sendMail'] = $extraSettings['options']['send_mail_default_option'];
}

View File

@@ -951,7 +951,7 @@ if (!empty($_GET['remind_inactive'])) {
if (empty($_GET['origin']) or $_GET['origin'] !== 'learnpath') {
// We are not in the learning path
Display::display_header($nameTools, get_lang('Announcements'));
Display::display_header($nameTools, 'Announcements');
}
// Tool introduction

View File

@@ -24,7 +24,7 @@
$("#comment-popup-save").on("click", function() {
var comment = $("#txt-comment").val();
if (comment == '') {
alert('<?php echo get_lang('ProvideACommentFirst'); ?>');
alert('<?php echo addslashes(get_lang('ProvideACommentFirst')); ?>');
return false;
}
var selected = $("#comment-selected").val();

View File

@@ -286,7 +286,7 @@ if (api_is_allowed_to_edit(null, true) ||
if ($allowSignature) {
$iconFullScreen = Display::url(
Display::return_icon('view_fullscreen.png', get_lang('SeeForTablet'), [], ICON_SIZE_SMALL),
api_get_self().'?'.api_get_cidreq().'&action=attendance_sheet_list&func=fullscreen&attendance_id='.$attendance_id.'&calendar_id='.$calendar['id']
api_get_self().'?'.api_get_cidreq().'&action=attendance_sheet_list&func=fullscreen&attendance_id='.$attendance_id.'&calendar_id='.$calendar['id'] . (!empty($groupId) ? '&group_id=' . $groupId : '')
);
$isBlocked = 0;
$iconBlockName = 'eyes.png';

View File

@@ -63,7 +63,7 @@
$("#sign_popup_save").on("click", function() {
if (signaturePad.isEmpty()) {
alert('<?php echo get_lang('ProvideASignatureFirst'); ?>');
alert('<?php echo addslashes(get_lang('ProvideASignatureFirst')); ?>');
return false;
}
var selected = $("#sign-selected").val();

View File

@@ -10,6 +10,11 @@
* This files provides the facebookConnect() and facebook_get_url functions
* Please edit the facebook.conf.php file to adapt it to your fb application parameter
*/
use Facebook\Exceptions\FacebookResponseException;
use Facebook\Exceptions\FacebookSDKException;
use Facebook\Facebook;
require_once __DIR__.'/../../inc/global.inc.php';
require_once __DIR__.'/facebook.init.php';
require_once __DIR__.'/functions.inc.php';
@@ -18,27 +23,29 @@ require_once __DIR__.'/functions.inc.php';
* This function connect to facebook and retrieves the user info
* If user does not exist in chamilo, it creates it and logs in
* If user already exists, it updates his info.
*
* @throws FacebookSDKException
*/
function facebookConnect()
{
$fb = new \Facebook\Facebook([
$fb = new Facebook([
'app_id' => $GLOBALS['facebook_config']['appId'],
'app_secret' => $GLOBALS['facebook_config']['secret'],
'default_graph_version' => 'v2.2',
'default_graph_version' => 'v21.0',
]);
$helper = $fb->getRedirectLoginHelper();
try {
$accessToken = $helper->getAccessToken();
} catch (Facebook\Exceptions\FacebookResponseException $e) {
} catch (FacebookResponseException $e) {
Display::addFlash(
Display::return_message('Facebook Graph returned an error: '.$e->getMessage(), 'error')
);
header('Location: '.api_get_path(WEB_PATH));
exit;
} catch (Facebook\Exceptions\FacebookSDKException $e) {
} catch (FacebookSDKException $e) {
Display::addFlash(
Display::return_message('Facebook SDK returned an error: '.$e->getMessage(), 'error')
);
@@ -79,7 +86,7 @@ function facebookConnect()
if (!$accessToken->isLongLived()) {
try {
$accessToken = $oAuth2Client->getLongLivedAccessToken($accessToken);
} catch (Facebook\Exceptions\FacebookSDKException $e) {
} catch (FacebookSDKException $e) {
Display::addFlash(
Display::return_message('Error getting long-lived access token: '.$e->getMessage(), 'error')
);
@@ -91,14 +98,14 @@ function facebookConnect()
try {
$response = $fb->get('/me?fields=id,first_name,last_name,locale,email', $accessToken->getValue());
} catch (Facebook\Exceptions\FacebookResponseException $e) {
} catch (FacebookResponseException $e) {
Display::addFlash(
Display::return_message('Graph returned an error: '.$e->getMessage(), 'error')
);
header('Location: '.api_get_path(WEB_PATH));
exit;
} catch (Facebook\Exceptions\FacebookSDKException $e) {
} catch (FacebookSDKException $e) {
Display::addFlash(
Display::return_message('Facebook SDK returned an error: '.$e->getMessage(), 'error')
);
@@ -108,10 +115,10 @@ function facebookConnect()
}
$user = $response->getGraphUser();
$language = facebookPluginGetLanguage($user['locale']);
$language = facebookPluginGetLanguage($user->getField('locale', ''));
if (!$language) {
$language = 'en_US';
$language = 'english';
}
$u = [
@@ -164,45 +171,40 @@ function facebookConnect()
/**
* Get facebook login url for the platform.
*
* @return string
* @throws FacebookSDKException
*/
function facebookGetLoginUrl()
function facebookGetLoginUrl(): string
{
$fb = new \Facebook\Facebook([
$fb = new Facebook([
'app_id' => $GLOBALS['facebook_config']['appId'],
'app_secret' => $GLOBALS['facebook_config']['secret'],
'default_graph_version' => 'v2.2',
]);
$helper = $fb->getRedirectLoginHelper();
$loginUrl = $helper->getLoginUrl(api_get_path(WEB_PATH).'?action=fbconnect', [
return $helper->getLoginUrl(api_get_path(WEB_PATH).'?action=fbconnect', [
'email',
]);
return $loginUrl;
}
/**
* Return a valid Chamilo login
* Chamilo login only use characters lettres, des chiffres et les signes _ . -.
*
* @param $in_txt
*
* @return mixed
*/
function changeToValidChamiloLogin($in_txt)
function changeToValidChamiloLogin(?string $in_txt): string
{
if (empty($in_txt)) {
return '';
}
return preg_replace("/[^a-zA-Z1-9_\-.]/", "_", $in_txt);
}
/**
* Get user language.
*
* @param string $language
*
* @return bool
*/
function facebookPluginGetLanguage($language = 'en_US')
function facebookPluginGetLanguage(string $language = 'en_US'): bool
{
$language = substr($language, 0, 2);
$sqlResult = Database::query(

View File

@@ -165,7 +165,7 @@ function external_add_user($u)
* Update the user in chamilo database. It upgrade only info that is present in the
* new_user array.
*
* @param $new_user associative array with the value to upgrade
* @param array $new_user associative array with the value to upgrade
* WARNING user_id key is MANDATORY
* Possible keys are :
* - firstname
@@ -186,11 +186,9 @@ function external_add_user($u)
* - courses : string of all courses code separated by '|'
* - admin : boolean
*
* @return bool|null
*
* @author ndiechburg <noel@cblue.be>
* */
function external_update_user($new_user)
function external_update_user(array $new_user): void
{
$old_user = api_get_user_info($new_user['user_id']);
$u = array_merge($old_user, $new_user);
@@ -214,7 +212,7 @@ function external_update_user($new_user)
$u['language'],
''
);
if (isset($u['courses']) && !empty($u['courses'])) {
if (!empty($u['courses'])) {
$autoSubscribe = explode('|', $u['courses']);
foreach ($autoSubscribe as $code) {
if (CourseManager::course_exists($code)) {

View File

@@ -298,10 +298,17 @@ function openid_verify_assertion($op_endpoint, $response) {
//TODO
$openid_association = Database::get_main_table(TABLE_MAIN_OPENID_ASSOCIATION);
$sql = sprintf("SELECT * FROM $openid_association WHERE assoc_handle = '%s'", $response['openid.assoc_handle']);
$res = Database::query($sql);
$association = Database::fetch_object($res);
if ($association && isset($association->session_type)) {
$association = Database::select(
'*',
$openid_association,
[
'where' => [
'assoc_handle = ?' => [$response['openid.assoc_handle']],
]
],
'first'
);
if ($association && isset($association['session_type'])) {
$keys_to_sign = explode(',', $response['openid.signed']);
$self_sig = _openid_signature($association, $response, $keys_to_sign);
if ($self_sig == $response['openid.sig']) {

View File

@@ -201,14 +201,14 @@ function _openid_meta_httpequiv($equiv, $html) {
/**
* Sign certain keys in a message
* @param $association - object loaded from openid_association or openid_server_association table
* @param $association - array loaded from openid_association or openid_server_association table
* - important fields are ->assoc_type and ->mac_key
* @param $message_array - array of entire message about to be sent
* @param $keys_to_sign - keys in the message to include in signature (without
* 'openid.' appended)
*/
function _openid_signature($association, $message_array, $keys_to_sign) {
$signature = '';
function _openid_signature(array $association, $message_array, $keys_to_sign): string
{
$sign_data = array();
foreach ($keys_to_sign as $key) {
@@ -218,7 +218,7 @@ function _openid_signature($association, $message_array, $keys_to_sign) {
}
$message = _openid_create_message($sign_data);
$secret = base64_decode($association->mac_key);
$secret = base64_decode($association['mac_key']);
$signature = _openid_hmac($secret, $message);
return base64_encode($signature);

View File

@@ -31,8 +31,12 @@ if (in_array($extension, ['xml', 'csv', 'imscc', 'mbz']) &&
(api_is_platform_admin(true) || api_is_drh() || CourseManager::is_course_teacher(api_get_user_id(), api_get_course_id()))
) {
$content_type = 'application/force-download';
} elseif ('zip' === $extension && $_cid && (api_is_platform_admin(true) || api_is_course_admin())) {
} elseif ('zip' === $extension) {
if ($_cid && (api_is_platform_admin(true) || api_is_course_admin())) {
$content_type = 'application/force-download';
} elseif (empty($_cid) && api_is_platform_admin()) {
$content_type = 'application/force-download';
}
}
if (empty($content_type)) {

View File

@@ -287,7 +287,7 @@ if ($visibilityChangeable) {
);
}
$url = api_get_path(WEB_CODE_PATH)."auth/inscription.php?c=$course_code&e=1";
$url = api_get_path(WEB_CODE_PATH)."auth/inscription.php?c=$course_code&";
$url = Display::url($url, $url);
$label = $form->addLabel(get_lang('DirectLink'), sprintf(get_lang('CourseSettingsRegisterDirectLink'), $url), true);

View File

@@ -33,7 +33,7 @@ if (!api_is_coach()) {
api_not_allowed(true);
}
$action = isset($_POST['action']) ? $_POST['action'] : '';
$action = $_POST['action'] ?? '';
$courseId = api_get_course_int_id();
$courseInfo = api_get_course_info_by_id($courseId);
@@ -52,10 +52,7 @@ $interbreadcrumb[] = [
'name' => get_lang('Maintenance'),
];
/**
* @param string $name
*/
function make_select_session_list($name, $sessions, $attr = [])
function make_select_session_list($name, $sessions, $attr = []): string
{
$attrs = '';
if (count($attr) > 0) {
@@ -184,7 +181,7 @@ function displayForm()
echo $html;
}
function searchCourses($idSession, $type)
function searchCourses($idSession, $type): xajaxResponse
{
$xajaxResponse = new xajaxResponse();
$return = null;
@@ -193,6 +190,7 @@ function searchCourses($idSession, $type)
if (!empty($type)) {
$idSession = (int) $idSession;
$courseList = SessionManager::get_course_list_by_session_id($idSession);
$course_list_destination = [];
$return .= '<select id="destination" name="SessionCoursesListDestination[]" style="width:380px;" >';
@@ -292,8 +290,6 @@ if (($action === 'course_select_form') ||
$cr = new CourseRestorer($course);
$cr->restore($destinationCourse, $destinationSession);
echo Display::return_message(get_lang('CopyFinished'), 'confirmation');
displayForm();
} else {
$arrCourseOrigin = [];
$arrCourseDestination = [];
@@ -333,16 +329,15 @@ if (($action === 'course_select_form') ||
$cr->restore($courseDestination, $destinationSession);
echo Display::return_message(get_lang('CopyFinished'), 'confirmation');
}
displayForm();
} else {
echo Display::return_message(
get_lang('YouMustSelectACourseFromOriginalSession'),
'error'
);
}
}
displayForm();
}
}
} elseif (isset($_POST['copy_option']) && $_POST['copy_option'] == 'select_items') {
// Else, if a CourseSelectForm is requested, show it
if (api_get_setting('show_glossary_in_documents') != 'none') {

View File

@@ -51,6 +51,7 @@ if ($action === 'course_select_form' && Security::check_token('post')) {
// Rebuild the course object based on selected resources
$cb = new CourseBuilder('partial');
$course = $cb->build(0, null, false, array_keys($selectedResources), $selectedResources);
$course = CourseSelectForm::get_posted_course(null, 0, '', $course);
// Get admin details
$adminId = (int) $_POST['admin_id'];
@@ -137,7 +138,7 @@ if ($action === 'course_select_form' && Security::check_token('post')) {
$exportDir = 'moodle_export_'.$courseId;
try {
$moodleVersion = isset($values['moodle_version']) ? $values['moodle_version'] : '3';
$moodleVersion = $values['moodle_version'] ?? '3';
$mbzFile = $exporter->export($courseId, $exportDir, $moodleVersion);
echo Display::return_message(get_lang('MoodleExportCreated'), 'confirm');
echo '<br />';

View File

@@ -715,8 +715,6 @@ class ImportCsv
if (!empty($data)) {
$this->logger->addInfo(count($data)." records found.");
$expirationDateOnCreation = api_get_utc_datetime(strtotime("+".intval($this->expirationDateInUserCreation)."years"));
$expirationDateOnUpdate = api_get_utc_datetime(strtotime("+".intval($this->expirationDateInUserUpdate)."years"));
$batchSize = $this->batchSize;
$em = Database::getManager();
@@ -752,7 +750,7 @@ class ImportCsv
$row['phone'],
null, //$row['picture'], //picture
$row['auth_source'], // ?
$expirationDateOnCreation, //'0000-00-00 00:00:00', //$row['expiration_date'], //$expiration_date = '0000-00-00 00:00:00',
null,
1, //active
0,
null, // extra
@@ -800,7 +798,7 @@ class ImportCsv
$userInfo['official_code'],
$userInfo['phone'],
$userInfo['picture_uri'],
$expirationDateOnUpdate,
null,
$userInfo['active'],
null, //$creator_id = null,
0, //$hr_dept_id = 0,
@@ -892,13 +890,6 @@ class ImportCsv
$language = $this->defaultLanguage;
$this->logger->addInfo(count($data)." records found.");
$expirationDateOnCreate = api_get_utc_datetime(
strtotime("+".intval($this->expirationDateInUserCreation)."years")
);
$expirationDateOnUpdate = api_get_utc_datetime(
strtotime("+".intval($this->expirationDateInUserUpdate)."years")
);
$counter = 1;
$secondsInYear = 365 * 24 * 60 * 60;
@@ -965,7 +956,7 @@ class ImportCsv
$row['phone'],
null, //$row['picture'], //picture
$row['auth_source'], // ?
$expirationDateOnCreate,
null,
1, //active
0,
null, // extra
@@ -1000,11 +991,6 @@ class ImportCsv
continue;
}
if (isset($row['action']) && $row['action'] === 'delete') {
// Inactive one year later
$userInfo['expiration_date'] = api_get_utc_datetime(api_strtotime(time() + $secondsInYear));
}
$password = $row['password']; // change password
$email = $row['email']; // change email
$resetPassword = 2; // allow password change
@@ -1075,7 +1061,7 @@ class ImportCsv
$userInfo['official_code'],
$userInfo['phone'],
$userInfo['picture_uri'],
$expirationDateOnUpdate,
null,
$userInfo['active'],
null, //$creator_id = null,
0, //$hr_dept_id = 0,

View File

@@ -0,0 +1,681 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This script imports users from an XLSX file, compares them to existing users in the Chamilo database,
* updates or inserts users based on specific rules, and generates XLSX files for accounts with missing
* email, lastname, or username, and exports accounts with duplicate Mail or Nom Prénom fields.
* Uses 'Nom' and 'Prénom' columns for name duplicates and includes 'Actif' in exports.
* - Skips import of users with empty 'Actif' unless they exist in DB (then updates, including inactive status)
* - Updates existing users by username (always generated via generateProposedLogin) instead of importing as new
* - Stores 'Matricule' as extra field 'external_user_id' without trimming leading zeros
* - Logs decisions (skipped, updated, inserted) with details in simulation or proceed mode
* - Stops processing on two consecutive empty rows in XLSX
* - Allows custom output directory for XLSX files via command line
* - Always generates username using generateProposedLogin()
* - Username format: lastname + first letter of each firstname word; for active duplicates, append next letter from last firstname part
* - For 3+ occurrences of lastname + firstname, append increasing letters from last firstname part (e.g., jpii, jpiii)
* - Generates unmatched_db_users.xlsx listing database users not found in the input XLSX based on username
* - Exports terminal output to import-yyyymmddhhiiss.log in the output directory.
*/
// Ensure the script is run from the command line
if (php_sapi_name() !== 'cli') {
exit('This script must be run from the command line.');
}
// Configuration
$domain = 'example.com'; // Manually configured domain for generated emails
// Include Chamilo bootstrap and necessary classes
require_once __DIR__.'/../../main/inc/global.inc.php';
// Include PHPExcel classes (assuming installed via Composer)
require_once __DIR__.'/../../vendor/autoload.php';
require_once __DIR__.'/../../vendor/phpoffice/phpexcel/Classes/PHPExcel.php';
require_once __DIR__.'/../../vendor/phpoffice/phpexcel/Classes/PHPExcel/IOFactory.php';
// Command-line arguments parsing (without getopt)
$proceed = false;
$outputDir = '/tmp'; // Default output directory
$xlsxFile = $argv[1] ?? ''; // Expect XLSX file path as first argument
// Parse arguments manually
for ($i = 2; $i < count($argv); $i++) {
$arg = $argv[$i];
if ($arg === '--proceed' || $arg === '-p') {
$proceed = true;
} elseif (preg_match('/^--output-dir=(.+)$/', $arg, $matches)) {
$outputDir = $matches[1];
} elseif ($arg === '-o' && $i + 1 < count($argv)) {
$outputDir = $argv[++$i];
} elseif (preg_match('/^--mail-domain=(.+)$/', $arg, $matches)) {
$domain = $matches[1];
}
}
// Validate and prepare output directory
if (!is_dir($outputDir)) {
if (!mkdir($outputDir, 0755, true)) {
exit("Error: Could not create output directory '$outputDir'\n");
}
}
if (!is_writable($outputDir)) {
exit("Error: Output directory '$outputDir' is not writable\n");
}
// Ensure trailing slash for consistency
$outputDir = rtrim($outputDir, '/').'/';
// Start output buffering to capture terminal output
ob_start();
// Add start timestamp
$startTime = new DateTime();
echo '['.$startTime->format('Y-m-d H:i:s')."] Script started\n";
// Debug: Log parsed arguments
echo "Parsed arguments:\n";
echo " XLSX file: $xlsxFile\n";
echo " Proceed: ".($proceed ? 'true' : 'false')."\n";
echo " Output directory: $outputDir\n";
if (empty($xlsxFile) || !file_exists($xlsxFile)) {
exit("Usage: php import_users_from_xlsx.php <path_to_xlsx_file> [-p|--proceed] [-o <directory>|--output-dir=<directory>]\n");
}
// Initialize database connection
global $database;
// Load XLSX file
try {
$inputFileType = PHPExcel_IOFactory::identify($xlsxFile);
$reader = PHPExcel_IOFactory::createReader($inputFileType);
$phpExcel = $reader->load($xlsxFile);
$worksheet = $phpExcel->getActiveSheet();
$xlsxRows = $worksheet->toArray();
} catch (Exception $e) {
exit("Error loading XLSX file: {$e->getMessage()}\n");
}
// Map XLSX columns to Chamilo database user table fields
$xlsxColumnMap = [
'Nom' => 'lastname',
'Prénom' => 'firstname',
'Nom Prénom' => 'fullname',
'Mail' => 'email',
'Matricule' => 'official_code',
'N° de badge' => 'password',
'Tel mobile' => 'phone',
'Actif' => 'active',
];
// Extract headers and validate
$xlsxHeaders = array_shift($xlsxRows);
$xlsxColumnIndices = [];
foreach ($xlsxColumnMap as $xlsxHeader => $dbField) {
$index = array_search($xlsxHeader, $xlsxHeaders);
if ($index === false) {
exit("Missing required column: {$xlsxHeader}\n");
}
$xlsxColumnIndices[$dbField] = $index;
}
// Initialize arrays to store rows with missing fields, duplicates, and XLSX usernames
$emailMissing = [];
$lastnameMissing = [];
$usernameMissing = [];
$xlsxEmailCounts = [];
$xlsxNameCounts = [];
$duplicateEmails = [];
$duplicateNames = [];
$xlsxUsernames = []; // Store usernames from XLSX
// Output columns for missing field and duplicate files
$outputColumns = ['Matricule', 'Nom', 'Prénom', 'Nom Prénom', 'Mail', 'N° de badge', 'Actif', 'Generated login'];
// Normalize string for duplicate detection
function normalizeName($name)
{
$name = strtolower(trim($name));
$name = preg_replace('/[\s-]+/', ' ', $name);
return $name;
}
// Remove accents from strings
function removeAccents($str)
{
$str = str_replace(
['à', 'á', 'â', 'ã', 'ä', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', 'ù', 'ú', 'û', 'ü', 'ý', 'ÿ', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ù', 'Ú', 'Û', 'Ü', 'Ý'],
['a', 'a', 'a', 'a', 'a', 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'n', 'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u', 'y', 'y', 'A', 'A', 'A', 'A', 'A', 'A', 'C', 'E', 'E', 'E', 'E', 'I', 'I', 'I', 'I', 'N', 'O', 'O', 'O', 'O', 'O', 'U', 'U', 'U', 'U', 'Y'],
$str
);
return $str;
}
// Generate login based on lastname and firstname
function generateProposedLogin($xlsxLastname, $xlsxFirstname, $isActive, &$usedLogins)
{
$lastname = strtolower(trim(removeAccents($xlsxLastname)));
$lastname = preg_replace('/[\s-]+/', '', $lastname);
$firstname = trim(removeAccents($xlsxFirstname));
$firstnameParts = preg_split('/[\s-]+/', $firstname, -1, PREG_SPLIT_NO_EMPTY);
$firstLetters = '';
foreach ($firstnameParts as $part) {
if (!empty($part)) {
$firstLetters .= strtolower(substr($part, 0, 1));
}
}
// Base username: lastname + first letter of each firstname word
$baseLogin = $lastname.$firstLetters;
$login = $baseLogin;
// Get last part of firstname for duplicate resolution
$lastFirstnamePart = end($firstnameParts);
$lastPartLetters = strtolower(preg_replace('/[\s-]+/', '', $lastFirstnamePart));
// Increment occurrence count for this login
$usedLogins['counts'][$login] = isset($usedLogins['counts'][$login]) ? $usedLogins['counts'][$login] + 1 : 1;
$occurrence = $usedLogins['counts'][$login];
// Handle duplicates
if (isset($usedLogins['logins'][$login])) {
// Only modify if both current and previous users are active
if ($isActive && $usedLogins['logins'][$login]['active']) {
if ($occurrence == 2) {
// Second occurrence: append next letter from last firstname part
if (strlen($lastPartLetters) > 1) {
$login = $baseLogin.substr($lastPartLetters, 1, 1); // e.g., 'i' from 'Pierre'
} else {
$login = $baseLogin.'1'; // Fallback if no more letters
}
} elseif ($occurrence >= 3) {
// Third+ occurrence: append increasing letters from last firstname part
$extraLetters = min($occurrence - 1, strlen($lastPartLetters) - 1); // e.g., 2 letters for 3rd, 3 for 4th
if ($extraLetters > 0) {
$login = $baseLogin.substr($lastPartLetters, 1, $extraLetters); // e.g., 'ii', 'iii'
} else {
$login = $baseLogin.($occurrence - 1); // Fallback to number
}
}
}
}
// Ensure uniqueness by appending a number if still conflicting
$suffix = 1;
$originalLogin = $login;
while (isset($usedLogins['logins'][$login])) {
$login = $originalLogin.$suffix;
$suffix++;
}
// Store login with active status
$usedLogins['logins'][$login] = ['active' => $isActive];
return $login;
}
// Generate XLSX files for missing fields and duplicates
function createMissingFieldFile($filename, $rows, $columns)
{
if (empty($rows)) {
echo "No rows to write for $filename\n";
return;
}
$phpExcel = new PHPExcel();
$worksheet = $phpExcel->getActiveSheet();
foreach ($columns as $colIndex => $column) {
$worksheet->setCellValueByColumnAndRow($colIndex, 1, $column);
}
foreach ($rows as $rowIndex => $rowData) {
foreach ($columns as $colIndex => $column) {
$worksheet->setCellValueByColumnAndRow($colIndex, $rowIndex + 2, $rowData[$column]);
}
}
try {
$writer = PHPExcel_IOFactory::createWriter($phpExcel, 'Excel2007');
$writer->save($filename);
echo "Generated $filename with ".count($rows)." rows\n";
} catch (Exception $e) {
echo "Error saving $filename: {$e->getMessage()}\n";
}
}
// Detect potential issues in XLSX file
$usedLogins = ['logins' => [], 'counts' => []];
$generatedEmailCounts = [];
$emptyRowCount = 0;
foreach ($xlsxRows as $rowIndex => $xlsxRow) {
// Check for empty row
$isEmpty = true;
foreach ($xlsxRow as $cell) {
if (!empty(trim($cell))) {
$isEmpty = false;
break;
}
}
if ($isEmpty) {
$emptyRowCount++;
if ($emptyRowCount >= 2) {
echo "Stopping processing: Found two consecutive empty rows at row ".($rowIndex + 2)."\n";
break;
}
continue;
} else {
$emptyRowCount = 0; // Reset counter if row is not empty
}
$xlsxUserData = [];
foreach ($xlsxColumnMap as $dbField) {
$xlsxUserData[$dbField] = $xlsxRow[$xlsxColumnIndices[$dbField]] ?? '';
}
// Generate username and store it
$isActive = !empty($xlsxUserData['active']);
$xlsxUserData['username'] = generateProposedLogin($xlsxUserData['lastname'], $xlsxUserData['firstname'], $isActive, $usedLogins);
$xlsxUsernames[] = $xlsxUserData['username'];
$rowData = [
'Matricule' => $xlsxUserData['official_code'],
'Nom' => $xlsxUserData['lastname'],
'Prénom' => $xlsxUserData['firstname'],
'Nom Prénom' => $xlsxUserData['fullname'],
'Mail' => $xlsxUserData['email'],
'N° de badge' => $xlsxUserData['password'],
'Actif' => $xlsxUserData['active'],
'Generated login' => $xlsxUserData['username'],
];
if ($isActive) {
if (empty($xlsxUserData['email']) && strpos($xlsxUserData['official_code'], '0009') !== false) {
$emailLastnameParts = preg_split('/[\s-]+/', trim(removeAccents($xlsxUserData['lastname'])), -1, PREG_SPLIT_NO_EMPTY);
$emailLastname = !empty($emailLastnameParts[0]) ? strtolower($emailLastnameParts[0]) : '';
$emailFirstnameParts = preg_split('/[\s-]+/', trim(removeAccents($xlsxUserData['firstname'])), -1, PREG_SPLIT_NO_EMPTY);
$emailFirstname = !empty($emailFirstnameParts[0]) ? strtolower($emailFirstnameParts[0]) : '';
$baseEmail = "{$emailLastname}.{$emailFirstname}@{$domain}";
$generatedEmail = $baseEmail;
$suffix = isset($generatedEmailCounts[$baseEmail]) ? count($generatedEmailCounts[$baseEmail]) + 1 : 1;
if ($suffix > 1) {
$generatedEmail = "{$emailLastname}.{$emailFirstname}{$suffix}@{$domain}";
}
$generatedEmail = strtoupper($generatedEmail);
$generatedEmailCounts[$baseEmail][] = $rowData;
$rowData['Mail'] = $generatedEmail;
$xlsxUserData['email'] = $generatedEmail;
$emailMissing[] = $rowData;
$xlsxEmailCounts[$generatedEmail][] = $rowData;
} elseif (empty($xlsxUserData['email'])) {
$emailMissing[] = $rowData;
}
if (empty($xlsxUserData['lastname'])) {
$lastnameMissing[] = $rowData;
}
// All usernames are generated
$usernameMissing[] = $rowData;
$email = strtolower(trim($xlsxUserData['email']));
$name = normalizeName($xlsxUserData['fullname']);
if (!empty($email)) {
$xlsxEmailCounts[$email][] = $rowData;
}
if (!empty($xlsxUserData['fullname'])) {
$xlsxNameCounts[$name][] = $rowData;
}
}
}
foreach ($xlsxEmailCounts as $email => $rows) {
if (count($rows) > 1) {
$duplicateEmails = array_merge($duplicateEmails, $rows);
}
}
foreach ($xlsxNameCounts as $name => $rowData) {
if (count($rowData) > 1) {
$duplicateNames = array_merge($duplicateNames, $rowData);
}
}
usort($duplicateEmails, function ($a, $b) {
return strcmp(strtolower($a['Mail'] ?? ''), strtolower($b['Mail'] ?? ''));
});
usort($duplicateNames, function ($a, $b) {
return strcmp(normalizeName($a['Nom Prénom'] ?? ''), normalizeName($b['Nom Prénom'] ?? ''));
});
createMissingFieldFile($outputDir.'email_missing.xlsx', $emailMissing, $outputColumns);
createMissingFieldFile($outputDir.'lastname_missing.xlsx', $lastnameMissing, $outputColumns);
createMissingFieldFile($outputDir.'username_missing.xlsx', $usernameMissing, $outputColumns);
createMissingFieldFile($outputDir.'duplicate_email.xlsx', $duplicateEmails, $outputColumns);
createMissingFieldFile($outputDir.'duplicate_name.xlsx', $duplicateNames, $outputColumns);
// Generate unmatched_db_users.xlsx
$unmatchedUsers = [];
$sql = "SELECT id, username, official_code, email FROM user";
$stmt = $database->query($sql);
while ($dbUser = $stmt->fetch()) {
if (!in_array($dbUser['username'], $xlsxUsernames) && !empty($dbUser['username'])) {
$unmatchedUsers[] = [
'Matricule' => $dbUser['official_code'],
'Username' => $dbUser['username'],
'User ID' => $dbUser['id'],
'E-mail' => $dbUser['email'],
];
}
}
$unmatchedColumns = ['Matricule', 'Username', 'User ID', 'E-mail'];
createMissingFieldFile($outputDir.'unmatched_db_users.xlsx', $unmatchedUsers, $unmatchedColumns);
// Process users: compare with database, log decisions, and update/insert if --proceed
echo "\n=== Processing Users ===\n";
$userManager = new UserManager();
$usedLogins = ['logins' => [], 'counts' => []]; // Reset usedLogins to avoid false duplicates
$emptyRowCount = 0;
$userActions = []; // Initialize array to store user actions
foreach ($xlsxRows as $rowIndex => $rowData) {
// Check for empty row
$isEmpty = true;
foreach ($rowData as $cell) {
if (!empty(trim($cell))) {
$isEmpty = false;
break;
}
}
if ($isEmpty) {
$emptyRowCount++;
if ($emptyRowCount >= 2) {
echo "Stopping processing: Found two consecutive empty rows at row ".($rowIndex + 2)."\n";
break;
}
continue;
} else {
$emptyRowCount = 0; // Reset counter if row is not empty
}
$xlsxUserData = [];
foreach ($xlsxColumnMap as $dbField) {
$xlsxUserData[$dbField] = $rowData[$xlsxColumnIndices[$dbField]] ?? '';
}
// Generate username
$isActive = !empty($xlsxUserData['active']);
$xlsxUserData['username'] = generateProposedLogin($xlsxUserData['lastname'], $xlsxUserData['firstname'], $isActive, $usedLogins);
$dbUsername = Database::escape_string($xlsxUserData['username']);
// Get current time for row logging
$rowTime = new DateTime();
// Skip users with Matricule starting with 0009
if (strpos($xlsxUserData['official_code'], '0009') === 0) {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - Matricule starts with 0009 (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxUserData['official_code'],
'Updated Fields' => 'Matricule starts with 0009',
];
continue;
}
// Check for existing user by username
$sql = "SELECT id, firstname, lastname, email, official_code, phone, active
FROM user
WHERE username = '$dbUsername'";
$stmt = $database->query($sql);
$dbUser = $stmt->fetch();
// Prepare data for logging and potential import/update
$xlsxMatricule = $xlsxUserData['official_code'] ?? ''; // Keep leading zeros
$xlsxActive = !empty($xlsxUserData['active']) ? 1 : 0;
// Decision logic
if (empty($dbUser) && empty($xlsxUserData['active'])) {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - 'Actif' is empty and no matching user in database (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Actif is empty and no matching user in database',
];
continue;
}
// Validate required fields
$requiredFields = ['lastname', 'firstname', 'email'];
$missingFields = [];
foreach ($requiredFields as $field) {
if (empty($xlsxUserData[$field])) {
$missingFields[] .= $field;
}
}
if (!empty($missingFields)) {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).': Skipped - missing fields: '.implode(', ', $missingFields)." (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Missing fields: '.implode(', ', $missingFields),
];
continue;
}
if ($dbUser) {
// Check for updates
$updates = [];
if ($dbUser['firstname'] !== $xlsxUserData['firstname']) {
$updates[] .= "firstname: '".$dbUser['firstname']."' -> '".$xlsxUserData['firstname']."' ";
}
if ($dbUser['lastname'] !== $xlsxUserData['lastname']) {
$updates[] .= "lastname: '".$dbUser['lastname']."' -> '".$xlsxUserData['lastname']."' ";
}
if ($dbUser['email'] !== $xlsxUserData['email']) {
$updates[] .= "email: '".$dbUser['email']."' -> '".$xlsxUserData['email']."' ";
}
if ($dbUser['official_code'] !== $xlsxUserData['official_code']) {
$updates[] .= "official_code: '".$dbUser['official_code']."' -> '".$xlsxUserData['official_code']."' ";
}
if ($dbUser['phone'] !== ($xlsxUserData['phone'] ?? '')) {
$updates[] .= "phone: '".$dbUser['phone']."' -> '".($xlsxUserData['phone'] ?? '')."' ";
}
if ($dbUser['active'] != $xlsxActive) {
$updates[] .= "active: ".$dbUser['active']." -> '".$xlsxActive."' ";
}
if (!empty($updates)) {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Update - Existing user found, updates needed (username: $dbUsername)\n";
echo " Updates: ".implode(', ', $updates)."\n";
if ($proceed) {
try {
$user = UserManager::update_user(
$dbUser['id'],
$xlsxUserData['firstname'],
$xlsxUserData['lastname'],
$xlsxUserData['username'], // username generated from lastname + firstname's first letter (although it should not change, it is required by the update_user method)
null, // password not updated
null, // auth_source
$xlsxUserData['email'],
null, // status
$xlsxUserData['official_code'],
$xlsxUserData['phone'],
null, // picture_uri
null, // expiration_date
$xlsxActive
);
if ($user) {
// Update extra field 'external_user_id'
UserManager::update_extra_field_value($dbUser['id'], 'external_user_id', $xlsxMatricule);
echo " Success: Updated user and external_user_id (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'updated',
'User ID' => $dbUser['id'],
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => implode(', ', array_map(function ($update) { return trim(explode(':', $update)[0]); }, $updates)),
];
} else {
echo " Error: Could not update user (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'skipped',
'User ID' => $dbUser['id'],
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Could not update user',
];
}
} catch (Exception $e) {
echo " Error: Failed to update user (username: $dbUsername): {$e->getMessage()}\n";
$userActions[] = [
'Action Type' => 'skipped',
'User ID' => $dbUser['id'],
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Failed to update user: '.$e->getMessage(),
];
}
} else {
echo " Sim mode: Updated user and external_user_id (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'updated',
'User ID' => $dbUser['id'],
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => implode(', ', array_map(function ($update) { return trim(explode(':', $update)[0]); }, $updates)),
];
}
} else {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": No action - no changes needed (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'skipped',
'User ID' => $dbUser['id'],
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'No changes needed',
];
}
} else {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Insert new user - No existing user found (username: $dbUsername)\n";
if ($proceed) {
try {
$password = !empty($xlsxUserData['password']) ? $xlsxUserData['password'] : 'temporary_password';
$userId = $userManager->create_user(
$xlsxUserData['firstname'],
$xlsxUserData['lastname'],
5, // status (5 = student, adjust as needed)
$xlsxUserData['email'],
$dbUsername,
$password,
$xlsxUserData['official_code'],
'', // language
$xlsxUserData['phone'],
'',
null,
null,
$xlsxActive,
null,
null // creator_id
);
if ($userId) {
// Add extra field 'external_user_id'
$userManager->update_extra_field_value($userId, 'external_user_id', $xlsxMatricule);
echo " Success: Created user and set external_user_id (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'created',
'User ID' => $userId,
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => '',
];
} else {
echo " Error: Could not create user (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Could not create user',
];
}
} catch (Exception $e) {
echo " Error: Failed to insert user (username: $dbUsername): {$e->getMessage()}\n";
$userActions[] = [
'Action Type' => 'skipped',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Failed to insert user: '.$e->getMessage(),
];
}
} else {
echo " Sim mode: Inserted user and external_user_id (username: $dbUsername)\n";
$userActions[] = [
'Action Type' => 'created',
'User ID' => '',
'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'],
'External User ID' => $xlsxMatricule,
'Updated Fields' => '',
];
}
}
}
// Generate user actions XLSX file
$actionColumns = ['Action Type', 'User ID', 'Username', 'Official Code', 'E-mail', 'External User ID', 'Updated Fields'];
createMissingFieldFile($outputDir.'user_actions.xlsx', $userActions, $actionColumns);
if (!$proceed) {
echo "\nUse --proceed to apply changes to the database.\n";
} else {
echo "\nImport completed successfully.\n";
}
// Save terminal output to log file
$output = ob_get_clean();
echo $output; // Output to terminal
$endTime = new DateTime();
$output .= '['.$endTime->format('Y-m-d H:i:s')."] Script completed\n";
$logTimestamp = $startTime->format('YmdHis');
$logFile = $outputDir.'import-'.$logTimestamp.'.log';
if (!file_put_contents($logFile, $output)) {
echo "Error: Could not write to log file $logFile\n";
} else {
echo "Generated log file: $logFile\n";
}

View File

@@ -190,10 +190,7 @@ class langstats
$vars = [];
$priority = ['trad4all'];
foreach ($priority as $file) {
$list = SubLanguageManager::get_all_language_variable_in_file(
$path.$file.'.inc.php',
true
);
$list = SubLanguageManager::get_all_language_variable_in_file($path.$file.'.inc.php');
foreach ($list as $var => $trad) {
$vars[$var] = $file.'.inc.php';
}
@@ -203,10 +200,7 @@ class langstats
if (substr($file, 0, 1) == '.' or in_array($file, $priority)) {
continue;
}
$list = SubLanguageManager::get_all_language_variable_in_file(
$path.$file,
true
);
$list = SubLanguageManager::get_all_language_variable_in_file($path.$file);
foreach ($list as $var => $trad) {
$vars[$var] = $file;
}

View File

@@ -20,7 +20,7 @@ $list = SubLanguageManager::get_lang_folder_files_list($path);
foreach ($list as $entry) {
$file = $path.'/'.$entry;
if (is_file($file)) {
$terms = array_merge($terms, SubLanguageManager::get_all_language_variable_in_file($file, true));
$terms = array_merge($terms, SubLanguageManager::get_all_language_variable_in_file($file));
}
}
// get only the array keys (the language variables defined in language files)

View File

@@ -20,7 +20,7 @@ $list = SubLanguageManager::get_lang_folder_files_list($path);
foreach ($list as $entry) {
$file = $path.'/'.$entry;
if (is_file($file)) {
$terms = array_merge($terms, SubLanguageManager::get_all_language_variable_in_file($file, true));
$terms = array_merge($terms, SubLanguageManager::get_all_language_variable_in_file($file));
}
}
// get only the array keys (the language variables defined in language files)

View File

@@ -20,7 +20,7 @@ $list = SubLanguageManager::get_lang_folder_files_list($path);
foreach ($list as $entry) {
$file = $path.'/'.$entry;
if (is_file($file)) {
$terms = array_merge($terms, SubLanguageManager::get_all_language_variable_in_file($file, true));
$terms = array_merge($terms, SubLanguageManager::get_all_language_variable_in_file($file));
}
}
foreach ($terms as $index => $translation) {

View File

@@ -0,0 +1,216 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class AnswerInOfficeDoc
* Allows a question type where the answer is written in an Office document.
*
* @author Cristian
*/
class AnswerInOfficeDoc extends Question
{
public $typePicture = 'options_evaluation.png';
public $explanationLangVar = 'AnswerInOfficeDoc';
public $sessionId;
public $userId;
public $exerciseId;
public $exeId;
private $storePath;
private $fileName;
private $filePath;
/**
* Constructor.
*/
public function __construct()
{
if ('true' !== OnlyofficePlugin::create()->get('enable_onlyoffice_plugin')) {
throw new Exception(get_lang('OnlyOfficePluginRequired'));
}
parent::__construct();
$this->type = ANSWER_IN_OFFICE_DOC;
$this->isContent = $this->getIsContent();
}
/**
* Initialize the file path structure.
*/
public function initFile(int $sessionId, int $userId, int $exerciseId, int $exeId): void
{
$this->sessionId = $sessionId ?: 0;
$this->userId = $userId;
$this->exerciseId = $exerciseId ?: 0;
$this->exeId = $exeId;
$this->storePath = $this->generateDirectory();
$this->fileName = $this->generateFileName();
$this->filePath = $this->storePath.$this->fileName;
}
/**
* Create form for uploading an Office document.
*/
public function createAnswersForm($form): void
{
if (!empty($this->exerciseList)) {
$this->exerciseId = reset($this->exerciseList);
}
$allowedFormats = [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/msword', // .doc
'application/vnd.ms-excel', // .xls
];
$form->addElement('file', 'office_file', get_lang('UploadOfficeDoc'));
$form->addRule('office_file', get_lang('ThisFieldIsRequired'), 'required');
$form->addRule('office_file', get_lang('InvalidFileFormat'), 'mimetype', $allowedFormats);
$allowedExtensions = implode(', ', ['.docx', '.xlsx', '.doc', '.xls']);
$form->addElement('static', 'file_hint', get_lang('AllowedFormats'), "<p>{$allowedExtensions}</p>");
if (!empty($this->extra)) {
$fileUrl = api_get_path(WEB_COURSE_PATH).$this->getStoredFilePath();
$form->addElement('static', 'current_office_file', get_lang('CurrentOfficeDoc'), "<a href='{$fileUrl}' target='_blank'>{$this->extra}</a>");
}
$form->addText('weighting', get_lang('Weighting'), ['class' => 'span1']);
global $text;
$form->addButtonSave($text, 'submitQuestion');
if (!empty($this->iid)) {
$form->setDefaults(['weighting' => float_format($this->weighting, 1)]);
} else {
if ($this->isContent == 1) {
$form->setDefaults(['weighting' => '10']);
}
}
}
/**
* Process the uploaded document and save it.
*/
public function processAnswersCreation($form, $exercise): void
{
if (!empty($_FILES['office_file']['name'])) {
$extension = pathinfo($_FILES['office_file']['name'], PATHINFO_EXTENSION);
$tempFilename = "office_".uniqid().".".$extension;
$tempPath = sys_get_temp_dir().'/'.$tempFilename;
if (!move_uploaded_file($_FILES['office_file']['tmp_name'], $tempPath)) {
return;
}
$this->weighting = $form->getSubmitValue('weighting');
$this->extra = "";
$this->save($exercise);
$this->exerciseId = $exercise->iid;
$uploadDir = $this->generateDirectory();
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$filename = "office_".$this->iid.".".$extension;
$filePath = $uploadDir.$filename;
if (!rename($tempPath, $filePath)) {
return;
}
$this->extra = $filename;
$this->save($exercise);
}
}
/**
* Get the stored file path dynamically.
*/
public function getStoredFilePath(): ?string
{
if (empty($this->extra)) {
return null;
}
return "{$this->course['path']}/exercises/onlyoffice/{$this->exerciseId}/{$this->iid}/{$this->extra}";
}
/**
* Get the absolute file path. Returns null if the file doesn't exist.
*/
public function getFileUrl(bool $loadFromDatabase = false): ?string
{
if ($loadFromDatabase) {
$em = Database::getManager();
$result = $em->getRepository('ChamiloCoreBundle:TrackEAttempt')->findOneBy([
'exeId' => $this->exeId,
'userId' => $this->userId,
'questionId' => $this->iid,
'sessionId' => $this->sessionId,
'cId' => $this->course['real_id'],
]);
if (!$result || empty($result->getFilename())) {
return null;
}
$this->fileName = $result->getFilename();
} else {
if (empty($this->extra)) {
return null;
}
$this->fileName = $this->extra;
}
$filePath = $this->getStoredFilePath();
if (is_file(api_get_path(SYS_COURSE_PATH).$filePath)) {
return $filePath;
}
return null;
}
/**
* Show the question in an exercise.
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$score['revised'] = $this->isQuestionWaitingReview($score);
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'">
<tr>
<th>'.get_lang("Answer").'</th>
</tr>';
return $header;
}
/**
* Generate the necessary directory for OnlyOffice documents.
*/
private function generateDirectory(): string
{
$exercisePath = api_get_path(SYS_COURSE_PATH).$this->course['path']."/exercises/onlyoffice/{$this->exerciseId}/{$this->iid}/";
if (!is_dir($exercisePath)) {
mkdir($exercisePath, 0775, true);
}
return rtrim($exercisePath, '/').'/';
}
/**
* Generate the file name for the OnlyOffice document.
*/
private function generateFileName(): string
{
return 'office_'.uniqid();
}
}

View File

@@ -49,7 +49,7 @@ if ((api_is_allowed_to_edit(null, true))) {
}
}
Display::display_header(get_lang('ImportAikenQuiz'), 'Exercises');
Display::display_header(get_lang('ImportAikenQuiz'), 'Exercise');
aiken_display_form();
generateAikenForm();
Display::display_footer();

View File

@@ -83,7 +83,7 @@ if ($exportXls) {
Export::arrayToXls($tableXls, $fileName);
exit;
}
Display::display_header($nameTools, get_lang('Exercise'));
Display::display_header($nameTools, 'Exercise');
$actions = '<a href="exercise_report.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
Display::return_icon(
'back.png',

View File

@@ -51,6 +51,7 @@ class Exercise
public $review_answers;
public $randomByCat;
public $text_when_finished;
public $text_when_finished_failure;
public $display_category_name;
public $pass_percentage;
public $edit_exercise_in_lp = false;
@@ -127,6 +128,7 @@ class Exercise
$this->review_answers = false;
$this->randomByCat = 0;
$this->text_when_finished = '';
$this->text_when_finished_failure = '';
$this->display_category_name = 0;
$this->pass_percentage = 0;
$this->modelType = 1;
@@ -204,6 +206,7 @@ class Exercise
$this->saveCorrectAnswers = $object->save_correct_answers;
$this->randomByCat = $object->random_by_category;
$this->text_when_finished = $object->text_when_finished;
$this->text_when_finished_failure = isset($object->text_when_finished_failure) ? $object->text_when_finished_failure : null;
$this->display_category_name = $object->display_category_name;
$this->pass_percentage = $object->pass_percentage;
$this->is_gradebook_locked = api_resource_is_locked_by_gradebook($id, LINK_EXERCISE);
@@ -456,6 +459,28 @@ class Exercise
$this->text_when_finished = $text;
}
/**
* Get the text to display when the user has failed the test.
*
* @return string html text : the text to display ay the end of the test
*/
public function getTextWhenFinishedFailure(): string
{
if (empty($this->text_when_finished_failure)) {
return '';
}
return $this->text_when_finished_failure;
}
/**
* Set the text to display when the user has succeeded in the test.
*/
public function setTextWhenFinishedFailure(string $text): void
{
$this->text_when_finished_failure = $text;
}
/**
* return 1 or 2 if randomByCat.
*
@@ -1599,6 +1624,7 @@ class Exercise
$review_answers = isset($this->review_answers) && $this->review_answers ? 1 : 0;
$randomByCat = (int) $this->randomByCat;
$text_when_finished = $this->text_when_finished;
$text_when_finished_failure = $this->text_when_finished_failure;
$display_category_name = (int) $this->display_category_name;
$pass_percentage = (int) $this->pass_percentage;
$session_id = $this->sessionId;
@@ -1654,6 +1680,10 @@ class Exercise
'hide_question_title' => $this->getHideQuestionTitle(),
];
if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
$paramsExtra['text_when_finished_failure'] = $text_when_finished_failure;
}
$allow = api_get_configuration_value('allow_quiz_show_previous_button_setting');
if ($allow === true) {
$paramsExtra['show_previous_button'] = $this->showPreviousButton();
@@ -1758,6 +1788,10 @@ class Exercise
'hide_question_title' => $this->getHideQuestionTitle(),
];
if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
$params['text_when_finished_failure'] = $text_when_finished_failure;
}
$allow = api_get_configuration_value('allow_exercise_categories');
if (true === $allow) {
if (!empty($this->getExerciseCategoryId())) {
@@ -2278,6 +2312,12 @@ class Exercise
null,
get_lang('HideCorrectAnsweredQuestions')
),
$form->createElement(
'checkbox',
'hide_comment',
null,
get_lang('HideComment')
),
];
$form->addGroup($group, null, get_lang('ResultsConfigurationPage'));
}
@@ -2505,6 +2545,16 @@ class Exercise
$editor_config
);
if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
$form->addHtmlEditor(
'text_when_finished_failure',
get_lang('TextAppearingAtTheEndOfTheTestWhenTheUserHasFailed'),
false,
false,
$editor_config
);
}
$allow = api_get_configuration_value('allow_notification_setting_per_exercise');
if ($allow === true) {
$settings = ExerciseLib::getNotificationSettings();
@@ -2572,6 +2622,7 @@ class Exercise
'notifications',
'remedialcourselist',
'advancedcourselist',
'subscribe_session_when_finished_failure',
], //exclude
false, // filter
false, // tag as select
@@ -2632,6 +2683,25 @@ class Exercise
}
}
if (true === api_get_configuration_value('exercise_subscribe_session_when_finished_failure')) {
$optionSessionWhenFailure = [];
if ($failureSession = ExerciseLib::getSessionWhenFinishedFailure($this->iid)) {
$defaults['subscribe_session_when_finished_failure'] = $failureSession->getId();
$optionSessionWhenFailure[$failureSession->getId()] = $failureSession->getName();
}
$form->addSelectAjax(
'extra_subscribe_session_when_finished_failure',
get_lang('SubscribeSessionWhenFinishedFailure'),
$optionSessionWhenFailure,
[
'url' => api_get_path(WEB_AJAX_PATH).'session.ajax.php?'
.http_build_query(['a' => 'search_session']),
]
);
}
$settings = api_get_configuration_value('exercise_finished_notification_settings');
if (!empty($settings)) {
$options = [];
@@ -2687,6 +2757,10 @@ class Exercise
$defaults['exercise_category_id'] = $this->getExerciseCategoryId();
$defaults['prevent_backwards'] = $this->getPreventBackwards();
if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
$defaults['text_when_finished_failure'] = $this->getTextWhenFinishedFailure();
}
if (!empty($this->start_time)) {
$defaults['activate_start_date_check'] = 1;
}
@@ -2715,6 +2789,11 @@ class Exercise
$defaults['results_disabled'] = 0;
$defaults['randomByCat'] = 0;
$defaults['text_when_finished'] = '';
if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
$defaults['text_when_finished_failure'] = '';
}
$defaults['start_time'] = date('Y-m-d 12:00:00');
$defaults['display_category_name'] = 1;
$defaults['end_time'] = date('Y-m-d 12:00:00', time() + 84600);
@@ -2886,6 +2965,11 @@ class Exercise
$this->updateSaveCorrectAnswers($form->getSubmitValue('save_correct_answers'));
$this->updateRandomByCat($form->getSubmitValue('randomByCat'));
$this->updateTextWhenFinished($form->getSubmitValue('text_when_finished'));
if (true === api_get_configuration_value('exercise_text_when_finished_failure')) {
$this->setTextWhenFinishedFailure($form->getSubmitValue('text_when_finished_failure'));
}
$this->updateDisplayCategoryName($form->getSubmitValue('display_category_name'));
$this->updateReviewAnswers($form->getSubmitValue('review_answers'));
$this->updatePassPercentage($form->getSubmitValue('pass_percentage'));
@@ -3872,15 +3956,16 @@ class Exercise
if ($answerType == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
$choiceTmp = $choice;
$choice = isset($choiceTmp['choice']) ? $choiceTmp['choice'] : '';
$choiceDegreeCertainty = isset($choiceTmp['choiceDegreeCertainty']) ? $choiceTmp['choiceDegreeCertainty'] : '';
$choice = $choiceTmp['choice'] ?? '';
$choiceDegreeCertainty = $choiceTmp['choiceDegreeCertainty'] ?? '';
}
if ($answerType == FREE_ANSWER ||
$answerType == ORAL_EXPRESSION ||
$answerType == CALCULATED_ANSWER ||
$answerType == ANNOTATION ||
$answerType == UPLOAD_ANSWER
$answerType == UPLOAD_ANSWER ||
$answerType == ANSWER_IN_OFFICE_DOC
) {
$nbrAnswers = 1;
}
@@ -3888,12 +3973,12 @@ class Exercise
$generatedFile = '';
if ($answerType == ORAL_EXPRESSION) {
$exe_info = Event::get_exercise_results_by_attempt($exeId);
$exe_info = isset($exe_info[$exeId]) ? $exe_info[$exeId] : null;
$exe_info = $exe_info[$exeId] ?? null;
$objQuestionTmp->initFile(
api_get_session_id(),
isset($exe_info['exe_user_id']) ? $exe_info['exe_user_id'] : api_get_user_id(),
isset($exe_info['exe_exo_id']) ? $exe_info['exe_exo_id'] : $this->iid,
isset($exe_info['exe_id']) ? $exe_info['exe_id'] : $exeId
$exe_info['exe_user_id'] ?? api_get_user_id(),
$exe_info['exe_exo_id'] ?? $this->iid,
$exe_info['exe_id'] ?? $exeId
);
// Probably this attempt came in an exercise all question by page
@@ -4003,7 +4088,12 @@ class Exercise
$matchingCorrectAnswers = [];
for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
$answer = $objAnswerTmp->selectAnswer($answerId);
$hideComment = (int) $this->getPageConfigurationAttribute('hide_comment');
if (1 === $hideComment) {
$answerComment = null;
} else {
$answerComment = $objAnswerTmp->selectComment($answerId);
}
$answerCorrect = $objAnswerTmp->isCorrect($answerId);
$answerWeighting = (float) $objAnswerTmp->selectWeighting($answerId);
$answerAutoId = $objAnswerTmp->selectId($answerId);
@@ -4068,7 +4158,7 @@ class Exercise
$userAnsweredQuestion = !empty($choice);
}
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
$studentChoice = $choice[$answerAutoId] ?? null;
if (isset($studentChoice)) {
$correctAnswerId[] = $answerAutoId;
if ($studentChoice == $answerCorrect) {
@@ -4110,8 +4200,8 @@ class Exercise
}
}
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
$studentChoiceDegree = isset($choiceDegreeCertainty[$answerAutoId]) ? $choiceDegreeCertainty[$answerAutoId] : null;
$studentChoice = $choice[$answerAutoId] ?? null;
$studentChoiceDegree = $choiceDegreeCertainty[$answerAutoId] ?? null;
// student score update
if (!empty($studentChoice)) {
@@ -4150,14 +4240,14 @@ class Exercise
$choice[$row['answer']] = 1;
}
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
$studentChoice = $choice[$answerAutoId] ?? null;
$real_answers[$answerId] = (bool) $studentChoice;
if ($studentChoice) {
$questionScore += $answerWeighting;
}
} else {
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
$studentChoice = $choice[$answerAutoId] ?? null;
$real_answers[$answerId] = (bool) $studentChoice;
if (isset($studentChoice)
@@ -4178,13 +4268,13 @@ class Exercise
while ($row = Database::fetch_array($resultans)) {
$choice[$row['answer']] = 1;
}
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
$studentChoice = $choice[$answerAutoId] ?? null;
$real_answers[$answerId] = (bool) $studentChoice;
if ($studentChoice) {
$questionScore += $answerWeighting;
}
} else {
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
$studentChoice = $choice[$answerAutoId] ?? null;
if (isset($studentChoice)) {
$questionScore += $answerWeighting;
}
@@ -4209,13 +4299,13 @@ class Exercise
$choice[$my_answer_id] = $option;
}
}
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
$studentChoice = $choice[$answerAutoId] ?? '';
$real_answers[$answerId] = false;
if ($answerCorrect == $studentChoice) {
$real_answers[$answerId] = true;
}
} else {
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : '';
$studentChoice = $choice[$answerAutoId] ?? '';
$real_answers[$answerId] = false;
if ($answerCorrect == $studentChoice) {
$real_answers[$answerId] = true;
@@ -4232,7 +4322,7 @@ class Exercise
$choice[$row['answer']] = 1;
}
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
$studentChoice = $choice[$answerAutoId] ?? null;
if (1 == $answerCorrect) {
$real_answers[$answerId] = false;
if ($studentChoice) {
@@ -4245,7 +4335,7 @@ class Exercise
}
}
} else {
$studentChoice = isset($choice[$answerAutoId]) ? $choice[$answerAutoId] : null;
$studentChoice = $choice[$answerAutoId] ?? null;
if (1 == $answerCorrect) {
$real_answers[$answerId] = false;
if ($studentChoice) {
@@ -4684,6 +4774,7 @@ class Exercise
break;
case UPLOAD_ANSWER:
case FREE_ANSWER:
case ANSWER_IN_OFFICE_DOC:
if ($from_database) {
$sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
WHERE
@@ -5345,6 +5436,18 @@ class Exercise
$questionScore,
$results_disabled
);
} elseif ($answerType == ANSWER_IN_OFFICE_DOC) {
$exe_info = Event::get_exercise_results_by_attempt($exeId);
$exe_info = $exe_info[$exeId] ?? null;
ExerciseShowFunctions::displayOnlyOfficeAnswer(
$feedback_type,
$exeId,
$exe_info['exe_user_id'] ?? api_get_user_id(),
$this->iid,
$questionId,
$questionScore,
true
);
} elseif ($answerType == ORAL_EXPRESSION) {
// to store the details of open questions in an array to be used in mail
/** @var OralExpression $objQuestionTmp */
@@ -5743,9 +5846,21 @@ class Exercise
$results_disabled
);
break;
case ANSWER_IN_OFFICE_DOC:
$exe_info = Event::get_exercise_results_by_attempt($exeId);
$exe_info = $exe_info[$exeId] ?? null;
ExerciseShowFunctions::displayOnlyOfficeAnswer(
$feedback_type,
$exeId,
$exe_info['exe_user_id'] ?? api_get_user_id(),
$this->iid,
$questionId,
$questionScore
);
break;
case ORAL_EXPRESSION:
echo '<tr>
<td valign="top">'.
<td>'.
ExerciseShowFunctions::display_oral_expression_answer(
$feedback_type,
$choice,
@@ -6385,6 +6500,24 @@ class Exercise
false,
$questionDuration
);
} elseif ($answerType == ANSWER_IN_OFFICE_DOC) {
$answer = $choice;
$exerciseId = $this->iid;
$questionId = $quesId;
$originalFilePath = $objQuestionTmp->getFileUrl();
$originalExtension = !empty($originalFilePath) ? pathinfo($originalFilePath, PATHINFO_EXTENSION) : 'docx';
$fileName = "response_{$exeId}.{$originalExtension}";
Event::saveQuestionAttempt(
$questionScore,
$answer,
$questionId,
$exeId,
0,
$exerciseId,
false,
$questionDuration,
$fileName
);
} elseif ($answerType == ORAL_EXPRESSION) {
$answer = $choice;
$absFilePath = $objQuestionTmp->getAbsoluteFilePath();
@@ -8816,6 +8949,7 @@ class Exercise
'hide_total_score' => $values['hide_total_score'] ?? '',
'hide_category_table' => $values['hide_category_table'] ?? '',
'hide_correct_answered_questions' => $values['hide_correct_answered_questions'] ?? '',
'hide_comment' => $values['hide_comment'] ?? '',
];
$type = Type::getType('array');
$platform = Database::getManager()->getConnection()->getDatabasePlatform();
@@ -11759,6 +11893,34 @@ class Exercise
return $result;
}
/**
* Return the text to display, based on the score and the max score.
*
* @param int|float $score
* @param int|float $maxScore
*/
public function getFinishText($score, $maxScore): string
{
if (true !== api_get_configuration_value('exercise_text_when_finished_failure')) {
return $this->getTextWhenFinished();
}
$passPercentage = $this->selectPassPercentage();
if (!empty($passPercentage)) {
$percentage = float_format(
($score / (0 != $maxScore ? $maxScore : 1)) * 100,
1
);
if ($percentage < $passPercentage) {
return $this->getTextWhenFinishedFailure();
}
}
return $this->getTextWhenFinished();
}
/**
* Get number of questions in exercise by user attempt.
*

View File

@@ -600,7 +600,7 @@ if ($is_allowedToEdit) {
if (!in_array($origin, ['learnpath', 'mobileapp'])) {
//so we are not in learnpath tool
Display::display_header($nameTools, get_lang('Exercise'));
Display::display_header($nameTools, 'Exercise');
if (isset($_GET['message']) && in_array($_GET['message'], ['ExerciseEdited'])) {
echo Display::return_message(get_lang('ExerciseEdited'), 'confirmation');
}

View File

@@ -181,7 +181,7 @@ if ($form->validate()) {
'name' => $objExercise->selectTitle(true),
];
Display::display_header($nameTools, get_lang('Exercise'));
Display::display_header($nameTools, 'Exercise');
echo '<div class="actions">';
if ($objExercise->iid != 0) {

View File

@@ -39,7 +39,7 @@ $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
$TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
$TBL_EXERCISES_QUESTION = Database::get_course_table(TABLE_QUIZ_QUESTION);
$TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
Display::display_header($nameTools, get_lang('Exercise'));
Display::display_header($nameTools, 'Exercise');
if (isset($_GET['message'])) {
if (in_array($_GET['message'], ['ExerciseEdited'])) {

View File

@@ -96,7 +96,7 @@ $interbreadcrumb[] = ['url' => 'exercise.php?'.api_get_cidreq(), 'name' => get_l
$hideHeaderAndFooter = in_array($origin, ['learnpath', 'embeddable', 'iframe']);
if (!$hideHeaderAndFooter) {
Display::display_header($nameTools, get_lang('Exercise'));
Display::display_header($nameTools, 'Exercise');
} else {
Display::display_reduced_header();
}

View File

@@ -82,7 +82,7 @@ $interbreadcrumb[] = ['url' => 'exercise.php?'.api_get_cidreq(), 'name' => get_l
$hideHeaderAndFooter = in_array($origin, ['learnpath', 'embeddable', 'iframe']);
if (!$hideHeaderAndFooter) {
Display::display_header($nameTools, get_lang('Exercise'));
Display::display_header($nameTools, 'Exercise');
} else {
Display::display_reduced_header();
}

View File

@@ -60,7 +60,7 @@ $locked = api_resource_is_locked_by_gradebook($exercise_id, LINK_EXERCISE);
$sessionId = api_get_session_id();
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : null;
if ('export_all_exercises_results' !== $action) {
if ('export_all_exercises_results' !== $action && !(isset($_GET['delete']) && $_GET['delete'] === 'delete')) {
if (empty($exercise_id)) {
api_not_allowed(true);
}
@@ -211,7 +211,7 @@ if (isset($_REQUEST['comments']) &&
// From the database.
$marksFromDatabase = $questionListData[$questionId]['marks'];
if (in_array($question->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
if (in_array($question->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
// From the form.
$params['marks'] = $marks;
if ($marksFromDatabase != $marks) {
@@ -312,7 +312,13 @@ if (isset($_REQUEST['comments']) &&
if (isset($_POST['send_notification'])) {
//@todo move this somewhere else
$subject = get_lang('ExamSheetVCC');
$message = isset($_POST['notification_content']) ? $_POST['notification_content'] : '';
$message = $_POST['notification_content'] ?? '';
$feedbackComments = ExerciseLib::getFeedbackComments($id);
$message .= "<div style='padding:10px; border:1px solid #ddd; border-radius:5px;'>";
$message .= $feedbackComments;
$message .= "</div>";
MessageManager::send_message_simple(
$student_id,
$subject,
@@ -500,6 +506,8 @@ if ($is_allowedToEdit && $origin !== 'learnpath') {
'comparative_group_report.php?'.api_get_cidreq().'&id='.$exercise_id,
['class' => 'btn btn-default']
);
$actions .= ExerciseFocusedPlugin::create()->getLinkReporting($exercise_id);
}
} else {
$actions .= '<a href="exercise.php">'.
@@ -521,6 +529,10 @@ if (($is_allowedToEdit || $is_tutor || api_is_coach()) &&
if (!empty($exe_id)) {
ExerciseLib::deleteExerciseAttempt($exe_id);
if (empty($exercise_id)) {
header('Location: pending.php');
exit;
}
header('Location: exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exercise_id);
exit;
}

View File

@@ -379,13 +379,6 @@ if (!empty($track_exercise_info['data_tracking'])) {
$questionList = $question_list_from_database;
}
// Display the text when finished message if we are on a LP #4227
$end_of_message = $objExercise->getTextWhenFinished();
if (!empty($end_of_message) && ($origin === 'learnpath')) {
echo Display::return_message($end_of_message, 'normal', false);
echo "<div class='clear'>&nbsp;</div>";
}
// for each question
$total_weighting = 0;
foreach ($questionList as $questionId) {
@@ -416,7 +409,7 @@ if ($allowRecordAudio && $allowTeacherCommentAudio) {
}
foreach ($questionList as $questionId) {
$choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : '';
$choice = $exerciseResult[$questionId] ?? '';
// destruction of the Question object
unset($objQuestionTmp);
$questionWeighting = 0;
@@ -451,6 +444,7 @@ foreach ($questionList as $questionId) {
case GLOBAL_MULTIPLE_ANSWER:
case FREE_ANSWER:
case UPLOAD_ANSWER:
case ANSWER_IN_OFFICE_DOC:
case ORAL_EXPRESSION:
case MATCHING:
case MATCHING_COMBINATION:
@@ -619,7 +613,7 @@ foreach ($questionList as $questionId) {
if ($isFeedbackAllowed && $action !== 'export') {
$name = 'fckdiv'.$questionId;
$marksname = 'marksName'.$questionId;
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
$url_name = get_lang('EditCommentsAndMarks');
} else {
$url_name = get_lang('AddComments');
@@ -696,7 +690,7 @@ foreach ($questionList as $questionId) {
}
if ($is_allowedToEdit && $isFeedbackAllowed && $action !== 'export') {
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
$marksname = 'marksName'.$questionId;
$arrmarks[] = $questionId;
@@ -853,7 +847,7 @@ foreach ($questionList as $questionId) {
}
}
if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
$scoreToReview = [
'score' => $my_total_score,
'comments' => isset($comnt) ? $comnt : null,
@@ -884,6 +878,13 @@ foreach ($questionList as $questionId) {
$exercise_content .= Display::panel($question_content);
} // end of large foreach on questions
// Display the text when finished message if we are on a LP #4227
$end_of_message = $objExercise->getFinishText($totalScore, $totalWeighting);
if (!empty($end_of_message) && ($origin === 'learnpath')) {
echo Display::return_message($end_of_message, 'normal', false);
echo "<div class='clear'>&nbsp;</div>";
}
$totalScoreText = '';
if ($answerType != MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
$pluginEvaluation = QuestionOptionsEvaluationPlugin::create();

View File

@@ -1030,7 +1030,7 @@ if (false !== $quizKeepAlivePingInterval) {
if (!in_array($origin, ['learnpath', 'embeddable', 'mobileapp', 'iframe'])) {
//so we are not in learnpath tool
SessionManager::addFlashSessionReadOnly();
Display::display_header(null, 'Exercises');
Display::display_header(null, 'Exercise');
} else {
Display::display_reduced_header();
echo '<div style="height:10px">&nbsp;</div>';

View File

@@ -44,8 +44,8 @@ function aiken_display_form()
}
/**
* Generates aiken format using AI api.
* Requires plugin ai_helper to connect to the api.
* Generates Aiken format using AI APIs (supports multiple providers).
* Requires plugin ai_helper to connect to the API.
*/
function generateAikenForm()
{
@@ -53,6 +53,12 @@ function generateAikenForm()
return false;
}
$plugin = AiHelperPlugin::create();
$availableApis = $plugin->getApiList();
$configuredApi = $plugin->get('api_name');
$hasSingleApi = count($availableApis) === 1 || isset($availableApis[$configuredApi]);
$form = new FormValidator(
'aiken_generate',
'post',
@@ -60,20 +66,31 @@ function generateAikenForm()
null
);
$form->addElement('header', get_lang('AIQuestionsGenerator'));
$form->addElement('text', 'quiz_name', [get_lang('QuestionsTopic'), get_lang('QuestionsTopicHelp')]);
if ($hasSingleApi) {
$apiName = $availableApis[$configuredApi] ?? $configuredApi;
$form->addHtml('<div style="margin-bottom: 10px; font-size: 14px; color: #555;">'
.sprintf(get_lang('UsingAIProviderX'), '<strong>'.htmlspecialchars($apiName).'</strong>').'</div>');
}
$form->addElement('text', 'quiz_name', get_lang('QuestionsTopic'));
$form->addRule('quiz_name', get_lang('ThisFieldIsRequired'), 'required');
$form->addElement('number', 'nro_questions', [get_lang('NumberOfQuestions'), get_lang('AIQuestionsGeneratorNumberHelper')]);
$form->addElement('number', 'nro_questions', get_lang('NumberOfQuestions'));
$form->addRule('nro_questions', get_lang('ThisFieldIsRequired'), 'required');
$options = [
'multiple_choice' => get_lang('MultipleAnswer'),
];
$form->addElement('select', 'question_type', get_lang('QuestionType'), $options);
if (!$hasSingleApi) {
$form->addElement(
'select',
'question_type',
get_lang('QuestionType'),
$options
'ai_provider',
get_lang('AIProvider'),
array_combine(array_keys($availableApis), array_keys($availableApis))
);
}
$generateUrl = api_get_path(WEB_PLUGIN_PATH).'ai_helper/tool/answers.php';
$language = api_get_interface_language();
@@ -87,10 +104,9 @@ function generateAikenForm()
var btnGenerate = $(this);
var quizName = $("[name=\'quiz_name\']").val();
var nroQ = parseInt($("[name=\'nro_questions\']").val());
var qType = $("[name=\'question_type\']").val();
var valid = (quizName != \'\' && nroQ > 0);
var qWeight = 1;
var qType = $("[name=\'question_type\']").val();'
.(!$hasSingleApi ? 'var provider = $("[name=\'ai_provider\']").val();' : 'var provider = "'.$configuredApi.'";').
'var valid = (quizName != \'\' && nroQ > 0);
if (valid) {
btnGenerate.attr("disabled", true);
btnGenerate.text("'.get_lang('PleaseWaitThisCouldTakeAWhile').'");
@@ -100,7 +116,8 @@ function generateAikenForm()
"quiz_name": quizName,
"nro_questions": nroQ,
"question_type": qType,
"language": "'.$language.'"
"language": "'.$language.'",
"ai_provider": provider
}).done(function (data) {
btnGenerate.attr("disabled", false);
btnGenerate.text("'.get_lang('Generate').'");

View File

@@ -893,7 +893,7 @@ class FillBlanks extends Question
// should always be
$i++;
}
$listAnswerResults['student_answer'][] = $listAnswerResults['words'][$i];
$listAnswerResults['student_answer'][] = Security::remove_XSS($listAnswerResults['words'][$i]);
if ($i + 1 < count($listAnswerResults['words'])) {
// should always be
$i++;
@@ -1201,7 +1201,8 @@ class FillBlanks extends Question
$answer,
$feedbackType,
$resultsDisabled = false,
$showTotalScoreAndUserChoices = false
$showTotalScoreAndUserChoices = false,
$exercise
) {
$result = '';
$listStudentAnswerInfo = self::getAnswerInfo($answer, true);
@@ -1215,7 +1216,8 @@ class FillBlanks extends Question
$listStudentAnswerInfo['words'][$i],
$feedbackType,
$resultsDisabled,
$showTotalScoreAndUserChoices
$showTotalScoreAndUserChoices,
$exercise
);
} else {
$listStudentAnswerInfo['student_answer'][$i] = self::getHtmlWrongAnswer(
@@ -1223,7 +1225,8 @@ class FillBlanks extends Question
$listStudentAnswerInfo['words'][$i],
$feedbackType,
$resultsDisabled,
$showTotalScoreAndUserChoices
$showTotalScoreAndUserChoices,
$exercise
);
}
}
@@ -1235,13 +1238,13 @@ class FillBlanks extends Question
continue;
}
}
$result .= isset($listStudentAnswerInfo['common_words'][$i]) ? $listStudentAnswerInfo['common_words'][$i] : '';
$studentLabel = isset($listStudentAnswerInfo['student_answer'][$i]) ? $listStudentAnswerInfo['student_answer'][$i] : '';
$result .= $listStudentAnswerInfo['common_words'][$i] ?? '';
$studentLabel = $listStudentAnswerInfo['student_answer'][$i] ?? '';
$result .= $studentLabel;
}
// the last common word (should be </p>)
$result .= isset($listStudentAnswerInfo['common_words'][$i]) ? $listStudentAnswerInfo['common_words'][$i] : '';
$result .= $listStudentAnswerInfo['common_words'][$i] ?? '';
return $result;
}
@@ -1264,10 +1267,14 @@ class FillBlanks extends Question
$right,
$feedbackType,
$resultsDisabled = false,
$showTotalScoreAndUserChoices = false
$showTotalScoreAndUserChoices = false,
$exercise
) {
$hideExpectedAnswer = false;
$hideUserSelection = false;
if (!$exercise->showExpectedChoiceColumn()) {
$hideExpectedAnswer = true;
}
switch ($resultsDisabled) {
case RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING:
case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER:
@@ -1356,7 +1363,8 @@ class FillBlanks extends Question
$correct,
$feedbackType,
$resultsDisabled = false,
$showTotalScoreAndUserChoices = false
$showTotalScoreAndUserChoices = false,
$exercise
) {
return self::getHtmlAnswer(
$answer,
@@ -1364,7 +1372,8 @@ class FillBlanks extends Question
true,
$feedbackType,
$resultsDisabled,
$showTotalScoreAndUserChoices
$showTotalScoreAndUserChoices,
$exercise
);
}
@@ -1384,7 +1393,8 @@ class FillBlanks extends Question
$correct,
$feedbackType,
$resultsDisabled = false,
$showTotalScoreAndUserChoices = false
$showTotalScoreAndUserChoices = false,
$exercise
) {
return self::getHtmlAnswer(
$answer,
@@ -1392,7 +1402,8 @@ class FillBlanks extends Question
false,
$feedbackType,
$resultsDisabled,
$showTotalScoreAndUserChoices
$showTotalScoreAndUserChoices,
$exercise
);
}

View File

@@ -129,9 +129,10 @@ if ((api_is_allowed_to_edit(null, true)) && (($finish == 0) || ($finish == 2)))
$unzip = 1;
}
$filename = api_replace_dangerous_char(trim($_FILES['userFile']['name']));
if ($finish == 0) {
// Generate new test folder if on first step of file upload.
$filename = api_replace_dangerous_char(trim($_FILES['userFile']['name']));
$fld = GenerateHpFolder($document_sys_path.$uploadPath.'/');
@mkdir($document_sys_path.$uploadPath.'/'.$fld, api_get_permissions_for_new_directories());
$doc_id = add_document($_course, '/HotPotatoes_files/'.$fld, 'folder', 0, $fld);
@@ -142,9 +143,6 @@ if ((api_is_allowed_to_edit(null, true)) && (($finish == 0) || ($finish == 2)))
'FolderCreated',
api_get_user_id()
);
} else {
// It is not the first step... get the filename directly from the system params.
$filename = $_FILES['userFile']['name'];
}
$allow_output_on_success = false;
@@ -207,21 +205,21 @@ if ((api_is_allowed_to_edit(null, true)) && (($finish == 0) || ($finish == 2)))
$select = "SELECT iid FROM $dbTable
WHERE c_id = $course_id
$sessionAnd
AND path = '$path'";
AND path = '".Database::escape_string($path)."'";
$query = Database::query($select);
if (Database::num_rows($query)) {
$row = Database::fetch_array($query);
$hotPotatoesDocumentId = $row['iid'];
// Update the record with the 'comment' (HP title)
$query = "UPDATE $dbTable
SET comment = '".Database::escape_string($title)."'
WHERE iid = $hotPotatoesDocumentId";
Database::query($query);
Database::update(
$dbTable,
['comment' => $title],
['iid = ?' => $row['iid']]
);
// Mark the addition of the HP quiz in the item_property table
api_item_property_update(
$_course,
TOOL_QUIZ,
$hotPotatoesDocumentId,
$row['iid'],
'QuizAdded',
api_get_user_id()
);
@@ -238,7 +236,7 @@ if ((api_is_allowed_to_edit(null, true)) && (($finish == 0) || ($finish == 2)))
exit;
}
Display::display_header($nameTools, get_lang('Exercise'));
Display::display_header($nameTools, 'Exercise');
echo '<div class="actions">';
echo '<a href="exercise.php?show=test">'.

View File

@@ -184,11 +184,59 @@ class MultipleAnswer extends Question
$form->setConstants(['nb_answers' => $nb_answers]);
}
/**
* Validate question answers before saving.
*
* @param object $form The form object.
*
* @return array|bool True if valid, or an array with errors if invalid.
*/
public function validateAnswers($form)
{
$nb_answers = $form->getSubmitValue('nb_answers');
$hasCorrectAnswer = false;
$hasValidWeighting = false;
$errors = [];
$error_fields = [];
for ($i = 1; $i <= $nb_answers; $i++) {
$isCorrect = $form->getSubmitValue("correct[$i]");
$weighting = trim($form->getSubmitValue("weighting[$i]") ?? '');
if ($isCorrect) {
$hasCorrectAnswer = true;
if (is_numeric($weighting) && floatval($weighting) > 0) {
$hasValidWeighting = true;
}
}
}
if (!$hasCorrectAnswer) {
$errors[] = get_lang('AtLeastOneCorrectAnswerRequired');
$error_fields[] = "correct";
}
if ($hasCorrectAnswer && !$hasValidWeighting) {
// Only show if at least one correct answer exists but its weighting is not valid
$errors[] = get_lang('AtLeastOneCorrectAnswerMustHaveAPositiveScore');
}
return empty($errors) ? true : ['errors' => $errors, 'error_fields' => $error_fields];
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$validationResult = $this->validateAnswers($form);
if ($validationResult !== true) {
Display::addFlash(Display::return_message(implode("<br>", $validationResult['errors']), 'error'));
return;
}
$questionWeighting = 0;
$objAnswer = new Answer($this->iid);
$nb_answers = $form->getSubmitValue('nb_answers');

View File

@@ -95,7 +95,7 @@ if (api_is_allowed_to_edit(null, true)) {
}
}
Display::display_header(get_lang('ImportQtiQuiz'), 'Exercises');
Display::display_header(get_lang('ImportQtiQuiz'), 'Exercise');
echo $message;

View File

@@ -75,6 +75,7 @@ abstract class Question
UPLOAD_ANSWER => ['UploadAnswer.php', 'UploadAnswer'],
MULTIPLE_ANSWER_DROPDOWN => ['MultipleAnswerDropdown.php', 'MultipleAnswerDropdown'],
MULTIPLE_ANSWER_DROPDOWN_COMBINATION => ['MultipleAnswerDropdownCombination.php', 'MultipleAnswerDropdownCombination'],
ANSWER_IN_OFFICE_DOC => ['AnswerInOfficeDoc.php', 'AnswerInOfficeDoc'],
];
/**
@@ -110,6 +111,7 @@ abstract class Question
FILL_IN_BLANKS,
FILL_IN_BLANKS_COMBINATION,
FREE_ANSWER,
ANSWER_IN_OFFICE_DOC,
ORAL_EXPRESSION,
CALCULATED_ANSWER,
ANNOTATION,
@@ -1663,6 +1665,9 @@ abstract class Question
self::$questionTypes[HOT_SPOT_DELINEATION] = null;
unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
}
if ('true' !== OnlyofficePlugin::create()->get('enable_onlyoffice_plugin')) {
unset(self::$questionTypes[ANSWER_IN_OFFICE_DOC]);
}
return self::$questionTypes;
}
@@ -2248,8 +2253,9 @@ abstract class Question
case FREE_ANSWER:
case UPLOAD_ANSWER:
case ORAL_EXPRESSION:
case ANSWER_IN_OFFICE_DOC:
case ANNOTATION:
$score['revised'] = isset($score['revised']) ? $score['revised'] : false;
$score['revised'] = $score['revised'] ?? false;
if ($score['revised'] == true) {
$scoreLabel = get_lang('Revised');
$class = '';
@@ -2298,8 +2304,8 @@ abstract class Question
}
$scoreCurrent = [
'used' => isset($score['score']) ? $score['score'] : '',
'missing' => isset($score['weight']) ? $score['weight'] : '',
'used' => $score['score'] ?? '',
'missing' => $score['weight'] ?? '',
];
// Check whether we need to hide the question ID

View File

@@ -51,29 +51,30 @@ if (is_object($objQuestion)) {
}
// FORM VALIDATION
if (isset($_POST['submitQuestion']) && $form->validate()) {
// Question
if (isset($_POST['submitQuestion'])) {
$validationResult = true;
if (method_exists($objQuestion, 'validateAnswers')) {
$validationResult = $objQuestion->validateAnswers($form);
}
if (is_array($validationResult) && !empty($validationResult['errors'])) {
echo Display::return_message(implode("<br>", $validationResult['errors']), 'error', false);
} elseif ($form->validate()) {
$objQuestion->processCreation($form, $objExercise);
$objQuestion->processAnswersCreation($form, $objExercise);
// TODO: maybe here is the better place to index this tool, including answers text
// redirect
if (in_array($objQuestion->type, [HOT_SPOT, HOT_SPOT_COMBINATION, HOT_SPOT_DELINEATION])) {
echo '<script type="text/javascript">window.location.href="admin.php?exerciseId='.$exerciseId.'&page='.$page.'&hotspotadmin='.$objQuestion->iid.'&'.api_get_cidreq(
).'"</script>';
echo '<script type="text/javascript">window.location.href="admin.php?exerciseId='.$exerciseId.'&page='.$page.'&hotspotadmin='.$objQuestion->iid.'&'.api_get_cidreq().'";</script>';
} elseif (in_array($objQuestion->type, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION])) {
$url = 'admin.php?'
.api_get_cidreq().'&'
.http_build_query(['exerciseId' => $exerciseId, 'page' => $page, 'mad_admin' => $objQuestion->iid]);
echo '<script type="text/javascript">window.location.href="'.$url.'"</script>';
$url = 'admin.php?'.api_get_cidreq().'&'.http_build_query(['exerciseId' => $exerciseId, 'page' => $page, 'mad_admin' => $objQuestion->iid]);
echo '<script type="text/javascript">window.location.href="'.$url.'";</script>';
} else {
if (isset($_GET['editQuestion'])) {
if (empty($exerciseId)) {
Display::addFlash(Display::return_message(get_lang('ItemUpdated')));
$url = 'admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'&editQuestion='.$objQuestion->iid;
echo '<script type="text/javascript">window.location.href="'.$url.'"</script>';
echo '<script type="text/javascript">window.location.href="'.$url.'";</script>';
exit;
}
echo '<script type="text/javascript">window.location.href="admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'&page='.$page.'&message=ItemUpdated"</script>';
echo '<script type="text/javascript">window.location.href="admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'&page='.$page.'&message=ItemUpdated";</script>';
} else {
// New question
$page = 1;
@@ -81,10 +82,13 @@ if (is_object($objQuestion)) {
if (!empty($length)) {
$page = round($objExercise->getQuestionCount() / $length);
}
echo '<script type="text/javascript">window.location.href="admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'&page='.$page.'&message=ItemAdded"</script>';
echo '<script type="text/javascript">window.location.href="admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'&page='.$page.'&message=ItemAdded";</script>';
}
}
} else {
exit();
}
}
if (isset($questionName)) {
echo '<h3>'.$questionName.'</h3>';
}
@@ -94,7 +98,6 @@ if (is_object($objQuestion)) {
if (!empty($msgErr)) {
echo Display::return_message($msgErr);
}
// display the form
$form->display();
}
}

View File

@@ -335,11 +335,66 @@ class UniqueAnswer extends Question
);
}
/**
* Validate question answers before saving.
*
* @return bool|string True if valid, error message if invalid.
*/
public function validateAnswers($form)
{
$correct = $form->getSubmitValue('correct');
$nb_answers = $form->getSubmitValue('nb_answers');
$hasCorrectAnswer = false;
$hasPositiveScore = false;
$errors = [];
$error_fields = [];
for ($i = 1; $i <= $nb_answers; $i++) {
$answer = trim($form->getSubmitValue("answer[$i]") ?? '');
$weighting = trim($form->getSubmitValue("weighting[$i]") ?? '');
$isCorrect = ($correct == $i);
if (empty($answer)) {
$errors[] = sprintf(get_lang('NoAnswerCanBeEmpty'), $i);
$error_fields[] = "answer[$i]";
}
if ($weighting === '' || !is_numeric($weighting)) {
$errors[] = sprintf(get_lang('ScoreMustBeNumeric'), $i);
$error_fields[] = "weighting[$i]";
}
if ($isCorrect) {
$hasCorrectAnswer = true;
if (floatval($weighting) > 0) {
$hasPositiveScore = true;
}
}
}
if (!$hasCorrectAnswer) {
$errors[] = get_lang('ACorrectAnswerIsRequired');
$error_fields[] = "correct";
}
if (!$hasPositiveScore) {
$errors[] = get_lang('TheCorrectAnswerMustHaveAPositiveScore');
}
return empty($errors) ? true : ['errors' => $errors, 'error_fields' => $error_fields];
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$validationResult = $this->validateAnswers($form);
if ($validationResult !== true) {
Display::addFlash(Display::return_message(implode("<br>", $validationResult['errors']), 'error'));
return;
}
$questionWeighting = $nbrGoodAnswers = 0;
$correct = $form->getSubmitValue('correct');
$objAnswer = new Answer($this->iid);

View File

@@ -41,7 +41,7 @@ $interbreadcrumb[] = [
'name' => get_lang('Exercises'),
];
Display::display_header(get_lang('ImportExcelQuiz'), 'Exercises');
Display::display_header(get_lang('ImportExcelQuiz'), 'Exercise');
echo '<div class="actions">';
echo lp_upload_quiz_actions();

View File

@@ -90,12 +90,12 @@ switch ($action) {
$form->addHtmlEditor(
'name',
get_lang('TermName'),
false,
true,
false,
['ToolbarSet' => 'TitleAsHtml']
);
} else {
$form->addElement('text', 'name', get_lang('TermName'), ['id' => 'glossary_title']);
$form->addText('name', get_lang('TermName'), true, ['id' => 'glossary_title']);
}
$form->addHtmlEditor(
@@ -107,7 +107,6 @@ switch ($action) {
);
$form->addButtonCreate(get_lang('TermAddButton'), 'SubmitGlossary');
// setting the rules
$form->addRule('name', get_lang('ThisFieldIsRequired'), 'required');
// The validation or display
if ($form->validate()) {
$check = Security::check_token('post');
@@ -154,12 +153,12 @@ switch ($action) {
$form->addHtmlEditor(
'name',
get_lang('TermName'),
false,
true,
false,
['ToolbarSet' => 'TitleAsHtml']
);
} else {
$form->addElement('text', 'name', get_lang('TermName'), ['id' => 'glossary_title']);
$form->addText('name', get_lang('TermName'), true, ['id' => 'glossary_title']);
}
$form->addHtmlEditor(
@@ -192,9 +191,6 @@ switch ($action) {
$form->addButtonUpdate(get_lang('TermUpdateButton'), 'SubmitGlossary');
$form->setDefaults($glossary_data);
// setting the rules
$form->addRule('name', get_lang('ThisFieldIsRequired'), 'required');
// The validation or display
if ($form->validate()) {
$check = Security::check_token('post');

View File

@@ -44,7 +44,7 @@ switch ($action) {
continue;
}
$exerciseId = $data['id'];
$exerciseId = $data['iid'];
$result = $exercise->read($exerciseId);
if ($result) {
$exercise->generateStats($exerciseId, api_get_course_info(), api_get_session_id());

View File

@@ -20,35 +20,39 @@ class DisplayGradebook
if (api_is_allowed_to_edit(null, true)) {
$header = '<div class="actions">';
if ('statistics' !== $page) {
$header .= '<a href="'.Category::getUrl().'selectcat='.$selectcat.'">'.
Display::return_icon('back.png', get_lang('FolderView'), '', ICON_SIZE_MEDIUM)
$header .= '<a href="'.Category::getUrl().'selectcat='.$selectcat.'">'
.Display::return_icon('back.png', get_lang('FolderView'), [], ICON_SIZE_MEDIUM)
.'</a>';
if (($evalobj->get_course_code() != null) && !$evalobj->has_results()) {
$header .= '<a href="gradebook_add_result.php?'.api_get_cidreq().'&selectcat='.$selectcat.'&selecteval='.$evalobj->get_id().'">
'.Display::return_icon('evaluation_rate.png', get_lang('AddResult'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="gradebook_add_result.php?'.api_get_cidreq().'&selectcat='.$selectcat.'&selecteval='.$evalobj->get_id().'">'
.Display::return_icon('evaluation_rate.png', get_lang('AddResult'), '', ICON_SIZE_MEDIUM)
.'</a>';
}
if (api_is_platform_admin() || $evalobj->is_locked() == false) {
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&selecteval='.$evalobj->get_id().'&import=">'.
Display::return_icon('import_evaluation.png', get_lang('ImportResult'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&selecteval='.$evalobj->get_id().'&import=">'
.Display::return_icon('import_evaluation.png', get_lang('ImportResult'), [], ICON_SIZE_MEDIUM)
.'</a>';
}
if ($evalobj->has_results()) {
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&selecteval='.$evalobj->get_id().'&export=">'.
Display::return_icon('export_evaluation.png', get_lang('ExportResult'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&selecteval='.$evalobj->get_id().'&export=">'
.Display::return_icon('export_evaluation.png', get_lang('ExportResult'), [], ICON_SIZE_MEDIUM)
.'</a>';
if (api_is_platform_admin() || $evalobj->is_locked() == false) {
$header .= '<a href="gradebook_edit_result.php?'.api_get_cidreq().'&selecteval='.$evalobj->get_id().'">'.
Display::return_icon('edit.png', get_lang('EditResult'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&selecteval='.$evalobj->get_id().'&deleteall=" onclick="return confirmationall();">'.
Display::return_icon('delete.png', get_lang('DeleteResult'), '', ICON_SIZE_MEDIUM).'</a>';
if (api_is_platform_admin() || !$evalobj->is_locked()) {
$header .= '<a href="gradebook_edit_result.php?'.api_get_cidreq().'&selecteval='.$evalobj->get_id().'">'
.Display::return_icon('edit.png', get_lang('EditResult'), [], ICON_SIZE_MEDIUM)
.'</a>';
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&selecteval='.$evalobj->get_id().'&deleteall=" onclick="return confirmationall();">'
.Display::return_icon('delete.png', get_lang('DeleteResult'), [], ICON_SIZE_MEDIUM).'</a>';
}
}
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&print=&selecteval='.$evalobj->get_id().'" target="_blank">'.
Display::return_icon('printer.png', get_lang('Print'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&print=&selecteval='.$evalobj->get_id().'" target="_blank">'
.Display::return_icon('printer.png', get_lang('Print'), [], ICON_SIZE_MEDIUM).'</a>';
} else {
$header .= '<a href="gradebook_view_result.php?'.api_get_cidreq().'&selecteval='.Security::remove_XSS($_GET['selecteval']).'"> '.
Display::return_icon('back.png', get_lang('FolderView'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="gradebook_view_result.php?'.api_get_cidreq().'&selecteval='.Security::remove_XSS($_GET['selecteval']).'"> '
.Display::return_icon('back.png', get_lang('FolderView'), [], ICON_SIZE_MEDIUM).'</a>';
}
$header .= '</div>';
}
@@ -112,17 +116,17 @@ class DisplayGradebook
if ($page != 'statistics') {
if (api_is_allowed_to_edit(null, true)) {
$evalinfo .= '<br /><a href="gradebook_statistics.php?'.api_get_cidreq().'&selecteval='.Security::remove_XSS($_GET['selecteval']).'"> '.
Display::return_icon(
$evalinfo .= '<br /><a href="gradebook_statistics.php?'.api_get_cidreq().'&selecteval='.Security::remove_XSS($_GET['selecteval']).'"> '
.Display::return_icon(
'statistics.png',
get_lang('ViewStatistics'),
'',
[],
ICON_SIZE_MEDIUM
).'</a>';
}
}
$evalinfo .= '</td><td>'.
Display::return_icon(
$evalinfo .= '</td><td>'
.Display::return_icon(
'tutorial.gif',
'',
['style' => 'float:right; position:relative;']
@@ -149,12 +153,12 @@ class DisplayGradebook
$select_cat = $catobj->get_parent_id();
$url = 'gradebook_flatview.php';
}
$header .= '<a href="'.$url.'?'.api_get_cidreq().'&selectcat='.$select_cat.'">'.
Display::return_icon('back.png', get_lang('FolderView'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="'.$url.'?'.api_get_cidreq().'&selectcat='.$select_cat.'">'
.Display::return_icon('back.png', get_lang('FolderView'), [], ICON_SIZE_MEDIUM).'</a>';
$pageNum = isset($_GET['flatviewlist_page_nr']) ? (int) $_GET['flatviewlist_page_nr'] : null;
$perPage = isset($_GET['flatviewlist_per_page']) ? (int) $_GET['flatviewlist_per_page'] : null;
$offset = isset($_GET['offset']) ? $_GET['offset'] : '0';
$offset = $_GET['offset'] ?? '0';
$exportCsvUrl = api_get_self().'?'.api_get_cidreq().'&'.http_build_query([
'export_format' => 'csv',
@@ -367,8 +371,8 @@ class DisplayGradebook
$additionalButtons = null;
if (!empty($certificateLinkInfo)) {
$additionalButtons .= '<div class="btn-group pull-right">';
$additionalButtons .= isset($certificateLinkInfo['certificate_link']) ? $certificateLinkInfo['certificate_link'] : '';
$additionalButtons .= isset($certificateLinkInfo['badge_link']) ? $certificateLinkInfo['badge_link'] : '';
$additionalButtons .= $certificateLinkInfo['certificate_link'] ?? '';
$additionalButtons .= $certificateLinkInfo['badge_link'] ?? '';
$additionalButtons .= '</div>';
}
$scoreinfo .= '<strong>'.sprintf(get_lang('TotalX'), $scorecourse_display.$additionalButtons).'</strong>';
@@ -381,33 +385,28 @@ class DisplayGradebook
$header = '<div class="actions"><table>';
$header .= '<tr>';
if (!$selectcat == '0') {
$header .= '<td><a href="'.api_get_self().'?selectcat='.$catobj->get_parent_id().'">'.
Display::return_icon(
$header .= '<td><a href="'.api_get_self().'?selectcat='.$catobj->get_parent_id().'">'
.Display::return_icon(
'back.png',
get_lang('BackTo').' '.get_lang('RootCat'),
'',
[],
ICON_SIZE_MEDIUM
).
'</a></td>';
)
.'</a></td>';
}
$header .= '<td>'.get_lang('CurrentCategory').'</td>'.
'<td><form name="selector"><select name="selectcat" onchange="document.selector.submit()">';
$header .= '<td>'.get_lang('CurrentCategory').'</td>'
.'<td><form name="selector"><select name="selectcat" onchange="document.selector.submit()">';
$cats = Category::load();
$tree = $cats[0]->get_tree();
unset($cats);
$line = null;
foreach ($tree as $cat) {
for ($i = 0; $i < $cat[2]; $i++) {
$line .= '&mdash;';
}
$line = isset($line) ? $line : '';
$line = str_repeat('&mdash;', $cat[2]);
if (isset($_GET['selectcat']) && $_GET['selectcat'] == $cat[0]) {
$header .= '<option selected value='.$cat[0].'>'.$line.' '.$cat[1].'</option>';
} else {
$header .= '<option value='.$cat[0].'>'.$line.' '.$cat[1].'</option>';
}
$line = '';
}
$header .= '</select></form></td>';
if (!empty($simple_search_form) && $message_resource === false) {
@@ -445,25 +444,25 @@ class DisplayGradebook
$my_api_cidreq = 'cidReq='.$my_category['course_code'];
}
if ($show_add_link && !$message_resource) {
$actionsLeft .= '<a href="gradebook_add_eval.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'" >'.
Display::return_icon('new_evaluation.png', get_lang('NewEvaluation'), '',
ICON_SIZE_MEDIUM).'</a>';
$actionsLeft .= '<a href="gradebook_add_eval.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'" >'
.Display::return_icon('new_evaluation.png', get_lang('NewEvaluation'), [], ICON_SIZE_MEDIUM)
.'</a>';
$cats = Category::load($selectcat);
if ($cats[0]->get_course_code() != null && !$message_resource) {
$actionsLeft .= '<a href="gradebook_add_link.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'.
Display::return_icon('new_online_evaluation.png', get_lang('MakeLink'), '',
ICON_SIZE_MEDIUM).'</a>';
$actionsLeft .= '<a href="gradebook_add_link.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'
.Display::return_icon('new_online_evaluation.png', get_lang('MakeLink'), [], ICON_SIZE_MEDIUM)
.'</a>';
} else {
$actionsLeft .= '<a href="gradebook_add_link_select_course.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'.
Display::return_icon('new_online_evaluation.png', get_lang('MakeLink'), '',
ICON_SIZE_MEDIUM).'</a>';
$actionsLeft .= '<a href="gradebook_add_link_select_course.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'
.Display::return_icon('new_online_evaluation.png', get_lang('MakeLink'), [], ICON_SIZE_MEDIUM)
.'</a>';
}
}
}
if ((empty($grade_model_id) || $grade_model_id == -1) && $accessToEdit) {
$actionsLeft .= '<a href="gradebook_add_cat.php?'.api_get_cidreq().'&selectcat='.$catobj->get_id().'">'.
Display::return_icon(
$actionsLeft .= '<a href="gradebook_add_cat.php?'.api_get_cidreq().'&selectcat='.$catobj->get_id().'">'
.Display::return_icon(
'new_folder.png',
get_lang('AddGradebook'),
[],
@@ -471,10 +470,11 @@ class DisplayGradebook
).'</a></td>';
}
if ($selectcat != '0' && $accessToRead) {
if ($selectcat != '0') {
if (!$message_resource) {
$actionsLeft .= '<a href="gradebook_flatview.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'.
Display::return_icon('statistics.png', get_lang('FlatView'), '', ICON_SIZE_MEDIUM).'</a>';
$actionsLeft .= '<a href="gradebook_flatview.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'
.Display::return_icon('statistics.png', get_lang('FlatView'), [], ICON_SIZE_MEDIUM)
.'</a>';
if ($my_category['generate_certificates'] == 1) {
$actionsLeft .= Display::url(
@@ -510,41 +510,40 @@ class DisplayGradebook
// Right icons
if ($accessToEdit) {
$actionsRight = '<a href="gradebook_edit_cat.php?editcat='.$catobj->get_id(
).'&cidReq='.$catobj->get_course_code().'&id_session='.$catobj->get_session_id().'">'.
Display::return_icon('edit.png', get_lang('Edit'), '', ICON_SIZE_MEDIUM).'</a>';
).'&cidReq='.$catobj->get_course_code().'&id_session='.$catobj->get_session_id().'">'
.Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_MEDIUM)
.'</a>';
if (api_get_plugin_setting('customcertificate', 'enable_plugin_customcertificate') === 'true' &&
api_get_course_setting('customcertificate_course_enable') == 1
) {
$actionsRight .= '<a href="'.api_get_path(
WEB_PLUGIN_PATH
).'customcertificate/src/index.php?'.
$my_api_cidreq.'&origin=gradebook&selectcat='.$catobj->get_id().'">'.
$actionsRight .= '<a href="'.api_get_path(WEB_PLUGIN_PATH).'customcertificate/src/index.php?'
.$my_api_cidreq.'&origin=gradebook&selectcat='.$catobj->get_id().'">'.
Display::return_icon(
'certificate.png',
get_lang('AttachCertificate'),
'',
[],
ICON_SIZE_MEDIUM
).'</a>';
} else {
$actionsRight .= '<a href="'.api_get_path(WEB_CODE_PATH).
'document/document.php?curdirpath=/certificates&'.
$my_api_cidreq.'&origin=gradebook&selectcat='.$catobj->get_id().'">'.
$actionsRight .= '<a href="'.api_get_path(WEB_CODE_PATH)
.'document/document.php?curdirpath=/certificates&'.$my_api_cidreq
.'&origin=gradebook&selectcat='.$catobj->get_id().'">'.
Display::return_icon(
'certificate.png',
get_lang('AttachCertificate'),
'',
[],
ICON_SIZE_MEDIUM
).'</a>';
}
if (empty($categories)) {
$actionsRight .= '<a href="gradebook_edit_all.php?id_session='.api_get_session_id(
).'&'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'.
Display::return_icon(
$actionsRight .= '<a href="gradebook_edit_all.php?id_session='.api_get_session_id()
.'&'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'
.Display::return_icon(
'percentage.png',
get_lang('EditAllWeights'),
'',
[],
ICON_SIZE_MEDIUM
).'</a>';
}
@@ -552,8 +551,8 @@ class DisplayGradebook
if (api_get_setting('teachers_can_change_score_settings') == 'true' &&
$score_display_custom['my_display_custom'] == 'true'
) {
$actionsRight .= '<a href="gradebook_scoring_system.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'.
Display::return_icon('ranking.png', get_lang('ScoreEdit'), '', ICON_SIZE_MEDIUM).'</a>';
$actionsRight .= '<a href="gradebook_scoring_system.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'
.Display::return_icon('ranking.png', get_lang('ScoreEdit'), [], ICON_SIZE_MEDIUM).'</a>';
}
}
}
@@ -568,14 +567,14 @@ class DisplayGradebook
);
if ($isDrhOfCourse) {
$actionsLeft .= '<a href="gradebook_flatview.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'.
Display::return_icon(
$actionsLeft .= '<a href="gradebook_flatview.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'
.Display::return_icon(
'statistics.png',
get_lang('FlatView'),
'',
[],
ICON_SIZE_MEDIUM
).
'</a>';
)
.'</a>';
}
if ($isCoach || api_is_allowed_to_edit(null, true)) {
@@ -607,8 +606,8 @@ class DisplayGradebook
}
$min_certification = get_lang('CertificateMinScore').' : '.$min_certification;
$edit_icon = '<a href="gradebook_edit_cat.php?editcat='.$catobj->get_id().'&cidReq='.$catobj->get_course_code().'&id_session='.$catobj->get_session_id().'">'.
Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL).'</a>';
$edit_icon = '<a href="gradebook_edit_cat.php?editcat='.$catobj->get_id().'&cidReq='.$catobj->get_course_code().'&id_session='.$catobj->get_session_id().'">'
.Display::return_icon('edit.png', get_lang('Edit')).'</a>';
$msg = $weight.' - '.$min_certification.$edit_icon;
//@todo show description
@@ -659,16 +658,16 @@ class DisplayGradebook
$header = '<div class="actions">';
if ($is_course_admin) {
$header .= '<a href="gradebook_flatview.php?'.api_get_cidreq().'&selectcat='.$catobj->get_id().'">'.
Display::return_icon('statistics.png', get_lang('FlatView'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="gradebook_scoring_system.php?'.api_get_cidreq().'&selectcat='.$catobj->get_id().'">'.
Display::return_icon('settings.png', get_lang('ScoreEdit'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="gradebook_flatview.php?'.api_get_cidreq().'&selectcat='.$catobj->get_id().'">'
.Display::return_icon('statistics.png', get_lang('FlatView'), [], ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="gradebook_scoring_system.php?'.api_get_cidreq().'&selectcat='.$catobj->get_id().'">'
.Display::return_icon('settings.png', get_lang('ScoreEdit'), [], ICON_SIZE_MEDIUM).'</a>';
} elseif (!(isset($_GET['studentoverview']))) {
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&studentoverview=&selectcat='.$catobj->get_id().'">'.
Display::return_icon('view_list.gif', get_lang('FlatView')).' '.get_lang('FlatView').'</a>';
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&studentoverview=&selectcat='.$catobj->get_id().'">'
.Display::return_icon('view_list.gif', get_lang('FlatView')).' '.get_lang('FlatView').'</a>';
} else {
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&studentoverview=&exportpdf=&selectcat='.$catobj->get_id().'" target="_blank">'.
Display::return_icon('pdf.png', get_lang('ExportPDF'), '', ICON_SIZE_MEDIUM).'</a>';
$header .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&studentoverview=&exportpdf=&selectcat='.$catobj->get_id().'" target="_blank">'
.Display::return_icon('pdf.png', get_lang('ExportPDF'), [], ICON_SIZE_MEDIUM).'</a>';
}
$header .= '</div>';
echo $header;

View File

@@ -915,7 +915,7 @@ class GradebookTable extends SortableTable
$averageWeight += $averageScore[1];
}
$categoryAverage = $categoryAverage / count($totalAverageList);
//$averageWeight = $averageWeight ($totalAverageList);
$averageWeight = $averageWeight / count($totalAverageList);
// Overwrite main weight
//$totalAverage[0] = $average / count($this->studentList);

View File

@@ -8,8 +8,27 @@
*/
require_once __DIR__.'/../inc/global.inc.php';
$allowedHelp = [
'Blogs',
'Group',
'Groups',
'Announcements',
'Settings',
'Doc',
'Dropbox',
'Exercise',
'Tracking',
'User',
'Links',
'Path',
'Survey',
'Classes',
'Wiki',
];
$help_name = isset($_GET['open']) ? Security::remove_XSS($_GET['open']) : null;
if (empty($help_name)) {
if (empty($help_name) || !in_array($help_name, $allowedHelp)) {
api_not_allowed(true);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -4,6 +4,9 @@
/**
* Responses to AJAX calls for the document upload.
*/
use Chamilo\CoreBundle\Component\Editor\Driver\Driver;
require_once __DIR__.'/../global.inc.php';
$action = $_REQUEST['a'];
@@ -153,17 +156,8 @@ switch ($action) {
}
$resultList = [];
foreach ($fileList as $file) {
if (isset($_REQUEST['chunkAction']) && 'done' === $_REQUEST['chunkAction']) {
// to rename and move the finished file
$tmpFile = disable_dangerous_file(
api_replace_dangerous_char($file['name'])
);
$chunkedFile = api_get_path(SYS_ARCHIVE_PATH).$tmpFile;
$file['tmp_name'] = $chunkedFile;
$file['size'] = filesize($chunkedFile);
$file['copy_file'] = true;
}
foreach ($fileList as $fileInfo) {
$file = processChunkedFile($fileInfo);
$globalFile = [];
$globalFile['files'] = $file;
@@ -208,6 +202,10 @@ switch ($action) {
}
break;
case 'ck_uploadimage':
if (true !== api_get_configuration_value('enable_uploadimage_editor')) {
exit;
}
api_protect_course_script(true);
// it comes from uploaimage drag and drop ckeditor
@@ -219,6 +217,14 @@ switch ($action) {
$data = [];
$fileUpload = $_FILES['upload'];
$mimeType = mime_content_type($fileUpload['tmp_name']);
$isMimeAccepted = (new Driver())->mimeAccepted($mimeType, ['image']);
if (!$isMimeAccepted) {
exit;
}
$isAllowedToEdit = api_is_allowed_to_edit(null, true);
if ($isAllowedToEdit) {
$globalFile = ['files' => $fileUpload];
@@ -233,14 +239,17 @@ switch ($action) {
false,
'files'
);
if ($result) {
if (!$result) {
exit;
}
$relativeUrl = str_replace(api_get_path(WEB_PATH), '/', $result['direct_url']);
$data = [
'uploaded' => 1,
'fileName' => $fileUpload['name'],
'url' => $relativeUrl,
];
}
} else {
$userId = api_get_user_id();
$syspath = UserManager::getUserPathById($userId, 'system').'my_files';
@@ -255,7 +264,14 @@ switch ($action) {
$suffix = '_'.uniqid();
$fileUploadName = $fileName.$suffix.'.'.$extension;
}
if (move_uploaded_file($fileUpload['tmp_name'], $syspath.$fileUploadName)) {
$personalDriver = new PersonalDriver();
$uploadResult = $personalDriver->mimeAccepted(mime_content_type($fileUpload['tmp_name']), ['image']);
if (!$uploadResult || !move_uploaded_file($fileUpload['tmp_name'], $syspath.$fileUploadName)) {
exit;
}
$url = $webpath.$fileUploadName;
$relativeUrl = str_replace(api_get_path(WEB_PATH), '/', $url);
$data = [
@@ -264,7 +280,6 @@ switch ($action) {
'url' => $relativeUrl,
];
}
}
echo json_encode($data);
exit;
case 'document_preview':

View File

@@ -79,14 +79,8 @@ switch ($action) {
}
$resultList = [];
foreach ($fileList as $file) {
if (isset($_REQUEST['chunkAction']) && 'done' === $_REQUEST['chunkAction']) {
// to rename and move the finished file
$chunkedFile = api_get_path(SYS_ARCHIVE_PATH).$file['name'];
$file['tmp_name'] = $chunkedFile;
$file['size'] = filesize($chunkedFile);
$file['copy_file'] = true;
}
foreach ($fileList as $fileInfo) {
$file = processChunkedFile($fileInfo);
$globalFile = [];
$globalFile['files'] = $file;

View File

@@ -489,23 +489,22 @@ switch ($action) {
}
// "all" or "simple" strings means that there's one or all questions exercise type
$type = isset($_REQUEST['type']) ? $_REQUEST['type'] : null;
$type = $_REQUEST['type'] ?? null;
// Questions choices.
$choice = isset($_REQUEST['choice']) ? $_REQUEST['choice'] : [];
$choice = $_REQUEST['choice'] ?? [];
// certainty degree choice
$choiceDegreeCertainty = isset($_REQUEST['choiceDegreeCertainty']) ? $_REQUEST['choiceDegreeCertainty'] : [];
$choiceDegreeCertainty = $_REQUEST['choiceDegreeCertainty'] ?? [];
// Hot spot coordinates from all questions.
$hot_spot_coordinates = isset($_REQUEST['hotspot']) ? $_REQUEST['hotspot'] : [];
$hot_spot_coordinates = $_REQUEST['hotspot'] ?? [];
// the filenames in upload answer type
$uploadAnswerFileNames = isset($_REQUEST['uploadChoice']) ? $_REQUEST['uploadChoice'] : [];
$uploadAnswerFileNames = $_REQUEST['uploadChoice'] ?? [];
// There is a reminder?
$remind_list = isset($_REQUEST['remind_list']) && !empty($_REQUEST['remind_list'])
? array_keys($_REQUEST['remind_list']) : [];
$remind_list = !empty($_REQUEST['remind_list']) ? array_keys($_REQUEST['remind_list']) : [];
// Needed in manage_answer.
$learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
@@ -662,7 +661,7 @@ switch ($action) {
if ($type === 'simple' && $question_id != $my_question_id) {
continue;
}
$my_choice = isset($choice[$my_question_id]) ? $choice[$my_question_id] : null;
$my_choice = $choice[$my_question_id] ?? null;
$objQuestionTmp = Question::read($my_question_id, $objExercise->course);
$myChoiceDegreeCertainty = null;
if ($objQuestionTmp->type === MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
@@ -700,8 +699,7 @@ switch ($action) {
// This variable came from exercise_submit_modal.php.
$hotspot_delineation_result = null;
if (isset($_SESSION['hotspot_delineation_result']) &&
isset($_SESSION['hotspot_delineation_result'][$objExercise->selectId()])
if (isset($_SESSION['hotspot_delineation_result'][$objExercise->selectId()])
) {
$hotspot_delineation_result = $_SESSION['hotspot_delineation_result'][$objExercise->selectId()][$my_question_id];
}

View File

@@ -23,7 +23,10 @@ switch ($action) {
Security::clear_token();
if (isset($_REQUEST['new_language']) && isset($_REQUEST['variable_language']) && isset($_REQUEST['category_id'])) {
$newLanguage = Security::remove_XSS($_REQUEST['new_language']);
$langVariable = Security::remove_XSS($_REQUEST['variable_language']);
$langVariable = ltrim(
Security::remove_XSS($_REQUEST['variable_language']),
'$'
);
$categoryId = (int) $_REQUEST['category_id'];
$languageId = (int) $_REQUEST['id'];
$subLanguageId = (int) $_REQUEST['sub'];
@@ -41,10 +44,7 @@ switch ($action) {
$returnValue = SubLanguageManager::add_file_in_language_directory($pathFolder);
//update variable language
// Replace double quotes to avoid parse errors
$newLanguage = str_replace('"', '\"', $newLanguage);
$newLanguage = str_replace("\n", "\\n", $newLanguage);
$allFileOfDirectory[$langVariable] = "\"".$newLanguage."\";";
$allFileOfDirectory[$langVariable] = $newLanguage;
$resultArray = [];
foreach ($allFileOfDirectory as $key => $value) {

View File

@@ -23,7 +23,7 @@ if (empty($savedRows)) {
$sidx = $_REQUEST['sidx']; //index (field) to filter
$sord = $_REQUEST['sord']; //asc or desc
$exportFilename = isset($_REQUEST['export_filename']) ? $_REQUEST['export_filename'] : '';
$exportFilename = $_REQUEST['export_filename'] ?? '';
if (strpos(strtolower($sidx), 'asc') !== false) {
$sidx = str_replace(['asc', ','], '', $sidx);
@@ -131,18 +131,20 @@ function getWhereClause($col, $oper, $val)
// If there is no search request sent by jqgrid, $where should be empty
$whereCondition = '';
$operation = isset($_REQUEST['oper']) ? $_REQUEST['oper'] : false;
$exportFormat = isset($_REQUEST['export_format']) ? $_REQUEST['export_format'] : 'csv';
$searchField = isset($_REQUEST['searchField']) ? $_REQUEST['searchField'] : false;
$searchOperator = isset($_REQUEST['searchOper']) ? $_REQUEST['searchOper'] : false;
$searchString = isset($_REQUEST['searchString']) ? $_REQUEST['searchString'] : false;
$search = isset($_REQUEST['_search']) ? $_REQUEST['_search'] : false;
$forceSearch = isset($_REQUEST['_force_search']) ? $_REQUEST['_force_search'] : false;
$operation = $_REQUEST['oper'] ?? false;
$exportFormat = $_REQUEST['export_format'] ?? 'csv';
$searchField = $_REQUEST['searchField'] ?? false;
$searchOperator = $_REQUEST['searchOper'] ?? false;
$searchString = $_REQUEST['searchString'] ?? false;
$search = $_REQUEST['_search'] ?? false;
$forceSearch = $_REQUEST['_force_search'] ?? false;
$extra_fields = [];
$accessStartDate = '';
$accessEndDate = '';
$overwriteColumnHeaderExport = [];
$result = [];
if (!empty($search)) {
$search = 'true';
}
@@ -290,18 +292,18 @@ if (!$sidx) {
switch ($action) {
case 'get_exercise_categories':
$manager = new ExerciseCategoryManager();
$courseId = isset($_REQUEST['c_id']) ? $_REQUEST['c_id'] : 0;
$courseId = $_REQUEST['c_id'] ?? 0;
$count = $manager->getCourseCount($courseId);
break;
case 'get_calendar_users':
$calendarPlugin = LearningCalendarPlugin::create();
$id = isset($_REQUEST['id']) ? $_REQUEST['id'] : 0;
$id = $_REQUEST['id'] ?? 0;
$count = $calendarPlugin->getUsersPerCalendarCount($id);
break;
case 'get_usergroups_users':
$usergroup = new UserGroup();
$usergroup->protectScript(null, true, true);
$id = isset($_REQUEST['id']) ? $_REQUEST['id'] : 0;
$id = $_REQUEST['id'] ?? 0;
$count = $usergroup->getUserGroupUsers($id, true);
break;
case 'get_learning_path_calendars':
@@ -324,9 +326,9 @@ switch ($action) {
$count = $object->get_count();
break;
case 'get_group_reporting':
$course_id = isset($_REQUEST['course_id']) ? $_REQUEST['course_id'] : null;
$group_id = isset($_REQUEST['gidReq']) ? $_REQUEST['gidReq'] : null;
$sessionId = isset($_REQUEST['session_id']) ? $_REQUEST['session_id'] : null;
$course_id = $_REQUEST['course_id'] ?? null;
$group_id = $_REQUEST['gidReq'] ?? null;
$sessionId = $_REQUEST['session_id'] ?? null;
$count = Tracking::get_group_reporting(
$course_id,
$sessionId,
@@ -628,7 +630,7 @@ switch ($action) {
return 0;
}
require_once api_get_path(SYS_CODE_PATH).'work/work.lib.php';
$workId = isset($_GET['work_id']) ? $_GET['work_id'] : null;
$workId = $_GET['work_id'] ?? null;
$count = getWorkUserListData(
$workId,
api_get_course_id(),
@@ -650,8 +652,8 @@ switch ($action) {
$exerciseId = $_REQUEST['exercise_id'] ?? 0;
$status = $_REQUEST['status'] ?? 0;
$questionType = $_REQUEST['questionType'] ?? 0;
$showAttemptsInSessions = $_REQUEST['showAttemptsInSessions'] ? true : false;
if (isset($_GET['filter_by_user']) && !empty($_GET['filter_by_user'])) {
$showAttemptsInSessions = (bool) $_REQUEST['showAttemptsInSessions'];
if (!empty($_GET['filter_by_user'])) {
$filter_user = (int) $_GET['filter_by_user'];
if (empty($whereCondition)) {
$whereCondition .= " te.exe_user_id = '$filter_user'";
@@ -660,7 +662,7 @@ switch ($action) {
}
}
if (isset($_GET['group_id_in_toolbar']) && !empty($_GET['group_id_in_toolbar'])) {
if (!empty($_GET['group_id_in_toolbar'])) {
$groupIdFromToolbar = (int) $_GET['group_id_in_toolbar'];
if (!empty($groupIdFromToolbar)) {
if (empty($whereCondition)) {
@@ -695,7 +697,7 @@ switch ($action) {
case 'get_exercise_results':
$exercise_id = $_REQUEST['exerciseId'];
if (isset($_GET['filter_by_user']) && !empty($_GET['filter_by_user'])) {
if (!empty($_GET['filter_by_user'])) {
$filter_user = (int) $_GET['filter_by_user'];
if (empty($whereCondition)) {
$whereCondition .= " te.exe_user_id = '$filter_user'";
@@ -704,7 +706,7 @@ switch ($action) {
}
}
if (isset($_GET['group_id_in_toolbar']) && !empty($_GET['group_id_in_toolbar'])) {
if (!empty($_GET['group_id_in_toolbar'])) {
$groupIdFromToolbar = (int) $_GET['group_id_in_toolbar'];
if (!empty($groupIdFromToolbar)) {
if (empty($whereCondition)) {
@@ -723,8 +725,8 @@ switch ($action) {
break;
case 'get_exercise_results_report':
api_protect_admin_script();
$exerciseId = isset($_REQUEST['exercise_id']) ? $_REQUEST['exercise_id'] : 0;
$courseId = isset($_REQUEST['course_id']) ? $_REQUEST['course_id'] : 0;
$exerciseId = $_REQUEST['exercise_id'] ?? 0;
$courseId = $_REQUEST['course_id'] ?? 0;
if (empty($exerciseId)) {
exit;
@@ -733,7 +735,7 @@ switch ($action) {
if (!empty($courseId)) {
$courseInfo = api_get_course_info_by_id($courseId);
} else {
$courseCode = isset($_REQUEST['cidReq']) ? $_REQUEST['cidReq'] : '';
$courseCode = $_REQUEST['cidReq'] ?? '';
if (!empty($courseCode)) {
$courseInfo = api_get_course_info($courseCode);
}
@@ -762,7 +764,7 @@ switch ($action) {
$count = ExerciseLib::get_count_exam_hotpotatoes_results($hotpot_path);
break;
case 'get_sessions_tracking':
$keyword = isset($_REQUEST['keyword']) ? $_REQUEST['keyword'] : '';
$keyword = $_REQUEST['keyword'] ?? '';
$description = '';
$setting = api_get_setting('show_session_description');
@@ -813,7 +815,7 @@ switch ($action) {
}
break;
case 'get_sessions':
$listType = isset($_REQUEST['list_type']) ? $_REQUEST['list_type'] : SessionManager::getDefaultSessionTab();
$listType = $_REQUEST['list_type'] ?? SessionManager::getDefaultSessionTab();
if ('custom' === $listType && api_get_configuration_value('allow_session_status')) {
$whereCondition .= ' AND (s.status IN ("'.SessionManager::STATUS_PLANNED.'", "'.SessionManager::STATUS_PROGRESS.'") ) ';
@@ -947,9 +949,9 @@ switch ($action) {
case 'get_usergroups_teacher':
$obj = new UserGroup();
$obj->protectScript(null, false, true);
$type = isset($_REQUEST['type']) ? $_REQUEST['type'] : 'registered';
$type = $_REQUEST['type'] ?? 'registered';
$groupFilter = isset($_REQUEST['group_filter']) ? (int) $_REQUEST['group_filter'] : 0;
$keyword = isset($_REQUEST['keyword']) ? $_REQUEST['keyword'] : '';
$keyword = $_REQUEST['keyword'] ?? '';
$course_id = api_get_course_int_id();
$sessionId = api_get_session_id();
@@ -1349,8 +1351,8 @@ switch ($action) {
'actions',
];
$titleToSearch = isset($_REQUEST['title_to_search']) ? $_REQUEST['title_to_search'] : '';
$userIdToSearch = isset($_REQUEST['user_id_to_search']) ? $_REQUEST['user_id_to_search'] : 0;
$titleToSearch = $_REQUEST['title_to_search'] ?? '';
$userIdToSearch = $_REQUEST['user_id_to_search'] ?? 0;
$sidx = in_array($sidx, $columns) ? $sidx : 'title';
$result = AnnouncementManager::getAnnouncements(
null,
@@ -1439,8 +1441,6 @@ switch ($action) {
'qualificator_id',
'correction',
];
$columns = array_merge($columns, $plagiarismColumns);
$columns[] = 'actions';
} else {
$columns = [
'fullname',
@@ -1449,9 +1449,9 @@ switch ($action) {
'sent_date',
'correction',
];
}
$columns = array_merge($columns, $plagiarismColumns);
$columns[] = 'actions';
}
$whereCondition = " AND $whereCondition ";
$columnOrderValidList = array_merge(['firstname', 'lastname'], $columns);
@@ -1513,13 +1513,11 @@ switch ($action) {
$columns = [
'type', 'firstname', 'lastname', 'title', 'qualification', 'sent_date', 'qualificator_id',
];
$columns = array_merge($columns, $plagiarismColumns);
$columns[] = 'actions';
} else {
$columns = ['type', 'firstname', 'lastname', 'title', 'sent_date'];
}
$columns = array_merge($columns, $plagiarismColumns);
$columns[] = 'actions';
}
if (trim($whereCondition) === '1 = 1') {
$whereCondition = '';
@@ -1547,13 +1545,11 @@ switch ($action) {
$columns = [
'type', 'title', 'qualification', 'sent_date', 'qualificator_id',
];
$columns = array_merge($columns, $plagiarismColumns);
$columns[] = 'actions';
} else {
$columns = ['type', 'title', 'qualification', 'sent_date'];
}
$columns = array_merge($columns, $plagiarismColumns);
$columns[] = 'actions';
}
$documents = getAllDocumentToWork($work_id, api_get_course_int_id());
if (trim($whereCondition) === '1 = 1') {
@@ -2796,7 +2792,7 @@ if (in_array($action, $allowed_actions)) {
foreach ($result as $row) {
// if results tab give not id, set id to $i otherwise id="null"
// for all <tr> of the jqgrid - ref #4235
if (!isset($row['id']) || isset($row['id']) && $row['id'] == '') {
if (!isset($row['id']) || $row['id'] == '') {
$response->rows[$i]['id'] = $i;
} else {
$response->rows[$i]['id'] = $row['id'];
@@ -2804,7 +2800,7 @@ if (in_array($action, $allowed_actions)) {
$array = [];
foreach ($columns as $col) {
if (in_array($col, ['correction', 'actions'])) {
$array[] = isset($row[$col]) ? $row[$col] : '';
$array[] = $row[$col] ?? '';
} else {
$array[] = isset($row[$col]) ? Security::remove_XSS($row[$col]) : '';
}

View File

@@ -4,6 +4,7 @@
use Chamilo\CoreBundle\Entity\Message;
use Chamilo\CoreBundle\Entity\MessageFeedback;
use ChamiloSession as Session;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Responses to AJAX calls.
@@ -19,29 +20,36 @@ switch ($action) {
echo '';
break;
}
if (Security::check_token('get', null, 'invitation')) {
$relation_type = USER_RELATION_TYPE_UNKNOWN; //Unknown contact
if (isset($_GET['is_my_friend'])) {
$relation_type = USER_RELATION_TYPE_FRIEND; //My friend
}
if (isset($_GET['friend_id'])) {
$my_current_friend = $_GET['friend_id'];
$my_current_friend = (int) $_GET['friend_id'];
if (SocialManager::hasInvitationByUser($current_user_id, $my_current_friend)) {
UserManager::relate_users($current_user_id, $my_current_friend, $relation_type);
UserManager::relate_users($my_current_friend, $current_user_id, $relation_type);
SocialManager::invitation_accepted($my_current_friend, $current_user_id);
Display::addFlash(
Display::return_message(get_lang('AddedContactToList'), 'success')
);
}
}
}
header('Location: '.api_get_path(WEB_CODE_PATH).'social/invitations.php');
exit;
}
break;
case 'deny_friend':
if (api_is_anonymous()) {
echo '';
break;
}
if (Security::check_token('get', null, 'invitation')) {
$relation_type = USER_RELATION_TYPE_UNKNOWN; //Contact unknown
if (isset($_GET['is_my_friend'])) {
$relation_type = USER_RELATION_TYPE_FRIEND; //my friend
@@ -51,19 +59,29 @@ switch ($action) {
Display::addFlash(
Display::return_message(get_lang('InvitationDenied'), 'success')
);
}
}
header('Location: '.api_get_path(WEB_CODE_PATH).'social/invitations.php');
exit;
}
break;
case 'delete_friend':
if (api_is_anonymous()) {
echo '';
break;
}
$my_delete_friend = (int) $_POST['delete_friend_id'];
if (!Security::check_token('post', null, 'social')) {
exit;
}
if (isset($_POST['delete_friend_id'])) {
$my_delete_friend = (int) $_POST['delete_friend_id'];
SocialManager::remove_user_rel_user($my_delete_friend);
JsonResponse::create([
'secToken' => Security::get_token('social'),
])->send();
break;
}
break;
case 'show_my_friends':
@@ -205,6 +223,10 @@ switch ($action) {
exit;
}
if (!Security::check_token('get', null, 'wall')) {
exit;
}
$messageId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if (empty($messageId)) {
@@ -225,7 +247,10 @@ switch ($action) {
);
if ($messageId) {
$messageInfo = MessageManager::get_message_by_id($messageId);
echo SocialManager::processPostComment($messageInfo);
JsonResponse::create([
'secToken' => Security::get_token('wall'),
'postHTML' => SocialManager::processPostComment($messageInfo),
])->send();
}
}
}

View File

@@ -8,7 +8,7 @@ use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
*/
require_once __DIR__.'/../global.inc.php';
api_protect_admin_script();
api_protect_admin_script(true);
$action = isset($_REQUEST['a']) ? $_REQUEST['a'] : null;
$sessionDuration = isset($_GET['session_duration']) ? (int) $_GET['session_duration'] : 0;

View File

@@ -463,6 +463,86 @@ switch ($action) {
header('Content-Type: application/json');
echo json_encode(['items' => $items]);
break;
case 'update_users':
$usersData = json_decode($_POST['users'], true);
$updatedCount = 0;
foreach ($usersData as $userData) {
if (empty($userData['user_id'])) {
continue;
}
$userId = (int) $userData['user_id'];
$currentUserData = api_get_user_info($userId);
if (!$currentUserData) {
continue;
}
$updatedData = [
'firstname' => $userData['firstname'] ?? $currentUserData['firstname'],
'lastname' => $userData['lastname'] ?? $currentUserData['lastname'],
'email' => $userData['email'] ?? $currentUserData['email'],
'phone' => $userData['phone'] ?? $currentUserData['phone'],
'official_code' => $userData['official_code'] ?? $currentUserData['official_code'],
'status' => isset($userData['status']) ? (int) $userData['status'] : $currentUserData['status'],
'active' => isset($userData['active']) ? (int) $userData['active'] : $currentUserData['active'],
];
if (!empty($userData['password'])) {
$updatedData['password'] = $userData['password'];
}
$extraFieldHandler = new ExtraField('user');
$extraFieldValue = new ExtraFieldValue('user');
$extraFields = [];
foreach ($userData as $key => &$value) {
if (strpos($key, 'extra_') === 0) {
$fieldName = str_replace('extra_', '', $key);
$fieldInfo = $extraFieldHandler->get_handler_field_info_by_field_variable($fieldName);
if ($fieldInfo) {
if ($fieldInfo['field_type'] == 10 && is_string($value) && strpos($value, ',') !== false) {
$value = explode(',', $value);
}
}
}
}
UserManager::update_user(
$userId,
$updatedData['firstname'],
$updatedData['lastname'],
$currentUserData['username'],
$updatedData['password'] ?? null,
$currentUserData['auth_source'],
$updatedData['email'],
$updatedData['status'],
$updatedData['official_code'],
$updatedData['phone'],
$currentUserData['picture_uri'],
null,
$updatedData['active'],
null,
null,
null,
$currentUserData['language']
);
$userData['item_id'] = $userId;
$extraFieldValue->saveFieldValues(
$userData,
false,
false,
[],
[],
true
);
$updatedCount++;
}
echo json_encode(['message' => get_lang('Saved').' '.$updatedCount]);
break;
default:
echo '';
}

View File

@@ -120,14 +120,8 @@ switch ($action) {
}
$resultList = [];
foreach ($fileList as $file) {
if (isset($_REQUEST['chunkAction']) && 'done' === $_REQUEST['chunkAction']) {
// to rename and move the finished file
$chunkedFile = api_get_path(SYS_ARCHIVE_PATH).$file['name'];
$file['tmp_name'] = $chunkedFile;
$file['size'] = filesize($chunkedFile);
$file['copy_file'] = true;
}
foreach ($fileList as $fileInfo) {
$file = processChunkedFile($fileInfo);
$globalFile = [];
$globalFile['files'] = $file;

View File

@@ -191,13 +191,26 @@ class Compilatio
$documentInfo = [
'report_url' => $dataDocument['report_url'],
];
if (isset($dataDocument['analyses']['anasim']['state'])) {
$documentInfo['analysis_status'] = $dataDocument['analyses']['anasim']['state'];
// anasim analyse type is applied for services Magister and Copyright
// anasim-premium analyse type is applied for services Magister+ and Copyright+
$anasim = 'anasim';
if (isset($dataDocument['analyses']['anasim-premium'])) {
$anasim = 'anasim-premium';
if (isset($dataDocument['analyses']['anasim'])) {
if (isset($dataDocument['analyses']['anasim']['creation_launch_date']) && isset($dataDocument['analyses']['anasim-premium']['creation_launch_date'])) {
// if the 2 analyses type exist (which could happen technically but would be exceptional) then we present the most recent one.
if ($dataDocument['analyses']['anasim']['creation_launch_date'] > $dataDocument['analyses']['anasim-premium']['creation_launch_date']) {
$anasim = 'anasim';
}
}
}
}
if (isset($dataDocument['analyses'][$anasim]['state'])) {
$documentInfo['analysis_status'] = $dataDocument['analyses'][$anasim]['state'];
}
if (isset($dataDocument['light_reports']['anasim']['scores']['global_score_percent'])) {
$documentInfo['report_percent'] = $dataDocument['light_reports']['anasim']['scores']['global_score_percent'];
if (isset($dataDocument['light_reports'][$anasim]['scores']['global_score_percent'])) {
$documentInfo['report_percent'] = $dataDocument['light_reports'][$anasim]['scores']['global_score_percent'];
}
return $documentInfo;
@@ -224,7 +237,6 @@ class Compilatio
[
'json' => [
'doc_id' => $compilatioId,
'recipe_name' => 'anasim',
],
]
)

View File

@@ -1998,7 +1998,7 @@ class CoursesAndSessionsCatalog
$action = isset($action) ? Security::remove_XSS($action) : $requestAction;
$searchTerm = isset($_REQUEST['search_term']) ? Security::remove_XSS($_REQUEST['search_term']) : '';
$keyword = isset($_REQUEST['keyword']) ? Security::remove_XSS($_REQUEST['keyword']) : '';
$searchTag = $_REQUEST['search_tag'] ? Security::remove_XSS($_REQUEST['search_tag']) : '';
$searchTag = isset($_REQUEST['search_tag']) ? Security::remove_XSS($_REQUEST['search_tag']) : '';
$languageSelect = isset($_REQUEST['course_language']) ? Security::remove_XSS($_REQUEST['course_language']) : '';
if ($action === 'subscribe_user_with_password') {

View File

@@ -979,6 +979,7 @@ class PortfolioController
{
$listByUser = false;
$listHighlighted = $httpRequest->query->has('list_highlighted');
$listAlphabetical = $httpRequest->query->has('list_alphabetical');
if ($httpRequest->query->has('user')) {
$this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
@@ -1041,7 +1042,7 @@ class PortfolioController
$portfolio = [];
if ($this->course) {
$frmTagList = $this->createFormTagFilter($listByUser);
$frmStudentList = $this->createFormStudentFilter($listByUser, $listHighlighted);
$frmStudentList = $this->createFormStudentFilter($listByUser, $listHighlighted, $listAlphabetical);
$frmStudentList->setDefaults(['user' => $this->owner->getId()]);
// it translates the category title with the current user language
$categories = $this->getCategoriesForIndex(null, 0);
@@ -1061,7 +1062,7 @@ class PortfolioController
if ($listHighlighted) {
$items = $this->getHighlightedItems();
} else {
$items = $this->getItemsForIndex($listByUser, $frmTagList);
$items = $this->getItemsForIndex($listByUser, $frmTagList, $listAlphabetical);
$foundComments = $this->getCommentsForIndex($frmTagList);
}
@@ -1086,6 +1087,7 @@ class PortfolioController
$template = new Template(null, false, false, false, false, false, false);
$template->assign('user', $this->owner);
$template->assign('listByUser', $listByUser);
$template->assign('course', $this->course);
$template->assign('session', $this->session);
$template->assign('portfolio', $portfolio);
@@ -1132,7 +1134,7 @@ class PortfolioController
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\TransactionRequiredException
*/
public function view(Portfolio $item)
public function view(Portfolio $item, $urlUser)
{
global $interbreadcrumb;
@@ -1203,12 +1205,18 @@ class PortfolioController
;
}
if (true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions')
&& $this->session && !$item->getSession() && !$item->isDuplicatedInSession($this->session)
) {
$comments = [];
} else {
$comments = $commentsQueryBuilder
->orderBy('comment.root, comment.lft', 'ASC')
->setParameter('item', $item)
->getQuery()
->getArrayResult()
;
}
$clockIcon = Display::returnFontAwesomeIcon('clock-o', '', true);
@@ -1408,10 +1416,15 @@ class PortfolioController
$this->baseUrl.http_build_query(['action' => 'edit_item', 'id' => $item->getId()])
);
$urlUserString = "";
if (isset($urlUser)) {
$urlUserString = "user=".$urlUser;
}
$actions = [];
$actions[] = Display::url(
Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
$this->baseUrl
$this->baseUrl.$urlUserString
);
if ($this->itemBelongToOwner($item)) {
@@ -3600,7 +3613,7 @@ class PortfolioController
/**
* @throws Exception
*/
private function createFormStudentFilter(bool $listByUser = false, bool $listHighlighted = false): FormValidator
private function createFormStudentFilter(bool $listByUser = false, bool $listHighlighted = false, bool $listAlphabeticalOrder = false): FormValidator
{
$frmStudentList = new FormValidator(
'frm_student_list',
@@ -3669,6 +3682,22 @@ class PortfolioController
$frmStudentList->addHtml("<p>$link</p>");
if (true !== api_get_configuration_value('portfolio_order_post_by_alphabetical_order')) {
if ($listAlphabeticalOrder) {
$link = Display::url(
get_lang('BackToDateOrder'),
$this->baseUrl
);
} else {
$link = Display::url(
get_lang('SeeAlphabeticalOrder'),
$this->baseUrl.http_build_query(['list_alphabetical' => true])
);
}
$frmStudentList->addHtml("<p>$link</p>");
}
return $frmStudentList;
}
@@ -3757,11 +3786,15 @@ class PortfolioController
private function getItemsForIndex(
bool $listByUser = false,
FormValidator $frmFilterList = null
FormValidator $frmFilterList = null,
bool $alphabeticalOrder = false
) {
$currentUserId = api_get_user_id();
if ($this->course) {
$showBaseContentInSession = $this->session
&& true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions');
$queryBuilder = $this->em->createQueryBuilder();
$queryBuilder
->select('pi')
@@ -3771,7 +3804,9 @@ class PortfolioController
$queryBuilder->setParameter('course', $this->course);
if ($this->session) {
$queryBuilder->andWhere('pi.session = :session');
$queryBuilder->andWhere(
$showBaseContentInSession ? 'pi.session = :session OR pi.session IS NULL' : 'pi.session = :session'
);
$queryBuilder->setParameter('session', $this->session);
} else {
$queryBuilder->andWhere('pi.session IS NULL');
@@ -3891,9 +3926,22 @@ class PortfolioController
}
$queryBuilder->setParameter('current_user', $currentUserId);
if ($alphabeticalOrder || true === api_get_configuration_value('portfolio_order_post_by_alphabetical_order')) {
$queryBuilder->orderBy('pi.title', 'ASC');
} else {
$queryBuilder->orderBy('pi.creationDate', 'DESC');
}
$items = $queryBuilder->getQuery()->getResult();
if ($showBaseContentInSession) {
$items = array_filter(
$items,
fn (Portfolio $item) => !($this->session && !$item->getSession() && $item->isDuplicatedInSession($this->session))
);
}
return $items;
} else {
$itemsCriteria = [];
$itemsCriteria['category'] = null;
@@ -3954,6 +4002,20 @@ class PortfolioController
$form->addButtonSave(get_lang('Save'));
if ($form->validate()) {
if ($this->session
&& true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions')
&& !$item->getSession()
) {
$duplicate = $item->duplicateInSession($this->session);
$this->em->persist($duplicate);
$this->em->flush();
$item = $duplicate;
$formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
}
$values = $form->exportValues();
$parentComment = $this->em->find(PortfolioComment::class, $values['parent']);

View File

@@ -112,6 +112,26 @@ class ScheduledAnnouncement extends Model
$form->addHidden('session_id', $sessionInfo['id']);
$form->addDateTimePicker('date', get_lang('Date'));
$useBaseProgress = api_get_configuration_value('scheduled_announcements_use_base_progress');
if ($useBaseProgress) {
$extraFieldValue = new ExtraFieldValue('scheduled_announcement');
$baseProgress = $extraFieldValue->get_values_by_handler_and_field_variable(
$id,
'use_base_progress'
);
$form->addNumeric('progress',
get_lang('Progress'),
[
'step' => 1,
'min' => 1,
'max' => 100,
'value' => $baseProgress['value'],
],
true
);
}
$form->addText('subject', get_lang('Subject'));
$form->addHtmlEditor('message', get_lang('Message'));
@@ -185,6 +205,11 @@ class ScheduledAnnouncement extends Model
$typeOptions['base_date'] = get_lang('BaseDate');
}
$useBaseProgress = api_get_configuration_value('scheduled_announcements_use_base_progress');
if ($useBaseProgress) {
$typeOptions['base_progress'] = get_lang('Progress');
}
$form->addSelect(
'type',
get_lang('Type'),
@@ -194,9 +219,15 @@ class ScheduledAnnouncement extends Model
if (this.options[this.selectedIndex].value == 'base_date') {
document.getElementById('options').style.display = 'block';
document.getElementById('specific_date').style.display = 'none';
} else {
document.getElementById('base_progress').style.display = 'none';
} else if (this.options[this.selectedIndex].value == 'specific_date') {
document.getElementById('options').style.display = 'none';
document.getElementById('specific_date').style.display = 'block';
document.getElementById('base_progress').style.display = 'none';
} else {
document.getElementById('options').style.display = 'block';
document.getElementById('specific_date').style.display = 'none';
document.getElementById('base_progress').style.display = 'block';
}
", ]
);
@@ -204,6 +235,20 @@ class ScheduledAnnouncement extends Model
$form->addHtml('<div id="specific_date">');
$form->addDateTimePicker('date', get_lang('Date'));
$form->addHtml('</div>');
$form->addHtml('<div id="base_progress" style="display:none">');
$form->addNumeric('progress',
get_lang('Progress'),
[
'step' => 1,
'min' => 1,
'max' => 100,
'value' => 100,
],
true
);
$form->addHtml('</div>');
$form->addHtml('<div id="options" style="display:none">');
$startDate = $sessionInfo['access_start_date'];
@@ -344,7 +389,19 @@ class ScheduledAnnouncement extends Model
$coachList = array_unique($coachList);
}
$useBaseProgress = api_get_configuration_value('scheduled_announcements_use_base_progress');
if ($useBaseProgress) {
$baseProgress = $extraFieldValue->get_values_by_handler_and_field_variable(
$result['id'],
'use_base_progress'
);
if (empty($baseProgress) || empty($baseProgress['value']) || $baseProgress['value'] < 1) {
$this->update(['id' => $result['id'], 'sent' => 1]);
}
} else {
$this->update(['id' => $result['id'], 'sent' => 1]);
}
$attachments = $this->getAttachmentToString($result['id']);
$subject = $result['subject'];
@@ -371,6 +428,44 @@ class ScheduledAnnouncement extends Model
);
}
if ($useBaseProgress) {
$baseProgress = $extraFieldValue->get_values_by_handler_and_field_variable(
$result['id'],
'use_base_progress'
);
if (!empty($baseProgress) && !empty($baseProgress['value']) && $baseProgress['value'] >= 1) {
if ((is_numeric($progress) && $progress > $baseProgress['value']) || !is_numeric($progress)) {
continue;
} else {
$comment = json_decode($baseProgress['comment'], true);
if ($comment !== null && is_array($comment)) {
if (isset($comment['sended']) && is_array($comment['sended'])) {
$userFound = false;
foreach ($comment['sended'] as $item) {
if (isset($item['user']) && $item['user'] === $user['user_id']) {
$userFound = true;
break;
}
}
if ($userFound) {
continue;
} else {
$comment['sended'][] = ['user' => $user['user_id'], 'send_date' => time(), 'progress_user' => $progress, 'progress_mark' => $baseProgress['value']];
$newExtraFieldParams = $baseProgress;
$newExtraFieldParams['comment'] = json_encode($comment);
$extraFieldValue->save($newExtraFieldParams);
}
}
} else {
$comment['sended'][] = ['user' => $user['user_id'], 'send_date' => time(), 'progress_user' => $progress, 'progress_mark' => $baseProgress['value']];
$newExtraFieldParams = $baseProgress;
$newExtraFieldParams['comment'] = json_encode($comment);
$extraFieldValue->save($newExtraFieldParams);
}
}
}
}
if (is_numeric($progress)) {
$progress = $progress.'%';
} else {

View File

@@ -774,6 +774,42 @@ class TicketManager
}
}
public static function deleteTicket($ticketId)
{
$ticketId = (int) $ticketId;
if ($ticketId <= 0) {
return false;
}
$table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
$table_ticket_message = Database::get_main_table('ticket_message');
$table_ticket_assigned_log = Database::get_main_table('ticket_assigned_log');
$table_ticket_message_attachments = Database::get_main_table('ticket_message_attachments');
$sql_get_message_ids = "SELECT id FROM $table_ticket_message WHERE ticket_id = $ticketId";
$sql_delete_attachments = "DELETE FROM $table_ticket_message_attachments WHERE message_id IN ($sql_get_message_ids)";
Database::query($sql_delete_attachments);
$sql_assigned_log = "DELETE FROM $table_ticket_assigned_log WHERE ticket_id = $ticketId";
Database::query($sql_assigned_log);
$sql_messages = "DELETE FROM $table_ticket_message WHERE ticket_id = $ticketId";
Database::query($sql_messages);
$sql_get_category = "SELECT category_id FROM $table_support_tickets WHERE id = $ticketId";
$res = Database::query($sql_get_category);
if ($row = Database::fetch_array($res)) {
$category_id = (int)$row['category_id'];
$table_ticket_category = Database::get_main_table('ticket_category');
$sql_update_category = "UPDATE $table_ticket_category SET total_tickets = total_tickets - 1 WHERE id = $category_id AND total_tickets > 0";
Database::query($sql_update_category);
}
$sql_ticket = "DELETE FROM $table_support_tickets WHERE id = $ticketId";
Database::query($sql_ticket);
return true;
}
/**
* Get tickets by userId.
*
@@ -801,7 +837,7 @@ class TicketManager
if (empty($userInfo)) {
return [];
}
$isAdmin = UserManager::is_admin($userId);
$isAdmin = UserManager::is_admin($userId) || (api_get_configuration_value('allow_session_admin_manage_tickets_and_export_ticket_report') && api_is_session_admin($userId));
if (!isset($_GET['project_id'])) {
return [];
@@ -893,6 +929,7 @@ class TicketManager
'keyword_source' => 'ticket.source ',
'keyword_status' => 'ticket.status_id',
'keyword_priority' => 'ticket.priority_id',
'keyword_created_by' => 'ticket.sys_insert_user_id',
];
foreach ($keywords as $keyword => $label) {
@@ -1007,6 +1044,14 @@ class TicketManager
<div class="blackboard_hide" id="div_'.$row['ticket_id'].'">&nbsp;&nbsp;</div>
</a>&nbsp;&nbsp;';
}
if ($isAdmin) {
$project_id = isset($row['project_id']) ? $row['project_id'] : (isset($_GET['project_id']) ? $_GET['project_id'] : 0);
$delete_link = '<a href="tickets.php?action=delete&ticket_id='.$row['ticket_id'].'&project_id='.$project_id.'" onclick="return confirm(\''.htmlentities(get_lang('AreYouSureYouWantToDeleteThisTicket')).'\')">'
. Display::return_icon('delete.png', get_lang('Delete')) .
'</a>';
$ticket[] = $delete_link;
}
$tickets[] = $ticket;
}
@@ -1079,10 +1124,11 @@ class TicketManager
'keyword_source' => 'ticket.source',
'keyword_status' => 'ticket.status_id',
'keyword_priority' => 'ticket.priority_id',
'keyword_created_by' => 'ticket.sys_insert_user_id',
];
foreach ($keywords as $keyword => $sqlLabel) {
if (isset($_GET[$keyword])) {
if (!empty($_GET[$keyword])) {
$data = Database::escape_string(trim($_GET[$keyword]));
$sql .= " AND $sqlLabel = '$data' ";
}

View File

@@ -284,6 +284,8 @@ class TrackingCourseLog
$row[4] = $ip;
}
$row[5] = Security::remove_XSS($row[5]);
$resources[] = $row;
}
}
@@ -949,6 +951,10 @@ class TrackingCourseLog
$userRow['lp_finalization_date'] = $user['lp_finalization_date'];
$userRow['quiz_finalization_date'] = $user['quiz_finalization_date'];
if (api_get_setting('show_email_addresses') === 'true') {
$userRow['email'] = $user['col4'];
}
// we need to display an additional profile field
if (isset($_GET['additional_profile_field'])) {
$data = Session::read('additional_user_profile_info');
@@ -989,10 +995,6 @@ class TrackingCourseLog
}
}
if (api_get_setting('show_email_addresses') === 'true') {
$userRow['email'] = $user['col4'];
}
$userRow['link'] = $user['link'];
if ($GLOBALS['export_csv']) {

View File

@@ -1,8 +1,6 @@
<?php
/* For licensing terms, see /license.txt */
use Brumann\Polyfill\Unserialize;
/**
* Class UnserializeApi.
*/
@@ -109,13 +107,13 @@ class UnserializeApi
}
if ($ignoreErrors) {
return @Unserialize::unserialize(
return @unserialize(
$serialized,
['allowed_classes' => $allowedClasses]
);
}
return Unserialize::unserialize(
return unserialize(
$serialized,
['allowed_classes' => $allowedClasses]
);

View File

@@ -3032,7 +3032,34 @@ class Agenda
);
$form->addElement('hidden', 'to', 'true');
} else {
$sendTo = isset($params['send_to']) ? $params['send_to'] : ['everyone' => true];
$defaultSendTo = ['everyone' => true];
if (api_get_configuration_value('course_agenda_set_default_send_to_with_none')) {
$defaultSendTo = ['everyone' => false];
}
$defaultSendToCurrentUser = '';
if (api_get_configuration_value('course_agenda_set_default_send_to_with_current_user')) {
$defaultSendToCurrentUser = api_get_user_id();
}
if (api_get_configuration_value('course_agenda_set_default_send_to_with_teachers')) {
$currentCourseInfo = api_get_course_info();
$sessionId = api_get_session_id();
if ($sessionId) {
$usersSendTo = SessionManager::getCoachesByCourseSession($sessionId, $currentCourseInfo['real_id']);
} else {
$courseTeachers = CourseManager::get_teacher_list_from_course_code($currentCourseInfo['code']);
$courseTeachersUid = [];
foreach ($courseTeachers as $courseTeacher) {
$courseTeachersUid[] = $courseTeacher['user_id'];
}
$usersSendTo = $courseTeachersUid;
}
$usersSendTo[] = $defaultSendToCurrentUser;
$defaultSendTo = ['users' => $usersSendTo];
} elseif ($defaultSendToCurrentUser) {
$defaultSendTo = ['users' => [$defaultSendToCurrentUser]];
}
$sendTo = isset($params['send_to']) ? $params['send_to'] : $defaultSendTo;
if ($this->type == 'course') {
$this->showToForm($form, $sendTo, [], false, true);
}

View File

@@ -544,6 +544,7 @@ define('HOT_SPOT_COMBINATION', 26);
define('FILL_IN_BLANKS_COMBINATION', 27);
define('MULTIPLE_ANSWER_DROPDOWN_COMBINATION', 28);
define('MULTIPLE_ANSWER_DROPDOWN', 29);
define('ANSWER_IN_OFFICE_DOC', 30);
define('EXERCISE_CATEGORY_RANDOM_SHUFFLED', 1);
define('EXERCISE_CATEGORY_RANDOM_ORDERED', 2);
@@ -591,6 +592,7 @@ define(
MULTIPLE_ANSWER_TRUE_FALSE.':'.
MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE.':'.
ORAL_EXPRESSION.':'.
ANSWER_IN_OFFICE_DOC.':'.
GLOBAL_MULTIPLE_ANSWER.':'.
MEDIA_QUESTION.':'.
CALCULATED_ANSWER.':'.
@@ -1449,7 +1451,7 @@ function api_protect_teacher_script()
function api_block_anonymous_users($printHeaders = true)
{
$user = api_get_user_info();
if (!(isset($user['user_id']) && $user['user_id']) || api_is_anonymous($user['user_id'], true)) {
if (empty($user['user_id']) || api_is_anonymous($user['user_id'], true)) {
api_not_allowed($printHeaders);
return false;
@@ -2957,7 +2959,7 @@ function api_get_session_visibility(
$totalDuration = $firstAccess + $duration + $userDuration;
return $totalDuration > $currentTime ? SESSION_AVAILABLE : SESSION_VISIBLE_READ_ONLY;
return $totalDuration > $currentTime ? SESSION_AVAILABLE : $visibility;
}
return SESSION_AVAILABLE;
@@ -4030,11 +4032,9 @@ function api_not_allowed(
global $this_section;
if (CustomPages::enabled() && !isset($user_id)) {
if (empty($user_id)) {
if (CustomPages::enabled() && (empty($user_id) || api_is_anonymous())) {
// Why the CustomPages::enabled() need to be to set the request_uri
$_SESSION['request_uri'] = $_SERVER['REQUEST_URI'];
}
CustomPages::display(CustomPages::INDEX_UNLOGGED);
}

View File

@@ -454,6 +454,8 @@ function return_navigation_array()
$navigation['follow_up']['icon'] = 'homepage.png';
}
buildParentCourseCategoriesMenu($navigation);
// Administration
if (api_is_platform_admin(true)) {
if (api_get_setting('show_tabs', 'platform_administration') == 'true') {
@@ -481,6 +483,8 @@ function return_navigation_array()
}
}
} else {
buildParentCourseCategoriesMenu($navigation);
// Show custom tabs that are specifically marked as public
$customTabs = getCustomTabs();
if (!empty($customTabs)) {
@@ -508,6 +512,57 @@ function return_navigation_array()
];
}
function buildParentCourseCategoriesMenu(array &$navigation)
{
if (!api_get_configuration_value('display_menu_use_course_categories')
|| 'true' !== api_get_setting('course_catalog_published')
) {
return;
}
foreach (CourseCategory::getCategoriesToDisplayInHomePage() as $category) {
$key = 'category_'.$category['code'];
$navigation[$key] = [
'url' => '#',
'title' => $category['name'],
'key' => $key,
'items' => buildChildrenCourseCategoriesMenu($category['code']),
];
}
}
function buildChildrenCourseCategoriesMenu($parentCode = 0): array
{
$baseCategoryUrl = api_get_path(WEB_CODE_PATH).'auth/courses.php?';
$commonParams = [
'search_term' => '',
'submit' => '_qf__s',
];
$items = [];
foreach (CourseCategory::getChildren($parentCode, false) as $category) {
$commonParams['category_code'] = $category['code'];
$categoryItem = [
'title' => $category['name'],
'key' => 'category_'.$category['code'],
'url' => $baseCategoryUrl.http_build_query($commonParams),
];
$children = buildChildrenCourseCategoriesMenu($category['code']);
if (!empty($children)) {
$categoryItem['items'] = $children;
}
$items[] = $categoryItem;
}
return $items;
}
/**
* Return the navigation menu elements as a flat array.
*

View File

@@ -518,7 +518,7 @@ class Certificate extends Model
$qrCode->setSize(120);
$qrCode->setMargin(5);
$qrCode->setWriterByName('png');
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::MEDIUM());
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::MEDIUM);
$qrCode->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0, 'a' => 0]);
$qrCode->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255, 'a' => 0]);
$qrCode->setValidateResult(false);

View File

@@ -398,24 +398,21 @@ class CourseCategory
return true;
}
/**
* @param string $categoryCode
*
* @return array
*/
public static function getChildren($categoryCode)
public static function getChildren(string $categoryCode, bool $getChildren = true): array
{
$table = Database::get_main_table(TABLE_MAIN_CATEGORY);
$categoryCode = Database::escape_string($categoryCode);
$sql = "SELECT code, id FROM $table
$sql = "SELECT name, code, id FROM $table
WHERE parent_id = '$categoryCode'";
$result = Database::query($sql);
$children = [];
while ($row = Database::fetch_array($result, 'ASSOC')) {
$children[] = $row;
if ($getChildren) {
$subChildren = self::getChildren($row['code']);
$children = array_merge($children, $subChildren);
}
}
return $children;
}
@@ -591,7 +588,7 @@ class CourseCategory
public static function getCategoriesToDisplayInHomePage()
{
$table = Database::get_main_table(TABLE_MAIN_CATEGORY);
$sql = "SELECT name FROM $table
$sql = "SELECT name, code FROM $table
WHERE parent_id IS NULL
ORDER BY tree_pos";

View File

@@ -721,10 +721,10 @@ class CourseHome
}
break;
case 'lp_category.gif':
$lpCategory = self::getPublishedLpCategoryFromLink($temp_row['link']);
if ($showInvisibleLpsForStudents) {
$add = true;
} else {
$lpCategory = self::getPublishedLpCategoryFromLink($temp_row['link']);
$add = learnpath::categoryIsVisibleForStudent($lpCategory, $user);
}
@@ -1384,18 +1384,19 @@ class CourseHome
/**
* Get published learning path category from link inside course home.
*
* @param string $link
*
* @return CLpCategory
*/
public static function getPublishedLpCategoryFromLink($link)
public static function getPublishedLpCategoryFromLink(string $link): ?CLpCategory
{
$query = parse_url($link, PHP_URL_QUERY);
parse_str($query, $params);
$id = isset($params['id']) ? (int) $params['id'] : 0;
if (empty($id)) {
return null;
}
$em = Database::getManager();
/** @var CLpCategory $category */
/** @var ?CLpCategory $category */
$category = $em->find('ChamiloCourseBundle:CLpCategory', $id);
return $category;

View File

@@ -843,4 +843,9 @@ class Database
{
return self::escape_string(preg_replace("/[^a-zA-Z0-9_.]/", '', $field));
}
public static function clearDatabaseName(string $dbName): string
{
return preg_replace('/[^a-zA-Z0-9_\-]/', '', $dbName);
}
}

View File

@@ -917,9 +917,10 @@ class Display
$attribute_list = '';
// Managing the additional attributes
if (!empty($additional_attributes) && is_array($additional_attributes)) {
$attribute_list = '';
foreach ($additional_attributes as $key => &$value) {
$attribute_list .= $key.'="'.$value.'" ';
$sanitized_key = htmlspecialchars($key, ENT_QUOTES, api_get_system_encoding());
$sanitized_value = htmlspecialchars($value, ENT_QUOTES, api_get_system_encoding());
$attribute_list .= $sanitized_key.'="'.$sanitized_value.'" ';
}
}
//some tags don't have this </XXX>
@@ -945,7 +946,6 @@ class Display
{
if (!empty($url)) {
$url = preg_replace('#&amp;#', '&', $url);
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
$attributes['href'] = $url;
}

View File

@@ -360,7 +360,7 @@ class DocumentManager
$fixLinksHttpToHttps = false,
$extraHeaders = []
) {
session_write_close(); //we do not need write access to session anymore
session_write_close(); //we do not need to write access to session anymore
if (!is_file($full_file_name)) {
return false;
}
@@ -383,7 +383,7 @@ class DocumentManager
// Force the browser to save the file instead of opening it
if (isset($sendFileHeaders) &&
!empty($sendFileHeaders)) {
header("X-Sendfile: $filename");
header("X-Sendfile: $full_file_name");
}
header('Content-type: application/octet-stream');
@@ -1653,7 +1653,9 @@ class DocumentManager
$session_id &&
SessionManager::isSessionFollowedByDrh($session_id, $userId);
$hasAccess = api_is_allowed_in_course() || api_is_platform_admin() || $drhAccessContent;
$sessionAdminAccessContent = api_get_configuration_value('session_admins_access_all_content');
$hasAccess = api_is_allowed_in_course() || api_is_platform_admin() || $drhAccessContent || $sessionAdminAccessContent;
if (false === $hasAccess) {
return false;
@@ -1824,6 +1826,17 @@ class DocumentManager
// 4. Checking document visibility (i'm repeating the code in order to be more clear when reading ) - jm
if ($user_in_course) {
if (true === api_get_configuration_value('document_enable_accessible_from_date')) {
$extraFieldValue = new ExtraFieldValue('document');
$extraValue = $extraFieldValue->get_values_by_handler_and_field_variable($doc_id, 'accessible_from');
if (!empty($extraValue) && isset($extraValue['value'])) {
$now = new DateTime();
$accessibleDate = new DateTime($extraValue['value']);
if ($now < $accessibleDate) {
return false;
}
}
}
// 4.1 Checking document visibility for a Course
if ($session_id == 0) {
$item_info = api_get_item_property_info(
@@ -3291,6 +3304,8 @@ class DocumentManager
}
$sql = "SELECT SUM(size)
FROM (
SELECT ref, size
FROM $TABLE_ITEMPROPERTY AS props
INNER JOIN $TABLE_DOCUMENT AS docs
ON (docs.id = props.ref AND props.c_id = docs.c_id)
@@ -3298,9 +3313,18 @@ class DocumentManager
props.c_id = $course_id AND
docs.c_id = $course_id AND
props.tool = '".TOOL_DOCUMENT."' AND
props.visibility <> 2
props.ref not in (
SELECT ref
FROM $TABLE_ITEMPROPERTY as cip
WHERE
cip.c_id = $course_id AND
cip.tool = '".TOOL_DOCUMENT."' AND
cip.visibility = 2
)
$group_condition
$session_condition
GROUP BY props.ref
) AS table1
";
$result = Database::query($sql);
@@ -5479,7 +5503,13 @@ class DocumentManager
} else {
// For a "PDF Download" of the file.
$pdfPreview = null;
if ($ext != 'pdf' && !in_array($ext, $webODFList)) {
if (OnlyofficePlugin::create()->isEnabled() &&
OnlyofficePlugin::isExtensionAllowed($document_data['file_extension']) &&
method_exists('OnlyofficeTools', 'getPathToView')
) {
$url = OnlyofficeTools::getPathToView($document_data['id']);
} elseif ($ext != 'pdf' && !in_array($ext, $webODFList)) {
$url = $basePageUrl.'showinframes.php?'.$courseParams.'&id='.$document_data['id'];
} else {
$pdfPreview = Display::url(
@@ -6627,7 +6657,15 @@ class DocumentManager
docs.path LIKE '$path/%' AND
props.c_id = $course_id AND
props.tool = '$tool_document' AND
$visibility_rule
$visibility_rule AND
props.ref not in (
SELECT ref
FROM $table_itemproperty as cip
WHERE
cip.c_id = $course_id AND
cip.tool = '$tool_document' AND
cip.visibility = 2
)
$session_condition
GROUP BY ref
) as table1";

View File

@@ -3,6 +3,7 @@
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use ChamiloSession as Session;
@@ -113,7 +114,7 @@ class ExerciseLib
}
}
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, UPLOAD_ANSWER]) && $freeze) {
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC]) && $freeze) {
return '';
}
@@ -283,6 +284,22 @@ class ExerciseLib
}
$s .= $multipleForm->returnForm();
break;
case ANSWER_IN_OFFICE_DOC:
if ('true' === OnlyofficePlugin::create()->get('enable_onlyoffice_plugin')) {
global $exe_id;
if (!empty($objQuestionTmp->extra)) {
$fileUrl = api_get_course_path()."/exercises/onlyoffice/{$exerciseId}/{$questionId}/".$objQuestionTmp->extra;
$documentUrl = OnlyofficeTools::getPathToView($fileUrl, false, $exe_id, $questionId);
echo '<div class="office-doc-container">';
echo "<iframe src='{$documentUrl}' width='100%' height='600' style='border:none;'></iframe>";
echo '</div>';
} else {
echo '<p>'.get_lang('NoOfficeDocProvided').'</p>';
}
} else {
echo '<p>'.get_lang('OnlyOfficePluginRequired').'</p>';
}
break;
case ORAL_EXPRESSION:
// Add nanog
if (api_get_setting('enable_record_audio') === 'true') {
@@ -2575,7 +2592,7 @@ HOTSPOT;
FROM $TBL_EXERCISES_REL_QUESTION terq
LEFT JOIN $TBL_EXERCISES_QUESTION teq
ON terq.question_id = teq.iid
WHERE teq.type in (".FREE_ANSWER.", ".ORAL_EXPRESSION.", ".ANNOTATION.", ".UPLOAD_ANSWER.")
WHERE teq.type in (".FREE_ANSWER.", ".ORAL_EXPRESSION.", ".ANNOTATION.", ".UPLOAD_ANSWER.", ".ANSWER_IN_OFFICE_DOC.")
";
$resultExerciseIds = Database::query($sqlExercise);
@@ -5368,21 +5385,6 @@ EOT;
);
}
// Display text when test is finished #4074 and for LP #4227
// Allows to do a remove_XSS for end text result of exercise with
// user status COURSEMANAGERLOWSECURITY BT#20194
if (true === api_get_configuration_value('exercise_result_end_text_html_strict_filtering')) {
$endOfMessage = Security::remove_XSS($objExercise->getTextWhenFinished(), COURSEMANAGERLOWSECURITY);
} else {
$endOfMessage = Security::remove_XSS($objExercise->getTextWhenFinished());
}
if (!empty($endOfMessage)) {
echo Display::div(
$endOfMessage,
['id' => 'quiz_end_message']
);
}
$question_list_answers = [];
$category_list = [];
$loadChoiceFromSession = false;
@@ -5399,7 +5401,7 @@ EOT;
$exerciseResult = Session::read('exerciseResult');
$exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
$delineationResults = Session::read('hotspot_delineation_result');
$delineationResults = isset($delineationResults[$objExercise->iid]) ? $delineationResults[$objExercise->iid] : null;
$delineationResults = $delineationResults[$objExercise->iid] ?? null;
}
$countPendingQuestions = 0;
@@ -5414,8 +5416,8 @@ EOT;
$choice = null;
$delineationChoice = null;
if ($loadChoiceFromSession) {
$choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
$delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
$choice = $exerciseResult[$questionId] ?? null;
$delineationChoice = $delineationResults[$questionId] ?? null;
}
// We're inside *one* question. Go through each possible answer for this question
@@ -5555,7 +5557,7 @@ EOT;
if ($show_results) {
$score = $calculatedScore;
}
if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
$reviewScore = [
'score' => $my_total_score,
'comments' => Event::get_comments($exeId, $questionId),
@@ -5618,6 +5620,22 @@ EOT;
}
}
// Display text when test is finished #4074 and for LP #4227
// Allows to do a remove_XSS for end text result of exercise with
// user status COURSEMANAGERLOWSECURITY BT#20194
$finishMessage = $objExercise->getFinishText($total_score, $total_weight);
if (true === api_get_configuration_value('exercise_result_end_text_html_strict_filtering')) {
$endOfMessage = Security::remove_XSS($finishMessage, COURSEMANAGERLOWSECURITY);
} else {
$endOfMessage = Security::remove_XSS($finishMessage);
}
if (!empty($endOfMessage)) {
echo Display::div(
$endOfMessage,
['id' => 'quiz_end_message']
);
}
$totalScoreText = null;
$certificateBlock = '';
if (($show_results || $show_only_score) && $showTotalScore) {
@@ -5791,6 +5809,13 @@ EOT;
$total_weight
);
if ($save_user_result
&& !$passed
&& true === api_get_configuration_value('exercise_subscribe_session_when_finished_failure')
) {
self::subscribeSessionWhenFinishedFailure($objExercise->iid);
}
$percentage = 0;
if (!empty($total_weight)) {
$percentage = ($total_score / $total_weight) * 100;
@@ -5811,6 +5836,26 @@ EOT;
];
}
public static function getSessionWhenFinishedFailure(int $exerciseId): ?SessionEntity
{
$objExtraField = new ExtraField('exercise');
$objExtraFieldValue = new ExtraFieldValue('exercise');
$subsSessionWhenFailureField = $objExtraField->get_handler_field_info_by_field_variable(
'subscribe_session_when_finished_failure'
);
$subsSessionWhenFailureValue = $objExtraFieldValue->get_values_by_handler_and_field_id(
$exerciseId,
$subsSessionWhenFailureField['id']
);
if (!empty($subsSessionWhenFailureValue['value'])) {
return api_get_session_entity((int) $subsSessionWhenFailureValue['value']);
}
return null;
}
/**
* It validates unique score when all user answers are correct by question.
* It is used for global questions.
@@ -6390,6 +6435,7 @@ EOT;
READING_COMPREHENSION,
MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY,
UPLOAD_ANSWER,
ANSWER_IN_OFFICE_DOC,
MATCHING_COMBINATION,
FILL_IN_BLANKS_COMBINATION,
MULTIPLE_ANSWER_DROPDOWN,
@@ -7373,4 +7419,452 @@ EOT;
return false;
}
/**
* Get formatted feedback comments for an exam attempt.
*/
public static function getFeedbackComments(int $examId): string
{
$TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
$TBL_QUIZ_QUESTION = Database::get_course_table(TABLE_QUIZ_QUESTION);
$sql = "SELECT ta.question_id, ta.teacher_comment, q.question AS title
FROM $TBL_TRACK_ATTEMPT ta
INNER JOIN $TBL_QUIZ_QUESTION q ON ta.question_id = q.iid
WHERE ta.exe_id = $examId
AND ta.teacher_comment IS NOT NULL
AND ta.teacher_comment != ''
GROUP BY ta.question_id
ORDER BY q.position ASC, ta.id ASC";
$result = Database::query($sql);
$commentsByQuestion = [];
while ($row = Database::fetch_array($result)) {
$questionId = $row['question_id'];
$questionTitle = Security::remove_XSS($row['title']);
$comment = Security::remove_XSS(trim(strip_tags($row['teacher_comment'])));
if (!empty($comment)) {
if (!isset($commentsByQuestion[$questionId])) {
$commentsByQuestion[$questionId] = [
'title' => $questionTitle,
'comments' => [],
];
}
$commentsByQuestion[$questionId]['comments'][] = $comment;
}
}
if (empty($commentsByQuestion)) {
return "<p>".get_lang('NoAdditionalComments')."</p>";
}
$output = "<h3>".get_lang('TeacherFeedback')."</h3>";
$output .= "<table border='1' cellpadding='5' cellspacing='0' width='100%' style='border-collapse: collapse;'>";
foreach ($commentsByQuestion as $questionId => $data) {
$output .= "<tr>
<td><b>".get_lang('Question')." #$questionId:</b> ".$data['title']."</td>
</tr>";
foreach ($data['comments'] as $comment) {
$output .= "<tr>
<td style='padding-left: 20px;'><i>".get_lang('Feedback').":</i> $comment</td>
</tr>";
}
}
$output .= "</table>";
return $output;
}
public static function replaceTermsInContent(string $search, string $replace): array
{
$replacements = [
Database::get_course_table(TABLE_QUIZ_TEST) => [
'iid' => ['title', 'description', 'sound'],
],
Database::get_course_table(TABLE_QUIZ_QUESTION) => [
'iid' => ['question', 'description'],
],
Database::get_course_table(TABLE_QUIZ_ANSWER) => [
'iid' => ['answer', 'comment'],
],
Database::get_course_table(TABLE_ANNOUNCEMENT) => [
'iid' => ['title', 'content'],
],
Database::get_course_table(TABLE_ANNOUNCEMENT_ATTACHMENT) => [
'iid' => ['path', 'comment'],
],
Database::get_course_table(TABLE_ATTENDANCE) => [
'iid' => ['name', 'description', 'attendance_qualify_title'],
],
Database::get_course_table(TABLE_BLOGS) => [
'iid' => ['blog_name', 'blog_subtitle'],
],
Database::get_course_table(TABLE_BLOGS_ATTACHMENT) => [
'iid' => ['path', 'comment'],
],
Database::get_course_table(TABLE_BLOGS_COMMENTS) => [
'iid' => ['title', 'comment'],
],
Database::get_course_table(TABLE_BLOGS_POSTS) => [
'iid' => ['title', 'full_text'],
],
Database::get_course_table(TABLE_BLOGS_TASKS) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_AGENDA) => [
'iid' => ['title', 'content', 'comment'],
],
Database::get_course_table(TABLE_AGENDA_ATTACHMENT) => [
'iid' => ['path', 'comment', 'filename'],
],
Database::get_course_table(TABLE_COURSE_DESCRIPTION) => [
'iid' => ['title', 'content'],
],
Database::get_course_table(TABLE_DOCUMENT) => [
'iid' => ['path', 'comment'],
],
Database::get_course_table(TABLE_DROPBOX_FEEDBACK) => [
'iid' => ['feedback'],
],
Database::get_course_table(TABLE_DROPBOX_FILE) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_DROPBOX_POST) => [
'iid' => ['feedback'],
],
Database::get_course_table(TABLE_FORUM_ATTACHMENT) => [
'iid' => ['path', 'comment', 'filename'],
],
Database::get_course_table(TABLE_FORUM_CATEGORY) => [
'iid' => ['cat_title', 'cat_comment'],
],
Database::get_course_table(TABLE_FORUM) => [
'iid' => ['forum_title', 'forum_comment', 'forum_image'],
],
Database::get_course_table(TABLE_FORUM_POST) => [
'iid' => ['post_title', 'post_text', 'poster_name'],
],
Database::get_course_table(TABLE_FORUM_THREAD) => [
'iid' => ['thread_title', 'thread_poster_name', 'thread_title_qualify'],
],
Database::get_course_table(TABLE_GLOSSARY) => [
'iid' => ['name', 'description'],
],
Database::get_course_table(TABLE_GROUP_CATEGORY) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_GROUP) => [
'iid' => ['name', 'description', 'secret_directory'],
],
Database::get_course_table(TABLE_LINK) => [
'iid' => ['description'],
],
Database::get_course_table(TABLE_LINK_CATEGORY) => [
'iid' => ['category_title', 'description'],
],
Database::get_course_table(TABLE_LP_MAIN) => [
'iid' => ['name', 'ref', 'description', 'path', 'content_license', 'preview_image', 'theme'],
],
Database::get_course_table(TABLE_LP_CATEGORY) => [
'iid' => ['name'],
],
Database::get_course_table(TABLE_LP_ITEM) => [
'iid' => ['prerequisite', 'description', 'title', 'parameters', 'launch_data', 'terms'],
],
Database::get_course_table(TABLE_LP_ITEM_VIEW) => [
'iid' => ['suspend_data', 'lesson_location'],
],
Database::get_course_table(TABLE_NOTEBOOK) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_ONLINE_LINK) => [
'iid' => ['name'],
],
Database::get_course_table(TABLE_QUIZ_QUESTION_CATEGORY) => [
'iid' => ['title', 'description'],
],
Database::get_course_table(TABLE_ROLE) => [
'iid' => ['role_name', 'role_comment'],
],
Database::get_course_table(TABLE_STUDENT_PUBLICATION) => [
'iid' => ['title', 'title_correction', 'description'],
],
Database::get_course_table(TABLE_STUDENT_PUBLICATION_ASSIGNMENT_COMMENT) => [
'iid' => ['comment', 'file'],
],
Database::get_course_table(TABLE_SURVEY) => [
'iid' => ['title', 'subtitle', 'surveythanks', 'invite_mail', 'reminder_mail', 'mail_subject', 'access_condition', 'form_fields'],
],
Database::get_course_table(TABLE_SURVEY_QUESTION_GROUP) => [
'iid' => ['name', 'description'],
],
Database::get_course_table(TABLE_SURVEY_QUESTION) => [
'iid' => ['survey_question', 'survey_question_comment'],
],
Database::get_course_table(TABLE_SURVEY_QUESTION_OPTION) => [
'iid' => ['option_text'],
],
Database::get_course_table(TABLE_THEMATIC) => [
'iid' => ['content', 'title'],
],
Database::get_course_table(TABLE_THEMATIC_ADVANCE) => [
'iid' => ['content'],
],
Database::get_course_table(TABLE_THEMATIC_PLAN) => [
'iid' => ['description'],
],
Database::get_course_table(TABLE_TOOL_LIST) => [
'iid' => ['description'],
],
Database::get_course_table(TABLE_TOOL_INTRO) => [
'iid' => ['intro_text'],
],
Database::get_course_table(TABLE_USER_INFO_DEF) => [
'iid' => ['comment'],
],
Database::get_course_table(TABLE_WIKI) => [
'iid' => ['title', 'content', 'comment', 'progress', 'linksto'],
],
Database::get_course_table(TABLE_WIKI_CONF) => [
'iid' => ['feedback1', 'feedback2', 'feedback3'],
],
Database::get_course_table(TABLE_WIKI_DISCUSS) => [
'iid' => ['comment'],
],
Database::get_main_table(TABLE_CAREER) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_CHAT) => [
'id' => ['message'],
],
Database::get_main_table(TABLE_MAIN_CLASS) => [
'id' => ['name'],
],
Database::get_main_table(TABLE_MAIN_COURSE_REQUEST) => [
'id' => ['description', 'title', 'objetives', 'target_audience'],
],
'course_type' => [
'id' => ['description'],
],
Database::get_main_table(TABLE_EVENT_EMAIL_TEMPLATE) => [
'id' => ['message', 'subject', 'event_type_name'],
],
Database::get_main_table(TABLE_GRADE_MODEL) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE) => [
'id' => ['path_certificate'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_EVALUATION) => [
'id' => ['description', 'name'],
],
Database::get_main_table(TABLE_MAIN_GRADEBOOK_LINKEVAL_LOG) => [
'id' => ['name', 'description'],
],
Database::get_main_table(TABLE_MAIN_LEGAL) => [
'id' => ['content', 'changes'],
],
Database::get_main_table(TABLE_MESSAGE) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_MESSAGE_ATTACHMENT) => [
'id' => ['path', 'comment', 'filename'],
],
Database::get_main_table(TABLE_NOTIFICATION) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_PERSONAL_AGENDA) => [
'id' => ['title', 'text'],
],
Database::get_main_table(TABLE_PROMOTION) => [
'id' => ['description'],
],
'room' => [
'id' => ['description'],
],
'sequence_condition' => [
'id' => ['description'],
],
'sequence_method' => [
'id' => ['description', 'formula'],
],
'sequence_rule' => [
'id' => ['description'],
],
'sequence_type_entity' => [
'id' => ['description'],
],
'sequence_variable' => [
'id' => ['description'],
],
Database::get_main_table(TABLE_MAIN_SESSION) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_MAIN_SHARED_SURVEY) => [
'survey_id' => ['subtitle', 'surveythanks', 'intro'],
],
Database::get_main_table(TABLE_MAIN_SHARED_SURVEY_QUESTION) => [
'question_id' => ['survey_question', 'survey_question_comment'],
],
Database::get_main_table(TABLE_MAIN_SHARED_SURVEY_QUESTION_OPTION) => [
'question_option_id' => ['option_text'],
],
Database::get_main_table(TABLE_MAIN_SKILL) => [
'id' => ['name', 'description', 'criteria'],
],
Database::get_main_table(TABLE_MAIN_SKILL_PROFILE) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_MAIN_SKILL_REL_USER) => [
'id' => ['argumentation'],
],
'skill_rel_user_comment' => [
'id' => ['feedback_text'],
],
Database::get_main_table(TABLE_MAIN_SYSTEM_ANNOUNCEMENTS) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_MAIN_SYSTEM_CALENDAR) => [
'id' => ['content'],
],
Database::get_main_table(TABLE_MAIN_SYSTEM_TEMPLATE) => [
'id' => ['comment', 'content'],
],
Database::get_main_table(TABLE_MAIN_TEMPLATES) => [
'id' => ['description', 'image'],
],
Database::get_main_table(TABLE_TICKET_CATEGORY) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_MESSAGE) => [
'id' => ['message'],
],
Database::get_main_table(TABLE_TICKET_MESSAGE_ATTACHMENTS) => [
'id' => ['filename', 'path'],
],
Database::get_main_table(TABLE_TICKET_PRIORITY) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_PROJECT) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_STATUS) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_TICKET_TICKET) => [
'id' => ['message'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT) => [
'id' => ['answer', 'teacher_comment', 'filename'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING) => [
'id' => ['teacher_comment'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_DEFAULT) => [
'default_id' => ['default_value'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES) => [
'exe_id' => ['data_tracking', 'questions_to_check'],
],
Database::get_main_table(TABLE_STATISTIC_TRACK_E_ITEM_PROPERTY) => [
'id' => ['content'],
],
'track_e_open' => [
'open_id' => ['open_remote_host', 'open_agent', 'open_referer'],
],
Database::get_main_table(TABLE_TRACK_STORED_VALUES) => [
'id' => ['sv_value'],
],
Database::get_main_table(TABLE_TRACK_STORED_VALUES_STACK) => [
'id' => ['sv_value'],
],
Database::get_main_table(TABLE_MAIN_USER_API_KEY) => [
'id' => ['description'],
],
Database::get_main_table(TABLE_USERGROUP) => [
'id' => ['name', 'description', 'picture', 'url'],
],
Database::get_main_table(TABLE_MAIN_BLOCK) => [
'id' => ['name', 'description', 'path'],
],
];
if (api_get_configuration_value('attendance_allow_comments')) {
$replacements['c_attendance_result_comment'] = [
'iid' => ['comment'],
];
}
if (api_get_configuration_value('exercise_text_when_finished_failure')) {
$replacements[Database::get_course_table(TABLE_QUIZ_TEST)]['iid'][] = 'text_when_finished_failure';
}
$changes = array_map(
fn ($table) => 0,
$replacements
);
foreach ($replacements as $table => $replacement) {
foreach ($replacement as $idColumn => $columns) {
$keys = array_map(fn ($column) => "$column LIKE %?%", $columns);
$values = array_fill(0, count($columns), $search);
$result = Database::select(
[$idColumn, ...$columns],
$table,
[
'where' => [
implode(' OR ', $keys) => $values,
],
'order' => "$idColumn ASC",
]
);
foreach ($result as $row) {
$attributes = array_combine(
$columns,
array_map(
fn ($column) => preg_replace('#'.$search.'#', $replace, $row[$column]),
$columns
)
);
try {
Database::update(
$table,
$attributes,
["$idColumn = ?" => $row[$idColumn]]
);
} catch (Exception $e) {
Database::handleError($e);
}
$changes[$table]++;
}
}
}
return $changes;
}
private static function subscribeSessionWhenFinishedFailure(int $exerciseId): void
{
$failureSession = self::getSessionWhenFinishedFailure($exerciseId);
if ($failureSession) {
SessionManager::subscribeUsersToSession(
$failureSession->getId(),
[api_get_user_id()],
SESSION_VISIBLE_READ_ONLY,
false
);
}
}
}

View File

@@ -30,7 +30,8 @@ class ExerciseShowFunctions
$answer,
$feedbackType,
$resultsDisabled,
$showTotalScoreAndUserChoices
$showTotalScoreAndUserChoices,
$exercise
);
if (empty($id)) {
@@ -220,13 +221,19 @@ class ExerciseShowFunctions
echo '<tr><td>&nbsp;</td></tr>';
}
} else {
echo '<tr>';
echo '<td>';
if (!empty($answer)) {
echo Security::remove_XSS($answer);
$text = '';
if (is_array($answer) && !empty($answer['answer'])) {
$text = $answer['answer'];
} elseif (!is_array($answer)) {
$text = $answer;
}
if (!empty($text)) {
echo '<tr><td>';
echo Security::remove_XSS($text);
echo '</td></tr>';
}
echo '</td>';
echo '</tr>';
}
}
@@ -995,4 +1002,68 @@ class ExerciseShowFunctions
}
}
}
/**
* Displays the submitted OnlyOffice document in an iframe.
*
* @param string $feedbackType The feedback type of the exercise.
* @param int $exeId The execution ID.
* @param int $userId The user ID.
* @param int $exerciseId The exercise ID.
* @param int $questionId The question ID.
* @param int $questionScore Score assigned to the response.
* @param bool $autorefresh If true, auto-refresh the iframe after a short delay (used in result view).
*/
public static function displayOnlyOfficeAnswer(
string $feedbackType,
int $exeId,
int $userId,
int $exerciseId,
int $questionId,
int $questionScore = 0,
bool $autorefresh = false
): void {
$filePathPattern = api_get_path(SYS_COURSE_PATH).api_get_course_path()."/exercises/onlyoffice/{$exerciseId}/{$questionId}/{$userId}/response_{$exeId}.*";
$files = glob($filePathPattern);
if (!empty($files)) {
$fileUrl = api_get_course_path()."/exercises/onlyoffice/{$exerciseId}/{$questionId}/{$userId}/".basename($files[0]);
$iframeId = "onlyoffice_result_frame_{$exerciseId}_{$questionId}_{$exeId}_{$userId}";
$loaderId = "onlyoffice_loader_{$exerciseId}_{$questionId}_{$exeId}_{$userId}";
$iframeSrc = OnlyofficeTools::getPathToView($fileUrl, false, $exeId, $questionId, true);
$iframeSrc .= '&t='.time();
echo '
<tr>
<td>
<p><b>'.get_lang('SubmittedDocument').':</b></p>';
if ($autorefresh) {
echo '
<div id="'.$loaderId.'">
<p><em>'.get_lang('LoadingLatestVersion').'...</em></p>
</div>
<iframe id="'.$iframeId.'" src="'.$iframeSrc.'" width="100%" height="600px" style="border:none; display:none;"></iframe>';
echo "<script>
setTimeout(function() {
var iframe = document.getElementById('{$iframeId}');
var loader = document.getElementById('{$loaderId}');
if (iframe && loader) {
iframe.src = iframe.src + '&reload=' + new Date().getTime();
iframe.style.display = 'block';
loader.style.display = 'none';
}
}, 5000);
</script>";
} else {
echo '
<iframe id="'.$iframeId.'" src="'.$iframeSrc.'" width="100%" height="600px" style="border:none;"></iframe>';
}
echo '</td></tr>';
} else {
echo '<tr><td>'.get_lang('NoOfficeDocProvided').'</td></tr>';
}
if ($questionScore <= 0 && EXERCISE_FEEDBACK_TYPE_EXAM !== $feedbackType) {
echo '<tr><td>'.ExerciseLib::getNotCorrectedYetText().'</td></tr>';
}
}
}

View File

@@ -178,6 +178,9 @@ class ExtraField extends Model
case 'attendance_calendar':
$this->extraFieldType = EntityExtraField::ATTENDANCE_CALENDAR_TYPE;
break;
case 'attendance':
$this->extraFieldType = EntityExtraField::ATTENDANCE_TYPE;
break;
}
$this->pageUrl = 'extra_fields.php?type='.$this->type;
@@ -213,6 +216,7 @@ class ExtraField extends Model
'message',
'document',
'attendance_calendar',
'attendance',
];
if (api_get_configuration_value('allow_scheduled_announcements')) {
@@ -779,7 +783,6 @@ class ExtraField extends Model
$itemId = (int) $itemId;
$form->addHidden('item_id', $itemId);
$extraData = false;
if (!empty($itemId)) {
$extraData = $this->get_handler_extra_data($itemId);
if (!empty($showOnlyTheseFields)) {
@@ -1103,6 +1106,12 @@ class ExtraField extends Model
'extra_'.$field_details['variable'],
'html_filter'
);
if (!empty($field_details['default_value'])) {
$defaults['extra_'.$field_details['variable']] = $field_details['default_value'];
}
if (!isset($form->_defaultValues['extra_'.$field_details['variable']])) {
$form->setDefaults($defaults);
}
if ($freezeElement) {
$form->freeze('extra_'.$field_details['variable']);
}
@@ -1369,6 +1378,23 @@ class ExtraField extends Model
);
$selectedOptions[] = $tag['tag'];
}
} else {
if (!empty($extraData) && isset($extraData['extra_'.$field_details['variable']])) {
$data = $extraData['extra_'.$field_details['variable']];
if (!empty($data)) {
foreach ($data as $option) {
$tagsSelect->addOption(
$option,
$option,
[
'selected' => 'selected',
'class' => 'selected',
]
);
$selectedOptions[] = $option;
}
}
}
}
$url = api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php';
} else {

View File

@@ -963,11 +963,9 @@ class ExtraFieldValue extends Model
}
/**
* @param int $itemId
*
* @return array
* Return extra fields details for an item if the extra field is marked as filter.
*/
public function getAllValuesByItem($itemId)
public function getAllValuesByItem(int $itemId): array
{
$itemId = (int) $itemId;
$extraFieldType = $this->getExtraField()->getExtraFieldType();
@@ -983,9 +981,9 @@ class ExtraFieldValue extends Model
$result = Database::query($sql);
$idList = [];
$finalResult = [];
if (Database::num_rows($result)) {
$result = Database::store_result($result, 'ASSOC');
$finalResult = [];
foreach ($result as $item) {
$finalResult[$item['id']] = $item;
}

View File

@@ -2255,3 +2255,20 @@ function getFileUploadSizeLimitForTeacher()
return $size;
}
function processChunkedFile(array $file): array
{
if (isset($_REQUEST['chunkAction']) && 'done' === $_REQUEST['chunkAction']) {
// to rename and move the finished file
$tmpFile = disable_dangerous_file(
api_replace_dangerous_char($file['name'])
);
$chunkedFile = api_get_path(SYS_ARCHIVE_PATH).$tmpFile;
$file['tmp_name'] = $chunkedFile;
$file['size'] = filesize($chunkedFile);
$file['copy_file'] = true;
}
return $file;
}

View File

@@ -2,6 +2,7 @@
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Component\Editor\CkEditor\CkEditor;
use Chamilo\CoreBundle\Component\HTMLPurifier\Filter\RemoveOnAttributes;
/**
* A html editor field to use with QuickForm.
@@ -110,4 +111,9 @@ class HtmlEditor extends HTML_QuickForm_textarea
return $result;
}
public function getValue(): ?string
{
return RemoveOnAttributes::filter($this->_value);
}
}

Some files were not shown because too many files have changed in this diff Show More