1 Commits
Author SHA1 Message Date
Xes 73154ae174 Chamilo 1.11.40 (ZIP oficial v1.11.40)
Behat tests 1.11.x 🐞 / PHP 7.4 Test on ubuntu-latest (push) Canceled after 0s
PHP-CS-Fixer / composer_install (7.4) (push) Canceled after 0s
Fuente: https://github.com/chamilo/chamilo-lms/releases/download/v1.11.40/chamilo-1.11.40.zip
sha256: 1cf4bf2cc7bae1ef1a1eff643235db1d552f78ddf4b6dd1e2d2dac9868679439
Snapshot independiente (rama huerfana); diffable vs 1.11.38. vendor incluido.
2026-08-06 17:59:45 +02:00
26794 changed files with 374158 additions and 1832961 deletions
+12
View File
@@ -0,0 +1,12 @@
Before reporting, make sure you check our [official documentation](https://docs.chamilo.org).
If you are fairly sure you found an issue, please use the following template (remove things in other languages than your own and remove these 2 first lines).
### Current behavior / Resultado actual / Résultat actuel
### Expected behavior / Resultado esperado / Résultat attendu
### Steps to reproduce / Pasos para reproducir / Étapes pour reproduire
### Chamilo Version / Versión de Chamilo / Version de Chamilo
+196
View File
@@ -0,0 +1,196 @@
name: Behat tests 1.11.x 🐞
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
operating-system: [ubuntu-latest]
php-versions: ['7.4']
services:
mysql:
image: mysql:latest
env:
MYSQL_ALLOW_EMPTY_PASSWORD: false
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: chamilo
ports:
- 3306/tcp
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup apache
run: |
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt-get update
sudo apt-get install apache2 php${{ matrix.php-versions }} php${{ matrix.php-versions }}-common php${{ matrix.php-versions }}-cli libapache2-mod-php${{ matrix.php-versions }}
sudo a2enmod rewrite actions
sudo cp -f tests/travis/gh-apache /etc/apache2/sites-available/000-default.conf
sudo chmod 777 -R $HOME
cat /etc/apache2/sites-available/000-default.conf
sudo systemctl restart apache2
- name: Setup PHP, with composer and extensions
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, xml, ctype, iconv, intl, pdo, pdo_mysql, mysqli, dom, gd, json, soap, zip, bcmath
ini-values: post_max_size=256M, max_execution_time=600, memory_limit=4096M
tools: composer:v2
- name: Get composer cache directory
id: composer-cache-v1
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache-v1.outputs.dir }}
# Use composer.json for key, if composer.lock is not committed.
# key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
key: v1-${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: v1-${{ runner.os }}-composer-
- name: Install dependencies with composer
run: php7.4 /usr/local/bin/composer install --prefer-dist --no-progress
# - name: Install assets
# run: php bin/console assets:install
#
# - name: Generate fos_js_routes.json
# run: php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json
# - name: Get yarn cache directory
# id: yarn-cache
# run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
#
# - name: Cache yarn dependencies
# uses: actions/cache@v2
# with:
# path: ${{ steps.yarn-cache.outputs.dir }}
# # Use composer.json for key, if composer.lock is not committed.
# # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
# key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
# restore-keys: ${{ runner.os }}-yarn-
#
# - name: Yarn install
# uses: borales/actions-yarn@v2.3.0
# with:
# cmd: install # will run `yarn install` command
#
# - name: Yarn run encore dev
# uses: borales/actions-yarn@v2.3.0
# with:
# cmd: run encore dev
- name: Install chrome
uses: browser-actions/setup-chrome@v1
id: setup-chrome
- name: Start chrome
run: |
${{ steps.setup-chrome.outputs.chrome-path }} --version
${{ steps.setup-chrome.outputs.chrome-path }} --headless --disable-gpu --remote-debugging-port=9222 http://localhost &
# https://github.com/marketplace/actions/setup-chromedriver
- name: Start chromedriver
uses: nanasess/setup-chromedriver@master
with:
# Optional: do not specify to match Chrome's version
chromedriver-version: '100.0.4896.60'
- run: |
export DISPLAY=:99
chromedriver --url-base=/wd/hub &
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional
# wget https://chromedriver.storage.googleapis.com/88.0.4324.96/chromedriver_linux64.zip --quiet && unzip chromedriver_linux64.zip
# sudo mv chromedriver /usr/bin
# sudo chmod +x /usr/bin/chromedriver
# chromedriver --version
# - run: |
# export DISPLAY=:99
# chromedriver --url-base=/wd/hub &
# sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional
# chromedriver --version
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Get Selenium
run: |
wget http://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar --quiet
- name: Run Selenium
run: |
java -version
export DISPLAY=:99.0
sudo xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
java -Dwebdriver.chrome.driver=/usr/bin/chromedriver -jar selenium-server-standalone-3.141.59.jar -log selenium.log > /dev/null &
- name: Start mysql service
run: |
sudo /etc/init.d/mysql start
sudo systemctl restart apache2
#
# - name: Install database
# run: |
# bin/console doctrine:database:create || echo "Error while creating the DB"
# env:
# DATABASE_URL: mysql://root:root@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/chamilo
# DATABASE_HOST: 127.0.0.1
# DATABASE_PORT: ${{ job.services.mysql.ports['3306'] }}
# DATABASE_NAME: chamilo
# DATABASE_USER: root
# DATABASE_PASSWORD: root
# APP_INSTALLED: 1
- name: Check settings
run: |
php -ini
php -v
php -m
- name: Install chash
run: |
git clone https://github.com/chamilo/chash
cd chash
git checkout 0.2.x
php7.4 /usr/local/bin/composer install
php7.4 -d phar.readonly=0 createPhar.php
chmod +x chash.phar
sudo mv chash.phar /usr/local/bin/chash
- name: Install chamilo
run: |
php7.4 ${{ github.workspace }}/chash/chash.php chash:chamilo_install 1.11.x ${{ github.workspace }} --no-interaction --sitename="Chamilo" --site_url="http://localhost/" --institution="Chamilo" --institution_url="https://chamilo.org" --encrypt_method="sha1" --firstname="John" --lastname="Doe" --language="english" --driver="pdo_mysql" --host="localhost" --port="3306" --dbname="chamilo" --dbuser="root" --dbpassword="root" --permissions_for_new_directories="0777" --permissions_for_new_files="0666" --linux-user="www-data" --linux-group="www-data" --username="admin" --password="admin" --email="admin@example.com" --phone="555-5555"
php7.4 ${{ github.workspace }}/chash/chash.php chash:chamilo_status
sudo chmod -R 777 app/cache app/logs app/courses app/upload web
bash tests/travis/post_installation.sh
sudo cp -f tests/travis/gh-apache /etc/apache2/sites-available/000-default.conf
sudo systemctl restart apache2
curl http://localhost/index.php
- name: Behat tests
run: |
cd ${{ github.workspace }}/tests/behat
../../vendor/behat/behat/bin/behat -vvv
+54
View File
@@ -0,0 +1,54 @@
name: PHP-CS-Fixer
on: [ push, pull_request ]
permissions:
contents: read
jobs:
composer_install:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
php-versions: ['7.4']
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, xml, ctype, iconv, intl, pdo, pdo_mysql, dom, gd, json, soap, zip, bcmath
ini-values: post_max_size=256M, max_execution_time=600, memory_limit=4096M
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer downloads
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Run composer install
run: composer install --prefer-dist --no-progress
- name: Download PHP-CS-Fixer
run: |
wget https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v3.4.0/php-cs-fixer.phar
- name: Run PHP-CS-Fixer
run: |
IFS='
'
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB "HEAD~..HEAD")
if ! echo "${CHANGED_FILES}" | grep -qE "^(\\.php-cs-fixer(\\.dist)?\\.php|composer\\.lock)$"; then EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "${CHANGED_FILES}"); else EXTRA_ARGS=''; fi
php ./php-cs-fixer.phar fix --config=.php-cs-fixer.php -v --dry-run --using-cache=no --diff ${EXTRA_ARGS}
+55
View File
@@ -0,0 +1,55 @@
# Cache directories
app/cache/*
!app/cache/.gitkeep
!app/cache/.htaccess
app/logs/*
!app/logs/.gitkeep
# Chamilo configuration
/app/config/parameters.yml
/app/config/add_course.conf.php
/app/config/auth.conf.php
/app/config/course_info.conf.php
/app/config/events.conf.php
/app/config/mail.conf.php
/app/config/profile.conf.php
/app/config/configuration.php
# Courses
app/courses/*
!app/courses/proxy.php
# Home
app/home/*
# Upload content
app/upload/*
.php_cs.cache
# Logs and databases #
*.log
# IDE settings
.idea
.idea/*
.idea/dictionaries/*
.idea/cssxfire.xml
*.orig
nbproject/*
# Plugins config files
plugin/bbb/config.vm.php
# Cron temp files
main/cron/incoming/*
plugin/vchamilo/templates/*
# Stuff updated through composer - Remove just before release
/vendor
web/
node_modules
yarn.lock
+15
View File
@@ -34,6 +34,21 @@ if (empty($certificateData)) {
api_not_allowed(false, Display::return_message(get_lang('NoCertificateAvailable'), 'warning')); api_not_allowed(false, Display::return_message(get_lang('NoCertificateAvailable'), 'warning'));
} }
// Access control: only the owner, a platform admin, or a teacher of the certificate's course
// may view or export a certificate. Compare against $certificate->user_id (set from DB in the
// constructor) — not the $userId GET parameter, which an attacker can spoof to their own ID
// while supplying someone else's certificate $id.
$currentUserId = api_get_user_id();
if (!api_is_anonymous() && (int) $currentUserId !== (int) $certificate->user_id) {
$isCourseTeacher = false;
if (!empty($category) && !empty($category->get_course_code())) {
$isCourseTeacher = CourseManager::is_course_teacher($currentUserId, $category->get_course_code());
}
if (!api_is_platform_admin() && !$isCourseTeacher) {
api_not_allowed(true);
}
}
CustomCertificatePlugin::redirectCheck($certificate, $certificateId, $userId); CustomCertificatePlugin::redirectCheck($certificate, $certificateId, $userId);
switch ($action) { switch ($action) {
+11 -7
View File
@@ -58,7 +58,7 @@
"doctrine/orm": "~2.5", "doctrine/orm": "~2.5",
"emojione/emojione": "1.3.0", "emojione/emojione": "1.3.0",
"endroid/qr-code": "2.5.*", "endroid/qr-code": "2.5.*",
"enshrined/svg-sanitize": "^0.16.0", "enshrined/svg-sanitize": "^0.22.0",
"essence/essence": "2.6.1", "essence/essence": "2.6.1",
"ezyang/htmlpurifier": "~4.9", "ezyang/htmlpurifier": "~4.9",
"facebook/graph-sdk": "^5.7", "facebook/graph-sdk": "^5.7",
@@ -68,7 +68,7 @@
"graphp/graphviz": "~0.2.0", "graphp/graphviz": "~0.2.0",
"guzzlehttp/guzzle": "~6.0", "guzzlehttp/guzzle": "~6.0",
"h5p/h5p-core": "*", "h5p/h5p-core": "*",
"imagine/imagine": "0.6.3",
"ircmaxell/password-compat": "~1.0.4", "ircmaxell/password-compat": "~1.0.4",
"jbroadway/urlify": "1.1.0-stable", "jbroadway/urlify": "1.1.0-stable",
"jeroendesloovere/vcard": "~1.7", "jeroendesloovere/vcard": "~1.7",
@@ -78,7 +78,7 @@
"knplabs/gaufrette": "~0.3", "knplabs/gaufrette": "~0.3",
"knplabs/knp-components": "~1.3", "knplabs/knp-components": "~1.3",
"league/csv": "~8.0", "league/csv": "~8.0",
"media-alchemyst/media-alchemyst": "~0.5", "symfony/process": "~3.0|~4.0",
"michelf/php-markdown": "~1.7", "michelf/php-markdown": "~1.7",
"monolog/monolog": "~1.0", "monolog/monolog": "~1.0",
"mpdf/mpdf": "^8.0", "mpdf/mpdf": "^8.0",
@@ -92,11 +92,11 @@
"php-xapi/repository-api": "dev-master as 0.3.1", "php-xapi/repository-api": "dev-master as 0.3.1",
"php-xapi/repository-doctrine": "dev-master", "php-xapi/repository-doctrine": "dev-master",
"php-xapi/symfony-serializer": "2.1.0 as 2.0", "php-xapi/symfony-serializer": "2.1.0 as 2.0",
"phpmailer/phpmailer": "~6.1", "phpmailer/phpmailer": "^6.8",
"phpoffice/phpexcel": "~1.8", "phpoffice/phpspreadsheet": "^1.28",
"phpoffice/phpword": "~0.14", "phpoffice/phpword": "~0.14",
"phpseclib/phpseclib": "^2.0", "phpseclib/phpseclib": "^2.0",
"robrichards/xmlseclibs": "3.0.*", "robrichards/xmlseclibs": "^3.1.5",
"sabre/vobject": "~3.1", "sabre/vobject": "~3.1",
"sonata-project/admin-bundle": "~3.1|~4.0", "sonata-project/admin-bundle": "~3.1|~4.0",
"sonata-project/core-bundle": "~3.1|~4.0", "sonata-project/core-bundle": "~3.1|~4.0",
@@ -107,6 +107,7 @@
"sylius/attribute": "0.13.0", "sylius/attribute": "0.13.0",
"sylius/translation": "0.13.0", "sylius/translation": "0.13.0",
"symfony/console": "~3.0|~4.0", "symfony/console": "~3.0|~4.0",
"symfony/css-selector": "~3.4|~4.0",
"symfony/doctrine-bridge": "~2.8", "symfony/doctrine-bridge": "~2.8",
"symfony/dom-crawler": "~3.4|~4.0", "symfony/dom-crawler": "~3.4|~4.0",
"symfony/filesystem": "~3.0|~4.0", "symfony/filesystem": "~3.0|~4.0",
@@ -197,6 +198,9 @@
], ],
"config": { "config": {
"sort-packages": true, "sort-packages": true,
"component-dir": "web/assets" "component-dir": "web/assets",
"audit": {
"block-insecure": false
}
} }
} }
Generated
+1626 -1809
View File
File diff suppressed because it is too large Load Diff
+440 -11
View File
@@ -110,6 +110,414 @@
</table> </table>
<div class="version" aria-label="1.11.40">
<a id="1.11.40"></a>
<h1>Chamilo 1.11.40 - Quimper, ?/06/2026</h1>
<h3>Release notes - summary</h3>
<p>Chamilo 1.11.40 is a security and bugfix release on top of 1.11.38. 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/Quimper">Quimper</a> is the prefecture of the Finistère department in Brittany, north-western France. It is known for its Gothic cathedral, well-preserved medieval old town, and its centuries-old tradition of hand-painted Quimper faïence pottery.</p>
<h3>Security fixes</h3>
<ul aria-live="off">
<li>[2026-05-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/064787b28633b00218a6bf365248fdbf07b00e79">064787b2</a> - <a href="https://github.com/chamilo/chamilo-lms/security/advisories/GHSA-wjxh-pvwh-h84p">GHSA-wjxh-pvwh-h84p</a>) Security: LP: Using current user ID instead of request uid</li>
<li>[2026-04-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/c497f7ef7fadeabb80ba27046a3ad5ce0edfc139">c497f7ef</a> - <a href="https://github.com/chamilo/chamilo-lms/security/advisories/GHSA-8rj2-88v5-mx6w">GHSA-8rj2-88v5-mx6w</a>) Security: Remove <code>eval</code> usage in <code>HTML_QuickForm</code> library and replace with safer array operations</li>
<li>[2026-04-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/7cc961d60ac27eaccd6bcc8db201de2c65adfdf3">7cc961d6</a> - <a href="https://github.com/chamilo/chamilo-lms/security/advisories/GHSA-prqp-8x29-cr88">GHSA-prqp-8x29-cr88</a>) Security: Validate and enforce <code>groupOp</code> keyword in AJAX filters to prevent improper usage or invalid values</li>
<li>[2026-03-31] (<a href="https://github.com/chamilo/chamilo-lms/commit/823ed10b983981cdaf0a44f79041757e08ffb3c1">823ed10b</a> - <a href="https://github.com/chamilo/chamilo-lms/security/advisories/GHSA-gcjp-f7jm-rrrg">GHSA-gcjp-f7jm-rrrg</a>) Security: Fix stored XSS via null-byte bypass in user registration</li>
<li>[2026-03-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/08b4fbf3e8ec957a982805997c080762394d714b">08b4fbf3</a> - <a href="https://github.com/chamilo/chamilo-lms/security/advisories/GHSA-4hwq-pv7c-3928">GHSA-4hwq-pv7c-3928</a>) Security: Improve languages and sublanguages scripts security</li>
<li>[2026-03-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/836c5c9ad6fb4ccf40f8c0c015e1bb38a81f91c8">836c5c9a</a>) Security: Fix multiple filtering issues about session_lifetime</li>
</ul>
<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 new feature</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>[2026-04-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/7e081bbf88524dea854c5699899c1aa69acbb7d4">7e081bbf</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Add <code>delete_session</code> method in API</li>
<li>[2026-04-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/8660b53d3eb529a1a42d4c3caaad625462dd5ea2">8660b53d</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Add <code>update_course</code> method in API</li>
</ul>
<h3>Improvements (minor features) and debug</h3>
In reverse chronological order...
<ul aria-live="off">
<li>[2026-05-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/61d29b7da232cd59f43c1fa16cca4372e67f5073">61d29b7d</a> - <a href="https://task.beeznest.com/issues/23439">BT#23439</a>) User: Internal: fix user update when imported user already exist and status with value user or teacher</li>
<li>[2026-04-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/729353b329a95b4e6b1eca393d8907a8df262058">729353b3</a> - <a href="https://task.beeznest.com/issues/23282">BT#23282</a>) Admin: Fix weird array-reference issue in user_move_stats.php</li>
<li>[2026-04-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/5b555a74357e3a96803e99c7ba5005001b38ccf6">5b555a74</a> - <a href="https://task.beeznest.com/issues/23282">BT#23282</a>) Tracking: Fix processUserDataMove() for main/admin/user_move_stats.php if some data are empty</li>
<li>[2026-04-16] (<a href="https://github.com/chamilo/chamilo-lms/commit/389ce3b62012ace163c28d91b68f6a4f09ea5aaa">389ce3b6</a>) Course: Set Moodle display defaults for PDFs and links</li>
<li>[2026-04-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/9531ba05f88f119ed9f4d0b593a07c97c217b78a">9531ba05</a> - <a href="https://task.beeznest.com/issues/23208">BT#23208</a>) Group: Fix group ordering to be the same for admin, teacher and student</li>
<li>[2026-03-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/b8b9c76bd88ad297c3a0e669a84fac64a0646b50">b8b9c76b</a>) Vendor: Upgrade <code>enshrined/svg-sanitize</code> to ^0.22.0</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>See new <code>delete_session</code> and <code>update_course</code> API methods in notable new features above</li>
</ul>
<h3>Removals</h3>
<ul aria-live="off">
<li>No notable removal</li>
</ul>
<h3>Known issues</h3>
<ul aria-live="off">
<li>(<a href="https://github.com/chamilo/chamilo-lms/issues/5910">GH#5910</a>, <a href="https://github.com/chamilo/chamilo-lms/issues/7354">GH#7354</a>, <a href="https://github.com/chamilo/chamilo-lms/issues/7676">GH#7676</a>) PDF certificate generation may fail to export embedded images in some configurations</li>
<li>(<a href="https://github.com/chamilo/chamilo-lms/issues/8153">GH#8153</a>, <a href="https://github.com/chamilo/chamilo-lms/issues/8406">GH#8406</a>) Learning paths may be invisible to students in some PHP 8.3 configurations despite being set to public</li>
</ul>
</div>
<div class="version" aria-label="1.11.38">
<a id="1.11.38"></a>
<h1>Chamilo 1.11.38 - Pontorson, 23/03/2026</h1>
<h3>Release notes - summary</h3>
<p>Chamilo 1.11.38 is a security and bugfix release on top of 1.11.36. For any significant change, please check the 1.11.30 release notes.</p>
<h3>Release name</h3>
<p><a href="https://fr.wikipedia.org/wiki/Pontorson_(commune_d%C3%A9l%C3%A9gu%C3%A9e)">Pontorson</a> is a former commune in the Manche department in Normandy, France. It is known as the gateway to the Mont Saint-Michel bay.</p>
<h3>Security fixes</h3>
<ul aria-live="off">
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/22b1cb1c609b643765c88654155aba27070c927e">22b1cb1c</a>) Security: Improve XML parsing by adding LIBXML_NONET and better error handling to prevent XXE attacks</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/e7400dd840586ae134b286d0a2374f3d269a9a9d">e7400dd8</a>) Security: Replace weak API key generation with cryptographically secure random keys using random_bytes</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/750a45312a0d5c3ad60dbfbd0d959ca40be4a18c">750a4531</a> - <a href="https://github.com/chamilo/chamilo-lms/security/advisories/GHSA-f27g-66gq-g7v2">GHSA-f27g-66gq-g7v2</a>) Security: Fix weak password recovery token generation - deterministic SHA1 replaced by cryptographic random token with expiry</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/4a119f93abbfba6fe833580f2463c8d4afa500c2">4a119f93</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6078">GH#6078</a>) Security: Restrict non-admin users from accessing GET_USER_INFO_FROM_USERNAME REST API action</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/4efb5ee8ed849ca147ca1fe7472ef7b98db17bff">4efb5ee8</a>) Security: Avoid information disclosure through direct access to .tpl template files</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/0acf8a196307c66c049f97f5ff76cf21c4a08127">0acf8a19</a>) Security: Restrict non-admin users from modifying admin-only user fields in REST API</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/9748f1ffbdb8b6dc84c0e0591c9d3c1d92e21c00">9748f1ff</a>) Security: Add filtering of .pht files to prevent extension-based upload filter bypass</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/6331d051b4468deb5830c01d1e047c5e5cf2c74f">6331d051</a> - <a href="https://github.com/chamilo/chamilo-lms/security/advisories/GHSA-3rv7-9fhx-j654">GHSA-3rv7-9fhx-j654</a>) Security: Fix IDOR in learning path progress saving endpoint</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/d3355d7873c7e5b907c5fa84cbd5d9b62ed33e51">d3355d78</a>) Security: Remove chained unauthenticated RCE in main/install/ scripts</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/02f82ba627fd9831b2bb7b01df3a2d3cdf44784a">02f82ba6</a>) Security: Remove configuration file update vector in install scripts</li>
<li>[2026-03-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/5b0531d0c84fa0cca7f8a5e2f416fc009591e17a">5b0531d0</a>) Security: Remove chained RCE in main/install/ scripts via $GLOBALS injection and unsanitized config writes</li>
<li>[2026-03-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/8cbe660de267f2b6ed625433bdfcf38dee8752b4">8cbe660d</a>) Security: Remove unused updateSound method from exercise.class.php</li>
<li>[2026-03-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/b005b3d3e76cf6eafc03e15ac445ceff089551c0">b005b3d3</a>) Security: Validate and sanitize page parameter in session course edit to prevent unauthorized redirects</li>
<li>[2026-03-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/63e1e6d3d717bd537c7c61719416da35aaa658dd">63e1e6d3</a>) Security: Strengthen evaluation editing logic by adding course ownership and ID validation</li>
<li>[2026-03-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/3b03306d1a0301a81b9284e86893b27f518ab151">3b03306d</a>) Security: Add evaluation ID validation in gradebook result operations to prevent unauthorized actions</li>
<li>[2026-03-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/3597b19b73d73d681e4fb503285e9bbfe71714bf">3597b19b</a>) Security: Sanitize shell command inputs using escapeshellarg to prevent command injection</li>
<li>[2026-03-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/ea6b7b7e90580c9b01dc4bcafe4ad737061e0ead">ea6b7b7e</a>) Security: Add URL safety checks to prevent SSRF attacks</li>
<li>[2026-03-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/4dddcc19d36119da27b7c49eb84a035800abae78">4dddcc19</a>) Security: Prevent path traversal attempts in HotPotatoes exercises</li>
<li>[2026-03-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/f968082b991d08a71718d5a75ecfef0c98e67ec9">f968082b</a>) Security: Add additional safety warning to configuration.php setting 'plugin_upload_enable'</li>
</ul>
<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 new feature</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>[2026-03-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/ce0192c62e48c9d9474d915c541b3274844afbf9">ce0192c6</a>) Learnpath: Deprecate and disable AICC support functionality</li>
</ul>
<h3>Improvements (minor features) and debug</h3>
In reverse chronological order...
<ul aria-live="off">
<li>[2026-03-14] (<a href="https://github.com/chamilo/chamilo-lms/commit/b20b1f950195007e170f54a6a599cd0646f70c40">b20b1f95</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Course: Fix Moodle export titles, visibility and embedded images</li>
<li>[2026-03-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/2ab28ec5a61b7eed34c0e4222dff03dcdfc5e6fa">2ab28ec5</a> - <a href="https://task.beeznest.com/issues/23292">BT#23292</a>) Announcement: Fix destination group select to only have the current session groups</li>
<li>[2026-03-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/2b85c8c9822b035a37ffcb7f2559e15eabb3a778">2b85c8c9</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/7673">GH#7673</a>) Social: Fix missing session_id parameter and add language management</li>
<li>[2026-03-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/cc1a341c9fcdc4e4e377980c2c880d77181e7929">cc1a341c</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Course: Improve Moodle export for quiz questions and embedded files</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>No notable change</li>
</ul>
<h3>Removals</h3>
<ul aria-live="off">
<li>AICC/HACP support has been deprecated and disabled (see features above)</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.36">
<a id="1.11.36"></a>
<h1>Chamilo 1.11.36 - Penzance, 08/03/2026</h1>
<h3>Release notes - summary</h3>
<p>Chamilo 1.11.36 is mostly a security release on top of 1.11.34. For any significant change, please check the 1.11.30 release notes.</p>
<h3>Release name</h3>
<p><a href="https://grokipedia.com/page/Penzance">Penzance</a> is a coastal town, civil parish, and port in Cornwall, England, located on the Penwith peninsula as the largest town in West Cornwall and the westernmost major settlement on the British mainland.</p>
<h3>Security fixes</h3>
<ul aria-live="off">
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/31b20f4123343efd774be790d49924bebc52bbd4">31b20f41</a>) Security: Fix authenticated XSS in session categories See advisory GHSA-qg5f-gq95-9vhq reported by @elliSzAt</li>
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/ede59ad0a0814c0cfa0c2cad029863cf8e54343b">ede59ad0</a>) Security: Fix authenticated XSS in session categories See advisory GHSA-qg5f-gq95-9vhq reported by @elliSzAt</li>
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/6ecb6ace3e827b147a2221730679c2a39e79b451">6ecb6ace</a>) Security: Fix authenticated XSS in session categories - reported by @elliSzAt</li>
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/f2045b746d4b4beae77fa3bebce201557a79a590">f2045b74</a>) Security: Prevent net-null double filtering of dates in statistics. Avoids possible SQL injection by admin role - Reported by @elliSzAt</li>
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/5530d8b4502a27e84d7ea291bcf74ff729564b1e">5530d8b4</a>) Security: Fix user enumeration vulnerability on password reminder page - reported by Joshua Chan (@popcorn94)</li>
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/99e7358fb429e8ff221aea71ec17df95c7bf403e">99e7358f</a>) Security: Fix authenticated RCE in plugin - Reported by @DhiyaneshDK</li>
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/f968082b991d08a71718d5a75ecfef0c98e67ec9">f968082b</a>) Security: Add additional safety warning to configuration.php setting 'plugin_upload_enable' as it bears considerable risks</li>
</ul>
<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>[2026-03-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/5d17107450c3f8ce0bf0bd42c5b11dd1d5651015">5d171074</a> - <a href="https://task.beeznest.com/issues/23282">BT#23282</a>) Admin: Add updates to track_e_access, track_e_access_complete (if it exists) and track_e_downloads to user_move_stats.php</li>
<li>[2026-03-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/f7a7203a0f4a80fcec562429931805a2706dc10d">f7a7203a</a> - <a href="https://task.beeznest.com/issues/23282">BT#23282</a>) Admin: Add unsubscription of user from previous session in user_move_stats.php if there was only one course in that session</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">
</ul>
<h3>Improvements (minor features) and debug</h3>
In reverse chronological order...
<ul aria-live="off">
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/41d285db2ac989fbc40b074c34eb13d9f51831e3">41d285db</a> - <a href="https://task.beeznest.com/issues/23289">BT#23289</a>) Forum: Fix quotes in SQL query</li>
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/94679333b33093a168c431a029d4bc1ea90abb20">94679333</a> - <a href="https://task.beeznest.com/issues/23289">BT#23289</a>) Fix image path conversion to calculate size with `api_getimagesize` function</li>
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/7667a4ee04b94b9795e7fee441f06ee59940cfe8">7667a4ee</a>) Increase session category list pagination limit from 1 to 20</li>
<li>[2026-03-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/80e1a18c981057f975cf5ef72828c3b7b2df5c93">80e1a18c</a> - <a href="https://task.beeznest.com/issues/23269">BT#23269</a>) Internal: Fix excel export after update of the generation library</li>
<li>[2026-03-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/f8d8a9b7be8f523c190186f63b739e80a14186e8">f8d8a9b7</a> - <a href="https://task.beeznest.com/issues/23219">BT#23219</a>) Learnpath: Fix class name casing for `learnpath` instantiations</li>
<li>[2026-03-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/20ac2896d2fd69d82a03bf00e2403856957503ac">20ac2896</a> - <a href="https://task.beeznest.com/issues/23282">BT#23282</a>) Script: Add script to fix bad user session data move</li>
<li>[2026-03-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/e7cf4803875b48c9376076efadf2f6e516b45cbe">e7cf4803</a> - <a href="https://task.beeznest.com/issues/22393">BT#22393</a>) Exercise: fix radar feedback that was not showing on the result page</li>
<li>[2026-03-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/3ea5aab0cc584c197c08f2c4fa08b9a42d3fcab0">3ea5aab0</a> - <a href="https://task.beeznest.com/issues/22393">BT#22393</a>) Exercise: fix radar feedback that was not enabling attempt access</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>No notable change</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.34">
<a id="1.11.34"></a>
<h1>Chamilo 1.11.34 - Cassis, 28/02/2026</h1>
<h3>Release notes - summary</h3>
<p>Chamilo 1.11.34 is mostly a security release on top of 1.11.32. 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/Cassis">Cassis</a> is a commune situated east of Marseille in the department of Bouches-du-Rhône in the Provence-Alpes-Côte d'Azur region, whose coastline is known in English as the French Riviera, in Southern France.</p>
<h3>Security fixes</h3>
<ul aria-live="off">
<li>[2025-08-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/a6ce8fb3dc88525c1fcfc1a1e24dacb4ea033684">a6ce8fb3</a>) Security: Replace $_REQUEST with HttpFoundation\Request class and remove XSS in assign, issued, and issued_all pages See advisory GHSA-cchj-3qmf-82j5</li>
<li>[2025-08-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/74e4fa299ddf91aafedb4788ac6d98383f0b67c3">74e4fa29</a>) Security: Exercise: Filter XSS when showing teacher comment</li>
<li>[2025-08-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/5b9a68bbdb93a03f01d16aadcfb44a3d8488566c">5b9a68bb</a>) Security: Filter SVG files uploaded from social network See advisory GHSA-2vq2-826h-6hp6</li>
<li>[2025-08-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/8c74517a460f412c3e6c44368b0838ebcd031419">8c74517a</a>) Security: Restrict category title updates to the current user on the category sorting page See advisory GHSA-x3h9-h7qf-wwrf</li>
<li>[2025-08-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/71c35dedcfd4d1ea2fec4d77cdd29c32f5dc668c">71c35ded</a>) Security: Replace $_REQUEST with HttpFoundation\Request class on the category sorting page See advisory GHSA-x3h9-h7qf-wwrf</li>
<li>[2025-09-01] (<a href="https://github.com/chamilo/chamilo-lms/commit/b21663fdf79cfd5cdba4d217133f296037d83670">b21663fd</a>) Security: Course description: Remove XSS when showing title See advisory GHSA-p32q-6gh3-3gcv</li>
<li>[2025-09-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/46d60597af923b5f5a7ae2190db7b6f70dd0fde8">46d60597</a>) Security: Blog: Add token validation for visibility and delete actions See advisory GHSA-rpj6-p9m5-q637</li>
<li>[2025-09-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/172e9fad41a3783a910bc658a6fdab75bc378d9f">172e9fad</a>) Security: Exercise: remove XSS when showing feedback See advisory GHSA-59h4-34mx-m67m</li>
<li>[2026-01-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/d96de9e75fa30b4b58d1383824e054e6c26aaa94">d96de9e7</a>) Security: Fix image upload vulnerability by re-encoding images See advisory GHSA-4pc3-4w2v-vwx8</li>
<li>[2026-02-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/df9002f99cd8ab7feeb62c64660cf4339b54bd0c">df9002f9</a>) Security: Remove unsafe custom_dates SQL concatenation in model.ajax.php The custom_dates parameter in the get_sessions action was directly concatenated into a raw SQL WHERE clause without sanitization, allowing unauthenticated SQL injection via the filters parameter. The parameter had no legitimate callers in the codebase (not used by jqGrid filters nor any frontend flow), so it has been removed completely with no functional impact. Se advisory GHSA-84gw-qjw9-v8jv</li>
<li>[2026-02-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/3a9df760bb918b744f3fd1da3965ca7710084f59">3a9df760</a>) Security: Fix upload issue reported by Meng Hokseng</li>
<li>[2026-02-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/7b5bcc771179d47feebe042afeac0e2702a084ff">7b5bcc77</a>) Security: Add authorization check on personal course categories edition - reported by Bulwarkers Websecurity PVT. LTD</li>
<li>[2026-02-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/9c9a73170faad500ff4619c812d32cd7b1148bd8">9c9a7317</a>) Security: Add CSRF to personal course categories edition - reported by Bulwarkers Websecurity PVT. LTD</li>
</ul>
<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>[2025-08-01] (<a href="https://github.com/chamilo/chamilo-lms/commit/3bcb5a21784f5d9c4d23cbdb391b88cd9f0d674f">3bcb5a21</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6501">GH#6501</a>) Ticket: Add Allow category manager to view all tickets in his category</li>
<li>[2025-08-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/12da449e0d46a307fa4921277a11cd5fec5595e3">12da449e</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6526">GH#6526</a>) Exercise: Add date filter to pending exercises</li>
<li>[2025-09-02] (<a href="https://github.com/chamilo/chamilo-lms/commit/7b28a2bd1e6619438be55e2709086a7d22c01cbd">7b28a2bd</a>) Exercise: Add sorting by date and search by user in pending.php</li>
<li>[2025-10-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/5b1657af106554c2b6a7e87c0798650bd9fcf5ed">5b1657af</a> - <a href="https://task.beeznest.com/issues/22963">BT#22963</a>) Learnpath: Enable drag-and-drop reordering for categories</li>
<li>[2026-02-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/4364b698077f1a931adf11336ea8b7148c365a98">4364b698</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/7132">GH#7132</a>) Admin: Add phone number search functionality to user search</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>[2025-07-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/872c79e8aad749e515e4d58983f96627aedcc77f">872c79e8</a> - <a href="https://task.beeznest.com/issues/22723">BT#22723</a>) Tracking: Add global parameter to enable theoretical time for course on myStudent tracking and resume session</li>
<li>[2025-08-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/d5e64a5a53f8e1667efcf85d18ec3c5e4f5e6359">d5e64a5a</a> - <a href="https://task.beeznest.com/issues/22708">BT#22708</a>) Add wysiwyg_image_auto_resize_max conf setting</li>
<li>[2025-09-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/d5ef11fd17efc28d49f10d5ce9dce6a62664a827">d5ef11fd</a> - <a href="https://task.beeznest.com/issues/22960">BT#22960</a>) User: Account unification: safe merge of duplicates by extra field</li>
<li>[2025-10-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/9856d886767e82c2c83a97543635289bf08441a6">9856d886</a>) Admin: Add configuration setting 'teacher_access_all_tracking' to allow teachers to access tracking of all courses</li>
<li>[2025-11-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/8aec3e8a6b48981eae6e5b7f8262394403dcf203">8aec3e8a</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6958">GH#6958</a>) Course: Moodle export: Add activities meta exports (Announcements, Attendance, Calendar, Gradebook, etc.)</li>
<li>[2025-11-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/87a7422fd5538a51460b05a2d4d5c352ec9c86a1">87a7422f</a> - <a href="https://task.beeznest.com/issues/23065">BT#23065</a>) Exercise: Add configuration setting 'quiz_result_pdf_export_include_official_code_in_file_name' to add official code in the quiz result pdf export file name</li>
<li>[2025-11-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/5021d6ca60f7e1e6ef3f34717acb9eeeb3d32b3f">5021d6ca</a> - <a href="https://task.beeznest.com/issues/23092">BT#23092</a>) Tracking: Add configuration setting 'session_admin_access_global_statistics' to give session admin access to statistics module</li>
<li>[2025-11-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/e537f667d0acf5606bb572a18a122df418b7966c">e537f667</a> - <a href="https://task.beeznest.com/issues/23089">BT#23089</a>) Admin: Add configuration setting 'disallow_hrm_login_as' to disallow HR managers to use the 'login as' feature</li>
<li>[2025-12-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/94e5bc284a159221fc078251a3e3b6c4edeb4433">94e5bc28</a> - <a href="https://task.beeznest.com/issues/22320">BT#22320</a>) Attendance: Add configuration setting 'attendance_add_official_code' to show official code in attendance table, pdf and xls export</li>
<li>[2025-12-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/b26553a8979ccb131e6e2e26e7440e2eea1b1b78">b26553a8</a> - <a href="https://task.beeznest.com/issues/23114">BT#23114</a>) Admin: Add configuration setting 'disallow_session_admin_edit_users'</li>
<li>[2025-12-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/4491592cae610f6550f082ce24a5dd6a6ba52a2d">4491592c</a> - <a href="https://task.beeznest.com/issues/23114">BT#23114</a>) Admin: Add configuration setting 'disallow_session_admin_login_as'</li>
</ul>
<h3>Improvements (minor features) and debug</h3>
In reverse chronological order...
<ul aria-live="off">
<li>[2026-02-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/7a5689e84b0dae00774784dc391ea154312eaf5a">7a5689e8</a>) Internal: Bump version to 1.11.34</li>
<li>[2026-02-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/7c5ef56c227775626a529c16d69d4570d3ebecc3">7c5ef56c</a>) Internal: Update code to avoid PHP8-specific deprecation alerts</li>
<li>[2026-02-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/74bc7c1c7b51ab14c2351bd23bc40f40a8535e1c">74bc7c1c</a>) Documentation: Update changelog for 1.11.34 release</li>
<li>[2026-02-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/37a8853912e0350f6274dd60920bdfd594cf2852">37a88539</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/7294">GH#7294</a>) Plugin: IMS/LTI: Add requirement for PHP OpenSSL to IMS/LTI plugin README.md</li>
<li>[2026-02-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/cc88ae0d719db0e2fff738234c57936f9fe855a1">cc88ae0d</a> - <a href="https://task.beeznest.com/issues/23032">BT#23032</a>) User: Internal: fix user update when imported user already exist and username is empty</li>
<li>[2026-02-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/f93a10ae7cfadbd06b7d014429c20ead7f07df55">f93a10ae</a>) Internal: Update calls to create_function() to increase compatibility with #PHP8</li>
<li>[2026-02-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/11e503c0925a48443cd05b77610c8bc5c82ef616">11e503c0</a>) Internal: Remove calls to mb_convert_encoding() with HTML-ENTITIES for compatibilty with PHP 8.3</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/352afd9e0a996e7c59924dbb840ca37d9519d813">352afd9e</a> - <a href="https://task.beeznest.com/issues/23219">BT#23219</a>) Tracking: Order session courses by position in tracking query</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/66fca6eab78b55ddf6fbee0874afa49286126710">66fca6ea</a> - <a href="https://task.beeznest.com/issues/23219">BT#23219</a>) Tracking: Add report for session student progress</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/2fa7fc25b0d407e7bb92d115ae3387bae245dff0">2fa7fc25</a> - <a href="https://task.beeznest.com/issues/23219">BT#23219</a>) Internal: Fix getFinalEvaluationItem return type and replace array_pop with end for clarity</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/f184d07bc1e2cbcfb659d6d0e038c3839e1a8dd4">f184d07b</a>) Internal: Update phpmailer to ^6.8 to increase compatibility with #PHP8</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/72ba5f3bd04f4eba3dc8849f43de99483800101c">72ba5f3b</a>) Internal: Fix export to document from wiki (use .docx rather than .odt for wider compatibility) + rename file export to include course name + fix temp path for generated file</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/d90c7578087550adebdff4060990c4c6cca5c0e6">d90c7578</a>) Internal: Fix image proportions in resize of social group images</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/a831f5e3d9d1efc7498725f5fac69c9385626e04">a831f5e3</a>) Internal: Fix group image display issue with convertion from imagine to GD</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/7168bdb677a7e83f4bb5185a7d133f7a2720f4f1">7168bdb6</a>) Internal: Remove dependency on imagine/imagine (only used in one place, replaceable) and replace media-alchemyst (abandoned) by symfony/process for .doc generation from wiki to increase #PHP8 support</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/768f2d3d7be15dcdeb826157f7ee70cb41fc1aa8">768f2d3d</a>) Internal: Upgrade imagine/imagine to 1.3 to increase PHP8 support</li>
<li>[2026-02-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/daa5d21568734f5301e955a1f1dcdcd5c73b063f">daa5d215</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Course: Fix Moodle export file references and LP activity mappings</li>
<li>[2026-02-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/ac03c9453b45d23a4884f98ffe2645129b9cbb73">ac03c945</a> - <a href="https://task.beeznest.com/issues/23032">BT#23032</a>) User: Internal: Add user update if user already exist when extra_field_to_validate_on_user_registration is set</li>
<li>[2026-02-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/f08781885913e2e394553c4d8cf3a41a6ec01994">f0878188</a> - <a href="https://task.beeznest.com/issues/23241">BT#23241</a>) User: Admin: Add configuration setting 'user_hide_expiration_date_for_session_admin' to hide expiration date for session admins + Adapt option to hide 'Never expire'</li>
<li>[2026-02-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/96356ff8286dd03816c409eef15be10e55779ebb">96356ff8</a>) Plugin: CustomCertiicate: Allow further access to Sessions Admins</li>
<li>[2026-02-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/1b42b0b4c76ee05af5bc1f5fd27633500433b547">1b42b0b4</a>) Plugin: CustomCertificate: Allow access to Sessions Admins</li>
<li>[2026-02-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/1ad65dadb4f4ae347a06c604d2f2d40b3b34079f">1ad65dad</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/7196">GH#7196</a>) Learnpath: Remove status prerequisite from check. Fixes issue when a student takes a test multiple times and the last time is not a success</li>
<li>[2026-02-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/8d29f011b7c4991d0ff5a2b2f694474715cea564">8d29f011</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/7176">GH#7176</a>) Exercise: Remove invisible/problematic Unicode characters introduced by word</li>
<li>[2026-02-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/1ce0dd8a5da33f6bead65451b423c4185060cec4">1ce0dd8a</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6745">GH#6745</a>) Learnpath: Remove max score checking and fix other code syntax issues</li>
<li>[2026-02-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/7c952909cae17b9ce440d823f044334c3758068e">7c952909</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6841">GH#6841</a>) Survey: Normalize survey_type to int to avoid strict type mismatch on PHP 8.3</li>
<li>[2026-02-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/50a003a35e1024b5e7b1378a2d08b0e53e076ac2">50a003a3</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/7113">GH#7113</a>) Tracking: Fix visibility of courses for HR managers (ignore api_drh_can_access_all_session_content())</li>
<li>[2026-02-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/3b8245e5a05f925fa078347334394afe06826fe4">3b8245e5</a> - <a href="https://task.beeznest.com/issues/22929">BT#22929</a>) Skill: Fix condition for display of skills block</li>
<li>[2026-02-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/13c4b4ac80abe770c3f7840c8c0826fca2bdc1b6">13c4b4ac</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6312">GH#6312</a>) Learnpath: Fix commit 638aa1cbb66a: Add missing lp_initialize_item.inc.php</li>
<li>[2026-02-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/c6d6154265bd92ff2fe022260127e1cba8d8d666">c6d61542</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6312">GH#6312</a>) Learnpath: Implement prefetching of init data for LMSInitialize(). Use JS promise to avoid synchronous XHR</li>
<li>[2026-02-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/638aa1cbb66ab0cfe33899ef02dd08e92a6853a0">638aa1cb</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6312">GH#6312</a>) Learnpath: Encapsulate LMSInitialize() inside a backend method</li>
<li>[2026-02-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/751175aee528ef4e0bcb284697eb359998ad1341">751175ae</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5557">GH#5557</a>) Exercise: Restrict Actions for Hidden Exercises in Base Course</li>
<li>[2026-02-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/453c668a1de4530d5fa4de519f299fa6b169a1e8">453c668a</a>) Internal: Replace phpoffice/phpexcel: ~1.8 by phpoffice/phpspreadsheet: ^1.28 to increase PHP8 support</li>
<li>[2026-02-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/017d34403d971330af3a7776fc0307ba375e05f0">017d3440</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5657">GH#5657</a>) WYSIWYG: Filter access to session-specific resources through ElFinder</li>
<li>[2026-02-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/447f7d784ebf5bf2856f75f79000c10e5d94043d">447f7d78</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/5657">GH#5657</a>) WYSIWYG: Filter access to session-specific resources through ElFinder</li>
<li>[2026-02-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/4157937cc67d8156a749025c25d0160c233cd0ee">4157937c</a>) Internal: Fix E_NOTICE Undefined variable: lpShowMaxProgress</li>
<li>[2026-02-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/0cd2120d5edf4c86b5ff2a539f583eb89f17fa23">0cd2120d</a> - <a href="https://task.beeznest.com/issues/23172">BT#23172</a>) Script: User: Add tolerance for other type of single quote in names + change phone column header title</li>
<li>[2026-02-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/8bee623b69f03837acd2a7b2b3b6aa8e02c3f703">8bee623b</a>) Documentation: Add more specific instructions for commit messages in changelog.html</li>
<li>[2026-01-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/19e0674d43bc774507761658cd82b080292dac90">19e0674d</a> - <a href="https://task.beeznest.com/issues/23189">BT#23189</a>) Learnpath: Fix course backup import and LP restore flow</li>
<li>[2026-01-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/f3074706a5c083f92701725ddd1b5adc74129468">f3074706</a> - <a href="https://task.beeznest.com/issues/23162">BT#23162</a>) Group: Add official code to usergroup_users list</li>
<li>[2026-01-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/0d535da60505d01e214fa97a03ac4b775b6cda70">0d535da6</a> - <a href="https://task.beeznest.com/issues/22393">BT#22393</a>) Exercise: Fix user answer for fill in blanks question with menu options already hashed</li>
<li>[2026-01-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/502d35da58017d6e1ce1eefbb0e3e9a6d69518e2">502d35da</a> - <a href="https://task.beeznest.com/issues/22396">BT#22396</a>) Tracking: Fix Show progress based on visibles LPs only error 500</li>
<li>[2026-01-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/9c9e26aace5d7a870d691d0c3d41ada7434f3707">9c9e26aa</a> - <a href="https://support.chamilo.org/issues/23180">CT#23180</a>) Attendance: Fix session copy with session content to avoid duplicate of attendance in the original sesion</li>
<li>[2026-01-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/4a34838acd11cf0ff80b6d38a1c8d755f53a8307">4a34838a</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Internal: Refactor session coach edit to use `SessionManager::getCoachesByCourseSession`</li>
<li>[2026-01-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/70e7e20ff7d7cd86eb077bec4693096159aacd47">70e7e20f</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Internal: Update extra field array key from `title` to `display_text` for consistency</li>
<li>[2026-01-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/ad24b8ffa309158effc63b42a3119a84116772fd">ad24b8ff</a> - <a href="https://task.beeznest.com/issues/23154">BT#23154</a>) Script: fix warning and move debug variable to avoid override in included files</li>
<li>[2026-01-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/33b6ed85b2d8ea1715e708afd3aaf30a00f3c7d8">33b6ed85</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Internal: Move code to function</li>
<li>[2026-01-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/8e8ad82579188247b18ee0146110b185764f1528">8e8ad825</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Internal: Clean handling of query parameters + improve type safety in `decodeParams` method</li>
<li>[2026-01-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/e34cc0bd3929028e7e33d0626f21634aa7d93db8">e34cc0bd</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Course: Fix lesson document exports and LP activity mapping</li>
<li>[2025-12-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/4b3495039fc40118199c851eaced95c84364126d">4b349503</a> - <a href="https://task.beeznest.com/issues/23132">BT#23132</a>) Exercise: Fix user choice presentation in result page for Dragging question</li>
<li>[2025-12-16] (<a href="https://github.com/chamilo/chamilo-lms/commit/256da57cacef46ed580177bb1e0558afab91fe3d">256da57c</a>) Remove invisible Unicode characters that can cause // Remove invisible Unicode characters that can cause comparison issues // U+200B = ZERO WIDTH SPACE // U+200C = ZERO WIDTH NON-JOINER // U+200D = ZERO WIDTH JOINER // U+FEFF = ZERO WIDTH NO-BREAK SPACE (BOM)</li>
<li>[2025-12-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/acd49cc940801bbe09cc712fe3eec7daf1a34a6c">acd49cc9</a> - <a href="https://task.beeznest.com/issues/23132">BT#23132</a>) Exercise: revert commit d005c99dda697a and apply a better fix for user choice and expected choice presentation on result page</li>
<li>[2025-12-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/d005c99dda697ac60089733878f3189b41c5c5fb">d005c99d</a> - <a href="https://task.beeznest.com/issues/23132">BT#23132</a>) Exercise: Fix user choice presentation in result page for matching question</li>
<li>[2025-12-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/7a6ed04102a59e328e32ef3390ce7778e6f93411">7a6ed041</a> - <a href="https://task.beeznest.com/issues/23122">BT#23122</a>) Script: User: Add filter for url white list and url black list</li>
<li>[2025-12-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/324be333569b037f103e620807e66d25251173d7">324be333</a> - <a href="https://task.beeznest.com/issues/23002">BT#23002</a>) Tracking: Change for method from GET to POST to avoid error with URL too long</li>
<li>[2025-12-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/aafc5380c480e021afff5e4bcbd06c077b20e830">aafc5380</a>) Exercise: Fix missing MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION in questions' duplicate() method</li>
<li>[2025-12-02] (<a href="https://github.com/chamilo/chamilo-lms/commit/ce11a8ba8791116f434ded7b8171a8234c3c8b7c">ce11a8ba</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) Exercise: Enable attempt deletion by session_admin if session_admins_edit_courses_content is true</li>
<li>[2025-11-30] (<a href="https://github.com/chamilo/chamilo-lms/commit/f8f363800205ba59648958bb1a31de2e55924a32">f8f36380</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) User: Account unification: Add possibility to filter on user status and the unification on a teacher account if one exist instead of the most recent account</li>
<li>[2025-11-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/aafbb7b7ab88f2997d40be70f9a5326573857da7">aafbb7b7</a> - <a href="https://task.beeznest.com/issues/23109">BT#23109</a>) Tracking: Language: Minor adapt translation variable to correspond to the sessions lists</li>
<li>[2025-11-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/46fe78a84fd47dee3b838b483dcf50011c7d2074">46fe78a8</a> - <a href="https://task.beeznest.com/issues/23090">BT#23090</a>) Course: Fix course backup import for accented document paths</li>
<li>[2025-11-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/288a8306b5efa9ebe232a04374c980fbc3fa241a">288a8306</a> - <a href="https://task.beeznest.com/issues/23109">BT#23109</a>) Tracking: Use advanced session multiselect in certificates session report</li>
<li>[2025-11-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/a3e0a5e4010d69adce0599346a9325022d15e905">a3e0a5e4</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) User: Account unification: session's id_coach and session_admin_id to the unification process</li>
<li>[2025-11-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/9f7c7c904f2baf158da01af203e65fb2cb0cbba0">9f7c7c90</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) Script: User: Use searched field as filter, fix syntax errors, add filter on URLs</li>
<li>[2025-11-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/e5da9444be3bd213cfcdb465e1ff449710ff5a28">e5da9444</a> - <a href="https://task.beeznest.com/issues/23109">BT#23109</a>) Tracking: Add multi-session selector and bulk certificate export in session filter</li>
<li>[2025-11-26] (<a href="https://github.com/chamilo/chamilo-lms/commit/18ab7d9d47dff39e8755009c77f48c4022e16a87">18ab7d9d</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Course: Fix Moodle export: include root Documents in folder and keep learnpath items ordered by display_order</li>
<li>[2025-11-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/a446e4be306d9e4405539e58c191f72d7ff0aabf">a446e4be</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) Script: User: Add timestamp to know how long it takes to unify a user</li>
<li>[2025-11-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/64fbd53cfed0a21ae8a6a48d818bd21b1eb16ffb">64fbd53c</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) Script: User: Add filtered user list to unify only those users</li>
<li>[2025-11-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/162d1358833ce7604ab140d374784f90ddadc68e">162d1358</a> - <a href="https://task.beeznest.com/issues/23089">BT#23089</a>) Gradebook: Allow show flatview to HR users</li>
<li>[2025-11-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/196c97a0abcc3c9661d6c48996f8d468239e43f5">196c97a0</a>) Internal: Add return and parameter types across gradebook classes for improved type safety and readability</li>
<li>[2025-11-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/7f2006f37c1700421c9e9fcc64e46c37be3978aa">7f2006f3</a> - <a href="https://task.beeznest.com/issues/23091">BT#23091</a>) Session: Add possibility to return session with no course</li>
<li>[2025-11-11] (<a href="https://github.com/chamilo/chamilo-lms/commit/7da8728bba6046ee3c6b070c72ab89b8df46cd57">7da8728b</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) User: Account unification: add track_e_access_complete to the unification process if the table exist</li>
<li>[2025-11-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/36fa8a6fbe0827eb2f2298fdafca24ce836778e0">36fa8a6f</a> - <a href="https://task.beeznest.com/issues/23071">BT#23071</a>) Learnpath: Internal: Fix commit 8ba13c4056 because there is not always a LearnpathVisible registry</li>
<li>[2025-11-10] (<a href="https://github.com/chamilo/chamilo-lms/commit/8ba13c4056f9358b2b1d608c76f8670657fe2d51">8ba13c40</a> - <a href="https://task.beeznest.com/issues/23071">BT#23071</a>) Learnpath: Internal: Fix lp visibility when there have been lp subscription in between</li>
<li>[2025-11-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/16fbd415e58f81eb56745f6c83530b8f0ba048a5">16fbd415</a> - <a href="https://task.beeznest.com/issues/23068">BT#23068</a>) Internal: Simplify conditional checks and use null coalescing operator across exercise-related files for cleaner and more consistent code readability</li>
<li>[2025-11-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/c2ec80ad85ecc37148bdf005ef783440412cfb13">c2ec80ad</a> - <a href="https://task.beeznest.com/issues/23068">BT#23068</a>) Exercise: Fix escape user answer for fill in blanks question</li>
<li>[2025-11-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/60fda7f3a9bd71d464daeaff13205d1553360ec8">60fda7f3</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) Script: User: Unify on most recent user duplicated users based on extra field value</li>
<li>[2025-10-28] (<a href="https://github.com/chamilo/chamilo-lms/commit/899aaa1dab77a426028b48c50a1a45b40e316332">899aaa1d</a>) Session: Improve option Show subscription column in session course list on main/mySpace/myStudents.php</li>
<li>[2025-11-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/dfe42b68b151b504bf7b6f22b47f27142ef12f7f">dfe42b68</a> - <a href="https://task.beeznest.com/issues/22320">BT#22320</a>) Cron: Update import_users_from_xlsx.php with improved tolerance for repeated accounts</li>
<li>[2025-10-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/857d5940f0256c23d62989f426ea27f9aff971fb">857d5940</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) User: Account unification: Language fix content and translation variable to have translation and add missing variables and translation in EN, FR and ES</li>
<li>[2025-10-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/f1b84396815349bceaa2409ef7422a8c9f551be1">f1b84396</a> - <a href="https://task.beeznest.com/issues/22702">BT#22702</a>) User: Account unification: Add registration date column</li>
<li>[2025-10-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/41d6a4f5d9c99435033ca3d40af7a1697b6aba87">41d6a4f5</a> - <a href="https://task.beeznest.com/issues/21977">BT#21977</a>) Course: Moodle export: mirror Documents tree; use LP item titles; restore document items</li>
<li>[2025-10-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/246873da0605840ad634850257b6dfbe5e62a112">246873da</a> - <a href="https://task.beeznest.com/issues/23018">BT#23018</a>) Plugin: AzureActiveDirectory: Fix user picture deletion on update user on login</li>
<li>[2025-10-20] (<a href="https://github.com/chamilo/chamilo-lms/commit/6c8c24b6204aad31863084ddb640d2a2003e7fa6">6c8c24b6</a> - <a href="https://task.beeznest.com/issues/23014">BT#23014</a>) Gradebook: Fix no action when clicking on import result</li>
<li>[2025-10-17] (<a href="https://github.com/chamilo/chamilo-lms/commit/ab212d35cb0312b7260f38fe1a3c69b4dce501c3">ab212d35</a> - <a href="https://task.beeznest.com/issues/23012">BT#23012</a>) Group: Fix random user assignation to group and only students no teacher</li>
<li>[2025-10-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/9b56878e244db4a5a919cb626459741315356975">9b56878e</a> - <a href="https://task.beeznest.com/issues/22963">BT#22963</a>) Learnpath: Enable drag-and-drop reordering for categories and lessons</li>
<li>[2025-10-09] (<a href="https://github.com/chamilo/chamilo-lms/commit/08debf33b7731adfef4ba7d004605d622b771de1">08debf33</a> - <a href="https://task.beeznest.com/issues/22974">BT#22974</a>) Internal: Fix opening file in fullscreen when sso forceredirect is activated because we need the config information</li>
<li>[2025-10-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/419daee2e290e749742695944d463b6317d9fa88">419daee2</a> - <a href="https://task.beeznest.com/issues/22768">BT#22768</a>) Language: Partial translation update for specific function user import with unique extrafield check</li>
<li>[2025-10-02] (<a href="https://github.com/chamilo/chamilo-lms/commit/8405a83a20c459279ab392012dab9f4877f8ca24">8405a83a</a>) Internal: Adjust subscribeUsersToSession to preserve course-level unsubscriptions when re-adding a user already subscribed to the session.</li>
<li>[2025-09-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/d1dcf0b7ecb8d73e6859c2757bb3209b0f472d93">d1dcf0b7</a> - <a href="https://task.beeznest.com/issues/22990">BT#22990</a>) Gradebook: Fix ranking calculation to show same position for users with the same score</li>
<li>[2025-09-23] (<a href="https://github.com/chamilo/chamilo-lms/commit/e3784d244fa769b0a8c7fec3d5548166bff0bfa1">e3784d24</a> - <a href="https://task.beeznest.com/issues/22959">BT#22959</a>) Learnpath: Fix learning path audio modification not accessible when in a session on the lp user subscription page</li>
<li>[2025-09-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/e63b2c5991ac8cb256dbd4a7bf501580451d1a82">e63b2c59</a> - <a href="https://task.beeznest.com/issues/22959">BT#22959</a>) Learnpath: Fix learning path edition and configuration not accessible when in a session on the lp user subscripcion page</li>
<li>[2025-09-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/4a10e1c77d751219580d307e5be3d364af61c596">4a10e1c7</a> - <a href="https://task.beeznest.com/issues/22899">BT#22899</a>) Exercise: Prevent duplicate question creation on Hotspot zone validation</li>
<li>[2025-09-19] (<a href="https://github.com/chamilo/chamilo-lms/commit/82feaa2bcb0d936c71e4a53878f13b9e102b7e5e">82feaa2b</a>) Mystudent.php : Add optional subscription column in student session details</li>
<li>[2025-09-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/ee2489905409d1f1b2bbef06ededcb0f157f09e8">ee248990</a>) Internal: Use null coalescing operator for cleaner input handling</li>
<li>[2025-09-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/90745c0b567e31833ca18b3d9a0b685756d2d5e7">90745c0b</a>) Internal: Fix undefined array key</li>
<li>[2025-09-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/ec1e185fd455b65e458d572dd0aa95f6f1a61ce0">ec1e185f</a>) Learnpath: Fix conditional check for quiz result validation</li>
<li>[2025-09-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/69c6e08dc3594c88ab2834cc09c09e60e1c50e4c">69c6e08d</a> - <a href="https://task.beeznest.com/issues/22929">BT#22929</a>) Skill: Minor: Fix skill block to be visible only if user is logged and is not an anonymous user</li>
<li>[2025-09-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/ba58b26ddad82749bf2b54dfdaed805f3e07c7d4">ba58b26d</a>) Internal: Sanitize option text and use Display::tag for generating option elements See advisory GHSA-pxrh-3rcp-h7m6</li>
<li>[2025-09-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/405b142780fe1ea5d452409703dd028b77db7aff">405b1427</a> - <a href="https://task.beeznest.com/issues/22709">BT#22709</a>) Portfolio: Fix exporting to ZIP when files are not found</li>
<li>[2025-09-05] (<a href="https://github.com/chamilo/chamilo-lms/commit/d0e1aff93fc84c343cd6007510c87ddac05cb51c">d0e1aff9</a> - <a href="https://task.beeznest.com/issues/22775">BT#22775</a>) Session: [Minor] Fix tipo for correct date format</li>
<li>[2025-09-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/557bb4c0122a3cb55b42bbbd5ad69c7176eedd1a">557bb4c0</a>) Ticket: Improve search and export ticket</li>
<li>[2025-09-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/04c907e9c3400a632b61a342ecee3d41d4b1b5d3">04c907e9</a> - <a href="https://task.beeznest.com/issues/22714">BT#22714</a>) Internal: Announcement: Fix select user and unselect all for formvalidator reverting part of commit 1b7e0030d1c34</li>
<li>[2025-09-01] (<a href="https://github.com/chamilo/chamilo-lms/commit/32e03374893969bbd393aa0aa00b3f2d48ea1ba5">32e03374</a> - <a href="https://task.beeznest.com/issues/22884">BT#22884</a>) Attendance: Fix student attendance calendar list with time to be in local time</li>
<li>[2025-09-01] (<a href="https://github.com/chamilo/chamilo-lms/commit/3534fec909991a49267461e9e13738e433eca51f">3534fec9</a>) Course description: Refactor course description handling to use Doctrine repository for improved data retrieval See advisory GHSA-p32q-6gh3-3gcv</li>
<li>[2025-08-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/adbe2138876fca039542077267eb8e9c7541145c">adbe2138</a> - <a href="https://task.beeznest.com/issues/22708">BT#22708</a>) WYSIWYG: Add automatic image resize before upload with ElFinder</li>
<li>[2025-08-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/63aafbcc8ec4fa6d936f13ae0e79536a0d20b072">63aafbcc</a> - <a href="https://task.beeznest.com/issues/22709">BT#22709</a>) Portfolio: Fix paths to handle video and audio tags when exporting to ZIP</li>
<li>[2025-08-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/6d99fd97a0dcad4398fe78b2165d2b9aa7f1d0cc">6d99fd97</a> - <a href="https://task.beeznest.com/issues/22709">BT#22709</a>) Portfolio: Include files linked in post and comments when exporting ZIP file</li>
<li>[2025-08-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/b0c75f40542070e7ee556324fbade7bb34d29e50">b0c75f40</a> - <a href="https://task.beeznest.com/issues/22709">BT#22709</a>) Portfolio: Fix undefined variable E_NOTICE</li>
<li>[2025-08-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/5f24fcaad7417e8e2a23d0302ea4ff92e4e20898">5f24fcaa</a>) Plugin: LTI: Fix translation of messages when replying tool in session courses</li>
<li>[2025-08-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/581252c31075d6082beb20a60daaa69c1b68da53">581252c3</a>) Tracking: #improve-option-summary-section-in-mystudent.php</li>
<li>[2025-08-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/216fd7d1d40aea49d73da285f2c63b966f567ae2">216fd7d1</a>) Plugin: LTI: Fix message when replying tool in session courses</li>
<li>[2025-08-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/dd8a919069ed6b35d7e1fe336912eeae0807b6c1">dd8a9190</a>) Tracking: [Minor] Add partial translation for new funcionality progress summary section in the sessions in mystudents</li>
<li>[2025-08-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/2f4d91317a913f1ff38b6d6fb1b26ba351f147cd">2f4d9131</a> - <a href="https://github.com/chamilo/chamilo-lms/issues/6531">GH#6531</a>) Tracking: Add option to enable progress summary section in the sessions in mystudents.php</li>
<li>[2025-07-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/0907f72a584f0453b67210c7140f12e6346c62a0">0907f72a</a>) Tracking: Fix total courses progress calculation in session in myStudent.php to consider only subscribed courses</li>
<li>[2025-07-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/660730fa6f07fc109dc87d7b8412ab556fb577ca">660730fa</a>) Tracking: Add PDF export to time report</li>
<li>[2025-07-29] (<a href="https://github.com/chamilo/chamilo-lms/commit/95083f4adce4bd2ac062d827f99e590a2e0dd6a9">95083f4a</a>) Exercise: Fix pending attempts Excel export to take into account question type and session users filters</li>
<li>[2025-07-25] (<a href="https://github.com/chamilo/chamilo-lms/commit/3eda9b4caca071e12d3922d4f8a54253de87a472">3eda9b4c</a> - <a href="https://task.beeznest.com/issues/22802">BT#22802</a>) User: Fix error when extra_field_to_validate_on_user_registration is not set</li>
<li>[2025-07-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/4c3fe93c0e2b02eb43cd9c42461b445bc5d01937">4c3fe93c</a> - <a href="https://task.beeznest.com/issues/22802">BT#22802</a>) User: Improve user import and extrafield validation</li>
<li>[2025-07-24] (<a href="https://github.com/chamilo/chamilo-lms/commit/75ef74554322f26ed883433e0460e212f66210af">75ef7455</a> - <a href="https://task.beeznest.com/issues/22802">BT#22802</a>) User: Enforce unique extrafield per URL on user creation</li>
<li>[2025-07-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/52e70db47bdafc681112a7f10c77ca74440d1144">52e70db4</a>) Plugin: GoogleMeet: Fix missing icon for Google Meet plugin on course homepage Author: @christianbeeznest</li>
<li>[2025-07-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/f8899d001a147c97de7c1c5566cefc64d0f3f6f4">f8899d00</a>) Ticket: Add subject to ticket xls export</li>
<li>[2025-07-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/1957a39d6cb56559f9ae87cafd21310961f04690">1957a39d</a> - <a href="https://task.beeznest.com/issues/22775">BT#22775</a>) Session: Add option to limit number of letter to show for extra field value, define specific value for course field #change date format and show learner variable for each student line in Excel export</li>
<li>[2025-07-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/a037586b112cfbffdbc4d08a827f463667321485">a037586b</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Documentation: Fixed logging message about replacing in tests when it replaces it everywhere (creates false asumption that it is slowlier than reality)</li>
<li>[2025-07-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/9093557d808eaf5a65d2b581932e9e33a20f1971">9093557d</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Documentation: Add more detailed logging to replace_course_code.php</li>
<li>[2025-07-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/5dc48f19e640dfc3c46a28d793c613753ad1b805">5dc48f19</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Documentation: Add more detailed logging to replace_course_code.php</li>
<li>[2025-07-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/69dcd65d89ed26b3561f6a2c8facd8cf9007bbe9">69dcd65d</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Documentation: Add PHPDoc to api_replace_terms_in_content</li>
<li>[2025-07-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/10794fc1e1fc43e2978e1123f0b655a50d759b41">10794fc1</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Script: Escape & sign in find to end cidReq search & replace syntax and avoid gobbing up larger course codes</li>
<li>[2025-07-18] (<a href="https://github.com/chamilo/chamilo-lms/commit/0f3d00c4a06a499f865c03532e1bd0733cb84638">0f3d00c4</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Script: Add & sign to end cidReq search & replace syntax and avoid gobbing up larger course codes</li>
<li>[2025-07-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/890acb26d7f63eea19abf5dd20c65c8eef1fe079">890acb26</a>) Internal: Move ExerciseLib::replaceTermsInContent() function to api.lib.php</li>
<li>[2025-07-15] (<a href="https://github.com/chamilo/chamilo-lms/commit/cc5805494e0041cbf28d8138b17fe952e43e81bf">cc580549</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Script: Add conversion of cidReq markers in HTML files to replace_course_code script + options to preview without change</li>
<li>[2025-07-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/aff71f39059fad28b1c1852e1e62244701d8b2f3">aff71f39</a> - <a href="https://task.beeznest.com/issues/22763">BT#22763</a>) Internal: Fix encoding error with ' in language variables</li>
<li>[2025-07-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/4c4edfed3d471e58ab36a2b1eea3321bc4504c97">4c4edfed</a> - <a href="https://task.beeznest.com/issues/22723">BT#22723</a>) Language: Add partial translation in ES, FR, EN for theoretical time functionnality</li>
<li>[2025-07-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/a3d222d1d368b49164d21616645fd66c38068718">a3d222d1</a> - <a href="https://task.beeznest.com/issues/22722">BT#22722</a>) Ticket: Fix date parameters for advanced search</li>
<li>[2025-07-04] (<a href="https://github.com/chamilo/chamilo-lms/commit/4f70bccd55d987f110b97ac2eaaa049806bcbccc">4f70bccd</a> - <a href="https://task.beeznest.com/issues/22722">BT#22722</a>) Ticket: Fix date parameters for excel export</li>
<li>[2025-07-03] (<a href="https://github.com/chamilo/chamilo-lms/commit/3322b97c7dd202ba4dd571d586c5c59fe02ba138">3322b97c</a> - <a href="https://task.beeznest.com/issues/22724">BT#22724</a>) Session: Replace session course ordering by arrows for drag and drop on resume_session and always available</li>
<li>[2025-06-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/cc625edf76dacc01b9f33cc8a18fe2698849380a">cc625edf</a>) CI: Use "not allowed" instead of "not authorized" in course feature</li>
<li>[2025-06-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/f93c3960b676830d17a6fa7211f99ab038daef9e">f93c3960</a>) CI: Use "not allowed" instead of "not authorized" in access company reports behat test</li>
<li>[2025-06-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/431a26a8708b0f54891784614af82d123b74f189">431a26a8</a>) CI: Fix classes feature</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-09-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/92e7d411e9ed1206a527030d9f9256588afe231b">92e7d411</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Webservice: Add update_session_from_extra_field WS - fix typo</li>
<li>[2025-09-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/09fb91d4141b3983c468e84825d44ef77c9c9236">09fb91d4</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Webservice: Add update_session_from_extra_field WS</li>
<li>[2025-09-22] (<a href="https://github.com/chamilo/chamilo-lms/commit/806f347034ee14f4d0ae5deea6f00aa2337e36e8">806f3470</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Webservice: Add subscribe_user_to_session_from_extra_field WS</li>
<li>[2025-09-21] (<a href="https://github.com/chamilo/chamilo-lms/commit/972456724d2b7dd01f393de7045da3a05ea47fff">97245672</a> - <a href="https://task.beeznest.com/issues/22302">BT#22302</a>) Webservice: Add subscribe_course_to_session_from_extra_field WS</li>
<li>[2026-01-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/b0ef139dadae47a55cfd7d4f8fbb81efdb68d8d5">b0ef139d</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Add `add_session_course_coaches` action</li>
<li>[2026-01-12] (<a href="https://github.com/chamilo/chamilo-lms/commit/430c33b44e642dff7e982d4baa8ea4ab7bf79aaa">430c33b4</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Improve course search response with detailed course information</li>
<li>[2026-01-08] (<a href="https://github.com/chamilo/chamilo-lms/commit/94d14ba702b8ea10ba07a4efe6ce1feea9321087">94d14ba7</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Refactor session retrieval APIs to avoid `$_POST` usage and improve type safety</li>
<li>[2026-01-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/9e1dc9a204064049ff30c968e17d0997f0a4be9f">9e1dc9a2</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Log course code and session ID in GET_COURSE_BY_CODE requests</li>
<li>[2026-01-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/fe4ce2182776732de769c5a66bcbb8bc718c0ed8">fe4ce218</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: allows getting user extra fields in request made by teachers</li>
<li>[2026-01-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/22457947b6749da5cded10f1da9e639c45b7867c">22457947</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Avoid to use $_POST and validate the `get_user_info_from_username` request</li>
<li>[2026-01-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/ba7f514f61f9ebc83705814354650c15ca55c62f">ba7f514f</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Allow passing course template during course creation</li>
<li>[2026-01-07] (<a href="https://github.com/chamilo/chamilo-lms/commit/43e19f89ede2347dec348da268b3c3c0064c8c11">43e19f89</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Refactor `addCourse` method to use `ParameterBag` for improved request handling</li>
<li>[2026-01-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/3b7f46da828e6683762e78ebe3d476cc401a8cb2">3b7f46da</a>) Webservice: RESTore composer.json</li>
<li>[2026-01-06] (<a href="https://github.com/chamilo/chamilo-lms/commit/d461887e9f9ed3a9dac507bdc0d4bead6e5195cc">d461887e</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Add GET_COURSE_BY_CODE API endpoint for retrieving course details by code</li>
<li>[2026-02-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/db0b0d99b34f11b86a0eb434d4bb299ec5399075">db0b0d99</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Restrict access to `GET_COURSE_GRADEBOOK`</li>
<li>[2026-02-27] (<a href="https://github.com/chamilo/chamilo-lms/commit/fbb022e66b9323f7372f32da25c5d6569af1e402">fbb022e6</a> - <a href="https://task.beeznest.com/issues/23138">BT#23138</a>) Webservice: Add `GET_COURSE_GRADEBOOK` action to retrieve gradebook data</li>
</ul>
<h3>Removals</h3>
<ul aria-live="off">
<li>Some very difficult to enable, unofficial, undocumented CMS/FAQ management features have been removed.</li>
</ul>
<h3>Known issues</h3>
<ul aria-live="off">
<li>Compatibility with PHP 8.3 is not fully tested. Where it might work, we do depend on legacy Symfony and Doctrine versions that are not compatible with PHP 8, so this package is built using PHP 7.4. It has reasonable chances to work in a PHP 8.3 environment, but if this is critical for you, we suggest starting with Chamilo 2.0.</li>
</ul>
</div>
<div class="version" aria-label="1.11.32"> <div class="version" aria-label="1.11.32">
<a id="1.11.32"></a> <a id="1.11.32"></a>
<h1>Chamilo 1.11.32 - Tikal, 27/06/2025</h1> <h1>Chamilo 1.11.32 - Tikal, 27/06/2025</h1>
@@ -248,12 +656,12 @@
<h3>Improvements (minor features) and debug</h3> <h3>Improvements (minor features) and debug</h3>
In reverse chronological order... In reverse chronological order...
<ul aria-live="off"> <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/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/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-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/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/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</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/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/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/77c1b6cb75751b197c5241f6d337acaf671a1632">77c1b6cb</a>) CI: Force use of PHP 7.4 in automated tests building sequence</li>
@@ -289,7 +697,7 @@
<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-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-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-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-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/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/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/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>
@@ -298,7 +706,7 @@
<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/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/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-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-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/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/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/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>
@@ -334,8 +742,8 @@
<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-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/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-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/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-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-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-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/07ce29dcfbf5a34a317eb2b26a642c69ad4598c7">07ce29dc</a>) Plugin: Azure: User the id property instead of objectId from resource</li>
@@ -366,9 +774,9 @@
<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-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/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-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/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-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-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-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/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/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>
@@ -461,9 +869,9 @@
<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-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-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-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-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-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> <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> </ul>
<h3>Removals</h3> <h3>Removals</h3>
<ul aria-live="off"> <ul aria-live="off">
@@ -17296,6 +17704,27 @@ a simple videoconferencing interface.</p>
<li>(#scorm): indicates that this feature relates to the SCORM standard</li> <li>(#scorm): indicates that this feature relates to the SCORM standard</li>
<li>(#fresh-users): indicates that this feature helps prevent drama with fresh users lacking experience and prone to error</li> <li>(#fresh-users): indicates that this feature helps prevent drama with fresh users lacking experience and prone to error</li>
</ul> </ul>
<h3>Commit messages syntax</h3>
We try to use a structured commit messages syntax that matches the above, so the changelog can be partially generated by a script of ours.<br />
A typical commit message should look like this:
<pre>
Exercise: Fix error changing test visibility - refs GH#1234
</pre>
The structure goes like this:
<pre>
[Minor: ][Tool: ][Action] [topic] [- refs [repo]#[issue ID]]
</pre>
Where:
<ul>
<li><em>Minor: </em> is a prefix *only used* if the change does not affect any logic and does not have any notable visual effect (documentation change, small spacing change in the interface, etc) so we know we can ignore it when searching for breaking changes. Otherwise just do not use that prefix and go straight to the Tool. Note: the space after the colon is important.</li>
<li><em>Tool: </em> is the name of any of the tools defined in the section above (starting with an uppercase letter). Note: the space after the colon is important.</li>
<li><em>Action</em> is a non-conjugated verb like "Fix", "Add", "Remove", "Move" etc. It defines the type of action undertaken by this change.</li>
<li><em>topic</em> is a continuation of the <em>Action</em> to explain the object that was changed and/or the effect that the change will have.</li>
<li><em> - refs </em> is a special keyword to link this change to some issue. Note: spaces around " - refs " are important.</li>
<li><em>repo</em> is a marker that identifies the location of the related issue: <em>GH</em>(or nothing) is for an issue number on Github, <em>BT</em> is for internal issues at BeezNest, and you should add your code if the change relates to your internal system.</li>
<li><em>issue ID</em> is the numerical ID of the corresponding issue in the given repo.</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
+33
View File
@@ -29,6 +29,7 @@
<li><a href="#9.Change-password-first-login">Change password on first login</a></li> <li><a href="#9.Change-password-first-login">Change password on first login</a></li>
<li><a href="#10.Hide-breadcrumb">Hide breadcrumb on unauthorized page load</a></li> <li><a href="#10.Hide-breadcrumb">Hide breadcrumb on unauthorized page load</a></li>
<li><a href="#11.SVG-and-XSS">SVG and XSS</a></li> <li><a href="#11.SVG-and-XSS">SVG and XSS</a></li>
<li><a href="#12.Template-files-access">Restricting access to template files</a></li>
</ol> </ol>
<h2><a id="1.Disclosing-server-info"></a>1. Disclosing server info</h2> <h2><a id="1.Disclosing-server-info"></a>1. Disclosing server info</h2>
@@ -280,6 +281,38 @@ This will prevent direct access to your settings and make it seem totally the sa
</ul> </ul>
</p> </p>
<h2><a id="12.Template-files-access"></a>12. Restricting access to template files</h2>
<p>
Twig template files (<code>.tpl</code>) under <code>main/template/</code> are
not meant to be served directly over HTTP. They are loaded by PHP from the
filesystem. If left accessible, they expose internal application logic,
AJAX endpoint URLs, admin panel structure, and variable names to
unauthenticated users.
</p>
<p>
Chamilo ships a <code>.htaccess</code> file in <code>main/template/</code>
that blocks direct access. If your Apache configuration does not support
<code>.htaccess</code> overrides, add the following to your VirtualHost
definition (replace <code>/var/www/URL</code> with your Chamilo root):
</p>
<pre>
&lt;Directory /var/www/URL/main/template&gt;
&lt;FilesMatch "\.tpl$"&gt;
Require all denied
&lt;/FilesMatch&gt;
&lt;/Directory&gt;
</pre>
<p>
For Nginx, add this rule near the top of your location blocks (before
any generic location rules) so it takes priority:
</p>
<pre>
location ~* \.tpl$ {
deny all;
return 403;
}
</pre>
<h2>Authors</h2> <h2>Authors</h2>
<ul> <ul>
<li>Yannick Warnier, Chamilo Project Leader, Zend Certified PHP Engineer, BeezNest Belgium SPRL, <li>Yannick Warnier, Chamilo Project Leader, Zend Certified PHP Engineer, BeezNest Belgium SPRL,
+3 -1
View File
@@ -195,7 +195,9 @@ $controller->tpl->assign('navigation_links', $controller->return_navigation_link
$controller->tpl->assign('notice_block', $controller->return_notice()); $controller->tpl->assign('notice_block', $controller->return_notice());
$controller->tpl->assign('help_block', $controller->return_help()); $controller->tpl->assign('help_block', $controller->return_help());
$controller->tpl->assign('student_publication_block', $controller->studentPublicationBlock()); $controller->tpl->assign('student_publication_block', $controller->studentPublicationBlock());
$controller->tpl->assign('skills_block', $controller->returnSkillLinks()); if (!api_is_anonymous() && api_user_is_login()) {
$controller->tpl->assign('skills_block', $controller->returnSkillLinks());
}
if (api_is_anonymous()) { if (api_is_anonymous()) {
$controller->tpl->setLoginBodyClass(); $controller->tpl->setLoginBodyClass();
+3 -2
View File
@@ -40,7 +40,7 @@ echo '</div>';
api_display_tool_title($tool_name); api_display_tool_title($tool_name);
if ($_POST['form_sent']) { if (!empty($_POST['form_sent']) && Security::check_token('post')) {
$form_sent = $_POST['form_sent']; $form_sent = $_POST['form_sent'];
$users = is_array($_POST['user_list']) ? $_POST['user_list'] : []; $users = is_array($_POST['user_list']) ? $_POST['user_list'] : [];
$url_list = is_array($_POST['url_list']) ? $_POST['url_list'] : []; $url_list = is_array($_POST['url_list']) ? $_POST['url_list'] : [];
@@ -79,7 +79,7 @@ $first_letter_user_lower = Database::escape_string(api_strtolower($first_letter_
$target_name = api_sort_by_first_name() ? 'firstname' : 'lastname'; $target_name = api_sort_by_first_name() ? 'firstname' : 'lastname';
$target_name = 'lastname'; $target_name = 'lastname';
$sql = "SELECT user_id,lastname,firstname,username FROM $tbl_user $sql = "SELECT user_id,lastname,firstname,username FROM $tbl_user
WHERE ".$target_name." LIKE '".$first_letter_user_lower."%' OR ".$target_name." LIKE '".$first_letter_user_lower."%' WHERE ".$target_name." LIKE '".$first_letter_user_lower."%'
ORDER BY ".(count($users) > 0 ? "(user_id IN(".implode(',', $users).")) DESC," : "")." ".$target_name; ORDER BY ".(count($users) > 0 ? "(user_id IN(".implode(',', $users).")) DESC," : "")." ".$target_name;
$result = Database::query($sql); $result = Database::query($sql);
$db_users = Database::store_result($result); $db_users = Database::store_result($result);
@@ -93,6 +93,7 @@ unset($result);
<form name="formulaire" method="post" action="<?php echo api_get_self(); ?>" style="margin:0px;"> <form name="formulaire" method="post" action="<?php echo api_get_self(); ?>" style="margin:0px;">
<input type="hidden" name="form_sent" value="1"/> <input type="hidden" name="form_sent" value="1"/>
<?php echo Security::get_HTML_token(); ?>
<table border="0" cellpadding="5" cellspacing="0" width="100%"> <table border="0" cellpadding="5" cellspacing="0" width="100%">
<tr> <tr>
<td width="40%" align="center"> <td width="40%" align="center">
+2 -1
View File
@@ -67,7 +67,7 @@ $errorMsg = '';
$users = $sessions = []; $users = $sessions = [];
$promotion = new Promotion(); $promotion = new Promotion();
$id = intval($_GET['id']); $id = intval($_GET['id']);
if (isset($_POST['form_sent']) && $_POST['form_sent']) { if (isset($_POST['form_sent']) && $_POST['form_sent'] && Security::check_token('post')) {
$form_sent = $_POST['form_sent']; $form_sent = $_POST['form_sent'];
$session_in_promotion_posted = $_POST['session_in_promotion_name']; $session_in_promotion_posted = $_POST['session_in_promotion_name'];
if (!is_array($session_in_promotion_posted)) { if (!is_array($session_in_promotion_posted)) {
@@ -157,6 +157,7 @@ if ($add_type == 'multiple') {
echo Display::input('hidden', 'id', $id); echo Display::input('hidden', 'id', $id);
echo Display::input('hidden', 'form_sent', '1'); echo Display::input('hidden', 'form_sent', '1');
echo Display::input('hidden', 'add_type', null); echo Display::input('hidden', 'add_type', null);
echo Security::get_HTML_token();
if (!empty($errorMsg)) { if (!empty($errorMsg)) {
echo Display::return_message($errorMsg, 'normal'); //main API echo Display::return_message($errorMsg, 'normal'); //main API
} }
+1 -1
View File
@@ -95,7 +95,7 @@ if ($usergroup->allowTeachers()) {
$onlyThisSessionList = array_column($sessionList, 'id'); $onlyThisSessionList = array_column($sessionList, 'id');
} }
} }
$session_list = SessionManager::get_sessions_list([], ['name'], null, null, 0, $onlyThisSessionList); $session_list = SessionManager::get_sessions_list([], ['name'], null, null, 0, $onlyThisSessionList, true);
$elements_not_in = $elements_in = []; $elements_not_in = $elements_in = [];
if (!empty($session_list)) { if (!empty($session_list)) {
+57 -57
View File
@@ -26,19 +26,68 @@ $course_list = CourseManager::get_courses_list(
'', '',
api_get_current_access_url_id() api_get_current_access_url_id()
); );
$formSent = null;
$courses = $selected_courses = []; $courses = $selected_courses = [];
if (isset($_POST['formSent']) && $_POST['formSent']) { $form = new FormValidator('export', 'post', api_get_self());
$formSent = $_POST['formSent']; $form->addHeader($tool_name);
$select_type = (int) ($_POST['select_type']); $form->addElement(
$file_type = $_POST['file_type']; 'radio',
$includeUsers = (empty($_POST['include_users']) ? false : true); 'select_type',
$includeExtraFields = (isset($_POST['include_extrafields']) && 1 === (int) $_POST['include_extrafields']); get_lang('Option'),
get_lang('ExportAllCoursesList'),
'1',
['onclick' => "javascript: if(this.checked){document.getElementById('div-course-list').style.display='none';}"]
);
$form->addElement(
'radio',
'select_type',
'',
get_lang('ExportSelectedCoursesFromCoursesList'),
'2',
['onclick' => "javascript: if(this.checked){document.getElementById('div-course-list').style.display='block';}"]
);
if (!empty($course_list)) {
$form->addHtml('<div id="div-course-list" style="display:none">');
$coursesInList = [];
foreach ($course_list as $course) {
$coursesInList[$course['code']] = $course['title'].' ('.$course['code'].')';
}
$form->addSelect(
'course_code',
get_lang('WhichCoursesToExport'),
$coursesInList,
['multiple' => 'multiple']
);
$form->addHtml('</div>');
}
$form->addElement('radio', 'file_type', get_lang('OutputFileType'), 'CSV', 'csv', null);
$form->addElement('radio', 'file_type', '', 'XLS', 'xls', null);
$form->addElement('radio', 'file_type', null, 'XML', 'xml', null, ['id' => 'file_type_xml']);
$form->addElement('checkbox', 'include_users', get_lang('ExportUsers'), '', '1');
$form->addElement('checkbox', 'include_extrafields', get_lang('ExportExtraFields'), '', '1');
$form->setDefaults(['select_type' => '1', 'file_type' => 'csv', 'include_users' => '1', 'include_extrafields' => 0]);
$form->addButtonExport(get_lang('ExportCourses'));
$form->protect();
if ($form->validate()) {
$values = $form->exportValues();
$select_type = (int) $values['select_type'];
$file_type = $values['file_type'];
$includeUsers = !empty($values['include_users']);
$includeExtraFields = isset($values['include_extrafields']) && 1 === (int) $values['include_extrafields'];
if (2 == $select_type) { if (2 == $select_type) {
// Get selected courses from courses list in form sent // Get selected courses from courses list in form sent
$selected_courses = $_POST['course_code']; $selected_courses = $values['course_code'] ?? [];
if (is_array($selected_courses)) { if (is_array($selected_courses)) {
foreach ($course_list as $course) { foreach ($course_list as $course) {
if (!in_array($course['code'], $selected_courses)) { if (!in_array($course['code'], $selected_courses)) {
@@ -156,55 +205,6 @@ if (isset($_POST['formSent']) && $_POST['formSent']) {
Display::display_header($tool_name); Display::display_header($tool_name);
$form = new FormValidator('export', 'post', api_get_self());
$form->addHeader($tool_name);
$form->addHidden('formSent', 1);
$form->addElement(
'radio',
'select_type',
get_lang('Option'),
get_lang('ExportAllCoursesList'),
'1',
['onclick' => "javascript: if(this.checked){document.getElementById('div-course-list').style.display='none';}"]
);
$form->addElement(
'radio',
'select_type',
'',
get_lang('ExportSelectedCoursesFromCoursesList'),
'2',
['onclick' => "javascript: if(this.checked){document.getElementById('div-course-list').style.display='block';}"]
);
if (!empty($course_list)) {
$form->addHtml('<div id="div-course-list" style="display:none">');
$coursesInList = [];
foreach ($course_list as $course) {
$coursesInList[$course['code']] = $course['title'].' ('.$course['code'].')';
}
$form->addSelect(
'course_code',
get_lang('WhichCoursesToExport'),
$coursesInList,
['multiple' => 'multiple']
);
$form->addHtml('</div>');
}
$form->addElement('radio', 'file_type', get_lang('OutputFileType'), 'CSV', 'csv', null);
$form->addElement('radio', 'file_type', '', 'XLS', 'xls', null);
$form->addElement('radio', 'file_type', null, 'XML', 'xml', null, ['id' => 'file_type_xml']);
$form->addElement('checkbox', 'include_users', get_lang('ExportUsers'), '', '1');
$form->addElement('checkbox', 'include_extrafields', get_lang('ExportExtraFields'), '', '1');
$form->setDefaults(['select_type' => '1', 'file_type' => 'csv', 'include_users' => '1', 'include_extrafields' => 0]);
$form->addButtonExport(get_lang('ExportCourses'));
$form->display(); $form->display();
Display::display_footer(); Display::display_footer();
+15 -13
View File
@@ -215,9 +215,23 @@ $tool_name = get_lang('ImportCourses').' CSV';
$interbreadcrumb[] = ['url' => 'index.php', 'name' => get_lang('PlatformAdmin')]; $interbreadcrumb[] = ['url' => 'index.php', 'name' => get_lang('PlatformAdmin')];
set_time_limit(0); set_time_limit(0);
$form = new FormValidator(
'import',
'post',
api_get_self(),
null,
['enctype' => 'multipart/form-data']
);
$form->addHeader($tool_name);
$form->addElement('file', 'import_file', get_lang('ImportCSVFileLocation'));
$form->addElement('checkbox', 'add_me_as_teacher', null, get_lang('AddMeAsTeacherInCourses'));
$form->addButtonImport(get_lang('Import'), 'save');
$form->protect();
Display::display_header($tool_name); Display::display_header($tool_name);
if (isset($_POST['formSent']) && $_POST['formSent']) { if ($form->validate()) {
if (empty($_FILES['import_file']['tmp_name'])) { if (empty($_FILES['import_file']['tmp_name'])) {
$error_message = get_lang('UplUploadFailed'); $error_message = get_lang('UplUploadFailed');
echo Display::return_message($error_message, 'error', false); echo Display::return_message($error_message, 'error', false);
@@ -250,18 +264,6 @@ if (isset($errors) && count($errors) != 0) {
echo Display::return_message($error_message, 'error', false); echo Display::return_message($error_message, 'error', false);
} }
$form = new FormValidator(
'import',
'post',
api_get_self(),
null,
['enctype' => 'multipart/form-data']
);
$form->addHeader($tool_name);
$form->addElement('file', 'import_file', get_lang('ImportCSVFileLocation'));
$form->addElement('checkbox', 'add_me_as_teacher', null, get_lang('AddMeAsTeacherInCourses'));
$form->addButtonImport(get_lang('Import'), 'save');
$form->addElement('hidden', 'formSent', 1);
$form->display(); $form->display();
?> ?>
+2 -1
View File
@@ -155,7 +155,7 @@ $errorMsg = $firstLetterCourse = '';
$UserList = []; $UserList = [];
$msg = ''; $msg = '';
if (isset($_POST['formSent']) && intval($_POST['formSent']) == 1) { if (isset($_POST['formSent']) && intval($_POST['formSent']) == 1 && Security::check_token('post')) {
$courses_list = isset($_POST['CoursesList']) ? $_POST['CoursesList'] : []; $courses_list = isset($_POST['CoursesList']) ? $_POST['CoursesList'] : [];
$affected_rows = CourseManager::subscribeCoursesToDrhManager($user_id, $courses_list); $affected_rows = CourseManager::subscribeCoursesToDrhManager($user_id, $courses_list);
if ($affected_rows) { if ($affected_rows) {
@@ -219,6 +219,7 @@ $result = Database::query($sql);
?> ?>
<form name="formulaire" method="post" action="<?php echo api_get_self(); ?>?user=<?php echo $user_id; ?>" style="margin:0px;"> <form name="formulaire" method="post" action="<?php echo api_get_self(); ?>?user=<?php echo $user_id; ?>" style="margin:0px;">
<input type="hidden" name="formSent" value="1" /> <input type="hidden" name="formSent" value="1" />
<?php echo Security::get_HTML_token(); ?>
<?php <?php
if (!empty($msg)) { if (!empty($msg)) {
echo Display::return_message($msg, 'normal'); //main API echo Display::return_message($msg, 'normal'); //main API
+2
View File
@@ -543,11 +543,13 @@ if (api_is_platform_admin()) {
$blockPlatform['items'] = $items; $blockPlatform['items'] = $items;
} elseif (api_is_session_admin()) { } elseif (api_is_session_admin()) {
$items = []; $items = [];
if (api_get_configuration_value('session_admin_access_global_statistics')) {
$items[] = [ $items[] = [
'class' => 'item-stats', 'class' => 'item-stats',
'url' => 'statistics/index.php', 'url' => 'statistics/index.php',
'label' => get_lang('Statistics'), 'label' => get_lang('Statistics'),
]; ];
}
if (api_get_configuration_value('session_admin_access_system_announcement')) { if (api_get_configuration_value('session_admin_access_system_announcement')) {
$items[] = [ $items[] = [
'class' => 'item-global-announcement', 'class' => 'item-global-announcement',
+5 -3
View File
@@ -271,6 +271,8 @@ $language_data = [];
while ($row = Database::fetch_array($result_select)) { while ($row = Database::fetch_array($result_select)) {
$row_td = []; $row_td = [];
$row_td[] = $row['id']; $row_td[] = $row['id'];
// escape the original name for safe output in HTML attribute and body contexts
$originalName = htmlspecialchars($row['original_name'], ENT_QUOTES, 'UTF-8');
// the first column is the original name of the language OR a form containing the original name // 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']) { if ($action == 'edit' and $row['id'] == $_GET['id']) {
$checked = ''; $checked = '';
@@ -278,10 +280,10 @@ while ($row = Database::fetch_array($result_select)) {
$checked = ' checked="checked" '; $checked = ' checked="checked" ';
} }
$row_td[] = '<input type="hidden" name="edit_id" value="'.Security::remove_XSS($_GET['id']).'" /><input type="text" name="txt_name" value="'.$row['original_name'].'" /> ' $row_td[] = '<input type="hidden" name="edit_id" value="'.Security::remove_XSS($_GET['id']).'" /><input type="text" name="txt_name" value="'.$originalName.'" /> '
.'<input type="checkbox" '.$checked.'name="platformlanguage" id="platformlanguage" value="'.$row['english_name'].'" /><label for="platformlanguage">'.$row['original_name'].' '.get_lang('AsPlatformLanguage').'</label> <input type="submit" name="Submit" value="'.get_lang('Ok').'" /><a name="value" />'; .'<input type="checkbox" '.$checked.'name="platformlanguage" id="platformlanguage" value="'.htmlspecialchars($row['english_name'], ENT_QUOTES, 'UTF-8').'" /><label for="platformlanguage">'.$originalName.' '.get_lang('AsPlatformLanguage').'</label> <input type="submit" name="Submit" value="'.get_lang('Ok').'" /><a name="value" />';
} else { } else {
$row_td[] = $row['original_name']; $row_td[] = $originalName;
} }
// the second column // the second column
+2 -1
View File
@@ -97,7 +97,8 @@ if (empty($annee) && empty($course)) {
//$sr = @ ldap_search($ds, "ou=people,$LDAPbasedn", "(|(edupersonprimaryorgunitdn=ou=$etape,ou=$annee,ou=diploma,o=Paris1,$LDAPbasedn)(edupersonprimaryorgunitdn=ou=02PEL,ou=$annee,ou=diploma,o=Paris1,$LDAPbasedn))"); //$sr = @ ldap_search($ds, "ou=people,$LDAPbasedn", "(|(edupersonprimaryorgunitdn=ou=$etape,ou=$annee,ou=diploma,o=Paris1,$LDAPbasedn)(edupersonprimaryorgunitdn=ou=02PEL,ou=$annee,ou=diploma,o=Paris1,$LDAPbasedn))");
//echo "(ou=*$annee,ou=$composante)"; //echo "(ou=*$annee,ou=$composante)";
$sr = @ldap_search($ds, $ldap_basedn, "(ou=*$annee)"); $anneeFilter = ldap_escape($annee, "", LDAP_ESCAPE_FILTER);
$sr = @ldap_search($ds, $ldap_basedn, "(ou=*$anneeFilter)");
$info = ldap_get_entries($ds, $sr); $info = ldap_get_entries($ds, $sr);
@@ -102,7 +102,8 @@ elseif (!empty($annee) && !empty($id_session) && empty($_POST['confirmed'])) {
//$sr = @ ldap_search($ds, "ou=people,$LDAPbasedn", "(|(edupersonprimaryorgunitdn=ou=$etape,ou=$annee,ou=diploma,o=Paris1,$LDAPbasedn)(edupersonprimaryorgunitdn=ou=02PEL,ou=$annee,ou=diploma,o=Paris1,$LDAPbasedn))"); //$sr = @ ldap_search($ds, "ou=people,$LDAPbasedn", "(|(edupersonprimaryorgunitdn=ou=$etape,ou=$annee,ou=diploma,o=Paris1,$LDAPbasedn)(edupersonprimaryorgunitdn=ou=02PEL,ou=$annee,ou=diploma,o=Paris1,$LDAPbasedn))");
//echo "(ou=*$annee,ou=$composante)"; //echo "(ou=*$annee,ou=$composante)";
$sr = @ldap_search($ds, $ldap_basedn, "(ou=*$annee)"); $anneeFilter = ldap_escape($annee, "", LDAP_ESCAPE_FILTER);
$sr = @ldap_search($ds, $ldap_basedn, "(ou=*$anneeFilter)");
$info = ldap_get_entries($ds, $sr); $info = ldap_get_entries($ds, $sr);
+1
View File
@@ -86,6 +86,7 @@ if ($languageId) {
$form->addHidden('sub', $languageInfo['id']); $form->addHidden('sub', $languageInfo['id']);
$form->addHidden('sub_language_id', $languageInfo['id']); $form->addHidden('sub_language_id', $languageInfo['id']);
$form->addHidden('redirect', true); $form->addHidden('redirect', true);
$form->addHidden('sec_token', Security::get_token());
$form->addButtonSave(get_lang('Save')); $form->addButtonSave(get_lang('Save'));
} }
+10 -3
View File
@@ -960,8 +960,16 @@ switch ($report) {
case 'users_active': case 'users_active':
$content = ''; $content = '';
if ($validated) { if ($validated) {
$startDate = $values['daterange_start']; // Validate date inputs strictly. Security::remove_XSS() (used for
$endDate = $values['daterange_end']; // the display value above) does not protect against SQL injection.
$rawStartDate = isset($values['daterange_start']) ? $values['daterange_start'] : '';
$rawEndDate = isset($values['daterange_end']) ? $values['daterange_end'] : '';
$parsedStart = !empty($rawStartDate) ? DateTime::createFromFormat('Y-m-d', $rawStartDate) : false;
$parsedEnd = !empty($rawEndDate) ? DateTime::createFromFormat('Y-m-d', $rawEndDate) : false;
$startDate = (false !== $parsedStart && $parsedStart->format('Y-m-d') === $rawStartDate)
? Database::escape_string($rawStartDate) : '';
$endDate = (false !== $parsedEnd && $parsedEnd->format('Y-m-d') === $rawEndDate)
? Database::escape_string($rawEndDate) : '';
$graph = '<div class="row">'; $graph = '<div class="row">';
$graph .= '<div class="col-md-4"><canvas id="canvas1" style="margin-bottom: 20px"></canvas></div>'; $graph .= '<div class="col-md-4"><canvas id="canvas1" style="margin-bottom: 20px"></canvas></div>';
@@ -986,7 +994,6 @@ switch ($report) {
$conditions = []; $conditions = [];
$extraConditions = ''; $extraConditions = '';
if (!empty($startDate) && !empty($endDate)) { if (!empty($startDate) && !empty($endDate)) {
// $extraConditions is already cleaned inside the function getUserListExtraConditions
$extraConditions .= " AND registration_date BETWEEN '$startDate' AND '$endDate' "; $extraConditions .= " AND registration_date BETWEEN '$startDate' AND '$endDate' ";
} }
+2 -1
View File
@@ -38,7 +38,8 @@ $htmlHeadXtra[] = '<script>
\'file_id\': file_id, \'file_id\': file_id,
\'id\': '.intval($_REQUEST['id']).', \'id\': '.intval($_REQUEST['id']).',
\'sub\': '.intval($_REQUEST['sub_language_id']).', \'sub\': '.intval($_REQUEST['sub_language_id']).',
\'sub_language_id\': '.intval($_REQUEST['sub_language_id']).' \'sub_language_id\': '.intval($_REQUEST['sub_language_id']).',
\'sec_token\': \''.Security::get_token().'\'
}, },
success: function(datos) { success: function(datos) {
if (datos == "1") { if (datos == "1") {
+12
View File
@@ -244,6 +244,18 @@ if (isset($_POST['SubmitAddNewLanguage'])) {
//Fixes BT#1636 //Fixes BT#1636
$english_name = api_strtolower($english_name); $english_name = api_strtolower($english_name);
// Validate the folder name is safe for filesystem use
if (!SubLanguageManager::isValidLanguageFolderName($english_name)) {
$msg .= Display::return_message(
get_lang('EnglishName').' '.get_lang('ContainsIllegalCharacters'),
'error'
);
Display::display_header($language_name);
echo $msg;
Display::display_footer();
exit();
}
$isocode = str_replace(' ', '_', $isocode); $isocode = str_replace(' ', '_', $isocode);
$str_info = '<br/>'.get_lang('OriginalName').' : '.$original_name.'<br/>'.get_lang('EnglishName').' : '.$english_name.'<br/>'.get_lang('PlatformCharsetTitle').' : '.$isocode; $str_info = '<br/>'.get_lang('OriginalName').' : '.$original_name.'<br/>'.get_lang('EnglishName').' : '.$english_name.'<br/>'.get_lang('PlatformCharsetTitle').' : '.$isocode;
+12 -1
View File
@@ -13,6 +13,11 @@ require_once __DIR__.'/../inc/global.inc.php';
api_protect_admin_script(); api_protect_admin_script();
// CSRF protection
if (!isset($_REQUEST['sec_token']) || !Security::check_token('request')) {
exit;
}
$new_language = Security::remove_XSS($_REQUEST['new_language']); $new_language = Security::remove_XSS($_REQUEST['new_language']);
$language_variable = ltrim( $language_variable = ltrim(
Security::remove_XSS($_REQUEST['variable_language']), Security::remove_XSS($_REQUEST['variable_language']),
@@ -20,7 +25,7 @@ $language_variable = ltrim(
); );
$file_id = intval($_REQUEST['file_id']); $file_id = intval($_REQUEST['file_id']);
$variableIsValid = isset($language_variable) && preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $language_variable); $variableIsValid = SubLanguageManager::isValidLanguageVariable($language_variable);
if (isset($new_language) && $variableIsValid && isset($file_id)) { if (isset($new_language) && $variableIsValid && isset($file_id)) {
$file_language = $language_files_to_load[$file_id].'.inc.php'; $file_language = $language_files_to_load[$file_id].'.inc.php';
@@ -28,6 +33,12 @@ if (isset($new_language) && $variableIsValid && isset($file_id)) {
$sub_language_id = intval($_REQUEST['sub']); $sub_language_id = intval($_REQUEST['sub']);
$all_data_of_language = SubLanguageManager::get_all_information_of_sub_language($id_language, $sub_language_id); $all_data_of_language = SubLanguageManager::get_all_information_of_sub_language($id_language, $sub_language_id);
if (empty($all_data_of_language) ||
!SubLanguageManager::isValidLanguageFolderName($all_data_of_language['dokeos_folder'])
) {
exit;
}
$path_folder = api_get_path(SYS_LANG_PATH).$all_data_of_language['dokeos_folder'].'/'.$file_language; $path_folder = api_get_path(SYS_LANG_PATH).$all_data_of_language['dokeos_folder'].'/'.$file_language;
$all_file_of_directory = SubLanguageManager::get_all_language_variable_in_file($path_folder); $all_file_of_directory = SubLanguageManager::get_all_language_variable_in_file($path_folder);
$return_value = SubLanguageManager::add_file_in_language_directory($path_folder); $return_value = SubLanguageManager::add_file_in_language_directory($path_folder);
+6 -3
View File
@@ -278,7 +278,9 @@ $form->addGroup($group, 'mail', get_lang('SendMailToNewUser'));
$hideNeverExpiresOpt = api_get_configuration_value('user_hide_never_expire_option'); $hideNeverExpiresOpt = api_get_configuration_value('user_hide_never_expire_option');
$lblExpiration = ''; $lblExpiration = '';
$defaultExpiration = 0; $defaultExpiration = 0;
if ($hideNeverExpiresOpt) { $hideExpirationDate = api_get_configuration_value('user_hide_expiration_date_for_session_admin');
if (!$hideExpirationDate || api_is_platform_admin()) {
if ($hideNeverExpiresOpt && !api_is_platform_admin()) {
$lblExpiration = get_lang('ExpirationDate'); $lblExpiration = get_lang('ExpirationDate');
$defaultExpiration = 1; $defaultExpiration = 1;
$group = []; $group = [];
@@ -288,7 +290,7 @@ if ($hideNeverExpiresOpt) {
'expiration_date', 'expiration_date',
null null
); );
} else { } else {
$form->addElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('NeverExpires'), 0); $form->addElement('radio', 'radio_expiration_date', get_lang('ExpirationDate'), get_lang('NeverExpires'), 0);
$group = []; $group = [];
$group[] = $form->createElement('radio', 'radio_expiration_date', null, get_lang('Enabled'), 1); $group[] = $form->createElement('radio', 'radio_expiration_date', null, get_lang('Enabled'), 1);
@@ -300,8 +302,9 @@ if ($hideNeverExpiresOpt) {
'onchange' => 'javascript: enable_expiration_date();', 'onchange' => 'javascript: enable_expiration_date();',
] ]
); );
}
$form->addGroup($group, 'max_member_group', $lblExpiration, null, false);
} }
$form->addGroup($group, 'max_member_group', $lblExpiration, null, false);
// Active account or inactive account // Active account or inactive account
$form->addElement('radio', 'active', get_lang('ActiveAccount'), get_lang('Active'), 1); $form->addElement('radio', 'active', get_lang('ActiveAccount'), get_lang('Active'), 1);
+5 -2
View File
@@ -12,7 +12,7 @@ $this_section = SECTION_PLATFORM_ADMIN;
api_protect_admin_script(true); api_protect_admin_script(true);
$user_id = isset($_GET['user_id']) ? (int) $_GET['user_id'] : (int) $_POST['user_id']; $user_id = isset($_GET['user_id']) ? (int) $_GET['user_id'] : (int) $_POST['user_id'];
api_protect_super_admin($user_id, null, true); api_protect_super_admin($user_id, null, true !== api_get_configuration_value('disallow_session_admin_edit_users'));
$is_platform_admin = api_is_platform_admin() ? 1 : 0; $is_platform_admin = api_is_platform_admin() ? 1 : 0;
$userInfo = api_get_user_info($user_id); $userInfo = api_get_user_info($user_id);
$userEntity = api_get_user_entity($user_id); $userEntity = api_get_user_entity($user_id);
@@ -320,8 +320,10 @@ $form->addElement('label', get_lang('RegistrationDate'), $date);
$defaultExpiration = 0; $defaultExpiration = 0;
if (!$user_data['platform_admin']) { if (!$user_data['platform_admin']) {
$hideNeverExpiresOpt = api_get_configuration_value('user_hide_never_expire_option'); $hideNeverExpiresOpt = api_get_configuration_value('user_hide_never_expire_option');
$hideExpirationDate = api_get_configuration_value('user_hide_expiration_date_for_session_admin');
$lblExpiration = ''; $lblExpiration = '';
if ($hideNeverExpiresOpt) { if (!$hideExpirationDate || api_is_platform_admin()) {
if ($hideNeverExpiresOpt && !api_is_platform_admin()) {
$lblExpiration = get_lang('ExpirationDate'); $lblExpiration = get_lang('ExpirationDate');
$defaultExpiration = 1; $defaultExpiration = 1;
$group = []; $group = [];
@@ -345,6 +347,7 @@ if (!$user_data['platform_admin']) {
); );
} }
$form->addGroup($group, 'max_member_group', $lblExpiration, null, false); $form->addGroup($group, 'max_member_group', $lblExpiration, null, false);
}
// Active account or inactive account // Active account or inactive account
$form->addElement('radio', 'active', get_lang('ActiveAccount'), get_lang('Active'), 1); $form->addElement('radio', 'active', get_lang('ActiveAccount'), get_lang('Active'), 1);
+152 -26
View File
@@ -255,14 +255,12 @@ function complete_missing_data(array $user): array
/** /**
* Save the imported data. * Save the imported data.
* *
* @uses \global variable $inserted_in_course, which returns the list of * @uses global $inserted_in_course, which returns the list of courses the user was inserted in
* courses the user was inserted in *
* @return array The $users array, with 'message' and (for reused users) 'id' set
*/ */
function save_data( function save_data(array $users, bool $sendMail = false, ?string $targetFolder = null): array
array $users, {
bool $sendMail = false,
?string $targetFolder = null
): array {
global $inserted_in_course, $extra_fields; global $inserted_in_course, $extra_fields;
// Not all scripts declare the $inserted_in_course array (although they should). // Not all scripts declare the $inserted_in_course array (although they should).
@@ -281,12 +279,105 @@ function save_data(
$optionsByField = []; $optionsByField = [];
// which extrafield variable are we validating on?
$uniqueField = api_get_configuration_value('extra_field_to_validate_on_user_registration');
foreach ($users as &$user) { foreach ($users as &$user) {
if ($user['has_error']) { if ($user['has_error']) {
$userError[] = $user; $userError[] = $user;
continue; continue;
} }
$returnMessage = '';
$user_id = null;
// 1) If the CSV row has that unique extrafield, try to look up an existing user
if (!empty($uniqueField) && !empty($user[$uniqueField])) {
// pass true to getUserId mode
$existing = UserManager::isExtraFieldValueUniquePerUrl($user[$uniqueField], true);
if ($existing !== null) {
// existing user found → reuse
$user_id = $existing;
$returnMessage = Display::return_message(
sprintf(
get_lang('ExistingUserWithSameExtraFieldValue'),
$uniqueField,
$existing
),
'info'
);
$userInfo = api_get_user_info($user_id);
$firstName = $user['FirstName'] ?? $userInfo['firstname'];
$lastName = $user['LastName'] ?? $userInfo['lastname'];
$userName = $userInfo['username'];
if (!empty($user['UserName'])) {
$userName = $user['UserName'];
}
$changePassMethod = 0;
$password = null;
$authSource = $userInfo['auth_source'];
if (isset($user['Password'])) {
$changePassMethod = 2;
$password = $user['Password'];
}
if (isset($user['AuthSource']) && $user['AuthSource'] != $authSource) {
$authSource = $user['AuthSource'];
$changePassMethod = 3;
}
$email = $user['Email'] ?? $userInfo['email'];
$status = api_status_key($user['Status']) ?? $userInfo['status'];
$officialCode = $user['OfficialCode'] ?? $userInfo['official_code'];
$phone = $user['PhoneNumber'] ?? $userInfo['phone'];
$pictureUrl = $user['PictureUri'] ?? $userInfo['picture_uri'];
$expirationDate = $user['ExpiryDate'] ?? $userInfo['expiration_date'];
// Fix wrong date in DB for old users (sometimes would be expiration_date = '9999-12-31 ********') where it should be null
if (substr($expirationDate, 0, 4) === '9999') {
$expirationDate = null;
}
$active = $userInfo['active'];
if (isset($user['Active'])) {
$user['Active'] = (int) $user['Active'];
if (-1 === $user['Active']) {
$user['Active'] = 0;
}
$active = $user['Active'];
}
$creatorId = $userInfo['creator_id'];
$hrDeptId = $userInfo['hr_dept_id'];
$language = $user['Language'] ?? $userInfo['language'];
UserManager::update_user(
$user_id,
$firstName,
$lastName,
$userName,
$password,
$authSource,
$email,
$status,
$officialCode,
$phone,
$pictureUrl,
$expirationDate,
$active,
$creatorId,
$hrDeptId,
$extra,
$language,
'',
false,
$changePassMethod
);
}
}
// 2) If not found, go through normal creation
if ($user_id === null) {
// fill in missing fields, generate password, etc.
$user = complete_missing_data($user); $user = complete_missing_data($user);
$user['Status'] = api_status_key($user['Status']); $user['Status'] = api_status_key($user['Status']);
$redirection = $user['Redirection'] ?? ''; $redirection = $user['Redirection'] ?? '';
@@ -320,19 +411,29 @@ function save_data(
if ($user_id) { if ($user_id) {
$returnMessage = Display::return_message(get_lang('UserAdded'), 'success'); $returnMessage = Display::return_message(get_lang('UserAdded'), 'success');
} else {
$returnMessage = Display::return_message(get_lang('Error'), 'error');
$userWarning[] = $user;
$user['message'] = $returnMessage;
continue;
}
}
// 3) At this point $user_id is either reused or newly created.
// Enroll in courses:
if (isset($user['Courses']) && is_array($user['Courses'])) { if (isset($user['Courses']) && is_array($user['Courses'])) {
foreach ($user['Courses'] as $course) { foreach ($user['Courses'] as $course) {
if (CourseManager::course_exists($course)) { if (CourseManager::course_exists($course)) {
$result = CourseManager::subscribeUser($user_id, $course, $user['Status']); $result = CourseManager::subscribeUser($user_id, $course, $user['Status']);
if ($result) { if ($result) {
$course_info = api_get_course_info($course); $info = api_get_course_info($course);
$inserted_in_course[$course] = $course_info['title']; $inserted_in_course[$course] = $info['title'];
} }
} }
} }
} }
// 4) Enroll in sessions:
if (isset($user['Sessions']) && is_array($user['Sessions'])) { if (isset($user['Sessions']) && is_array($user['Sessions'])) {
foreach ($user['Sessions'] as $sessionId) { foreach ($user['Sessions'] as $sessionId) {
$sessionInfo = api_get_session_info($sessionId); $sessionInfo = api_get_session_info($sessionId);
@@ -347,41 +448,47 @@ function save_data(
} }
} }
// 5) Subscribe to usergroups:
if (!empty($user['ClassId'])) { if (!empty($user['ClassId'])) {
$classId = explode('|', trim($user['ClassId'])); $classIds = explode('|', trim($user['ClassId']));
foreach ($classId as $id) { foreach ($classIds as $id) {
$usergroup->subscribe_users_to_usergroup($id, [$user_id], false); $usergroup->subscribe_users_to_usergroup($id, [$user_id], false);
} }
} }
// We are sure that the extra field exists. // 6) Update extrafield values (for newly created or even reused users):
foreach ($extra_fields as $extras) { foreach ($extra_fields as $extras) {
if (!isset($user[$extras[1]])) { $fieldVar = $extras[1];
$matchedKey = null;
foreach ($user as $colName => $colVal) {
if (strtolower($colName) === strtolower($fieldVar)) {
$matchedKey = $colName;
break;
}
}
if ($matchedKey === null) {
continue; continue;
} }
$key = $extras[1]; $value = $user[$matchedKey];
$value = $user[$key]; if (!array_key_exists($matchedKey, $optionsByField)) {
$optionsByField[$matchedKey] = $efo->getOptionsByFieldVariable($matchedKey);
if (!array_key_exists($key, $optionsByField)) {
$optionsByField[$key] = $efo->getOptionsByFieldVariable($key);
} }
/** @var ExtraFieldOptions $option */ /** @var ExtraFieldOptions $option */
foreach ($optionsByField[$key] as $option) { foreach ($optionsByField[$matchedKey] as $option) {
if ($option->getDisplayText() === $value) { if ($option->getDisplayText() === $value) {
$value = $option->getValue(); $value = $option->getValue();
break;
} }
} }
UserManager::update_extra_field_value($user_id, $key, $value); UserManager::update_extra_field_value($user_id, $matchedKey, $value);
}
$userSaved[] = $user;
} else {
$returnMessage = Display::return_message(get_lang('Error'), 'warning');
$userWarning[] = $user;
} }
// 7) Record success
$user['id'] = $user_id;
$user['message'] = $returnMessage; $user['message'] = $returnMessage;
$userSaved[] = $user;
} }
// Save with success, error and warning users // Save with success, error and warning users
@@ -708,6 +815,25 @@ if (isset($_POST['formSent']) && $_POST['formSent'] && $_FILES['import_file']['s
Session::erase('user_import_data_'.$userId); Session::erase('user_import_data_'.$userId);
$users = Import::csvToArray($_FILES['import_file']['tmp_name']); $users = Import::csvToArray($_FILES['import_file']['tmp_name']);
$uniqueField = api_get_configuration_value('extra_field_to_validate_on_user_registration');
if (!empty($uniqueField) && !empty($users)) {
$firstRow = reset($users);
$csvHeader = array_keys($firstRow);
$csvHeaderLower = array_map('trim', array_map('strtolower', $csvHeader));
if (!in_array($uniqueField, $csvHeaderLower, true)) {
Display::addFlash(
Display::return_message(
sprintf('The column "%s" is required in the CSV for this platform', $uniqueField),
'error'
)
);
header('Location: '.api_get_self());
exit;
}
}
$users = parse_csv_data( $users = parse_csv_data(
$users, $users,
$cleanFileName, $cleanFileName,
+24 -5
View File
@@ -155,6 +155,7 @@ function trimVariables()
'keyword_username', 'keyword_username',
'keyword_email', 'keyword_email',
'keyword_officialcode', 'keyword_officialcode',
'keyword_phone',
]; ];
foreach ($filterVariables as $variable) { foreach ($filterVariables as $variable) {
@@ -235,6 +236,7 @@ function prepare_user_sql_query($getCount)
'keyword_username', 'keyword_username',
'keyword_email', 'keyword_email',
'keyword_officialcode', 'keyword_officialcode',
'keyword_phone',
'keyword_status', 'keyword_status',
'keyword_active', 'keyword_active',
'keyword_inactive', 'keyword_inactive',
@@ -264,7 +266,8 @@ function prepare_user_sql_query($getCount)
concat(u.lastname,' ',u.firstname) LIKE '$keywordFiltered' OR concat(u.lastname,' ',u.firstname) LIKE '$keywordFiltered' OR
u.username LIKE '$keywordFiltered' OR u.username LIKE '$keywordFiltered' OR
u.official_code LIKE '$keywordFiltered' OR u.official_code LIKE '$keywordFiltered' OR
u.email LIKE '$keywordFiltered' u.email LIKE '$keywordFiltered' OR
u.phone LIKE '$keywordFiltered'
) )
"; ";
} elseif (isset($keywordListValues) && !empty($keywordListValues)) { } elseif (isset($keywordListValues) && !empty($keywordListValues)) {
@@ -308,6 +311,9 @@ function prepare_user_sql_query($getCount)
if (!empty($keywordListValues['keyword_officialcode'])) { if (!empty($keywordListValues['keyword_officialcode'])) {
$sql .= " AND u.official_code LIKE '".Database::escape_string("%".$keywordListValues['keyword_officialcode']."%")."' "; $sql .= " AND u.official_code LIKE '".Database::escape_string("%".$keywordListValues['keyword_officialcode']."%")."' ";
} }
if (!empty($keywordListValues['keyword_phone'])) {
$sql .= " AND u.phone LIKE '".Database::escape_string("%".$keywordListValues['keyword_phone']."%")."' ";
}
$sql .= " $keyword_admin $keyword_extra_value "; $sql .= " $keyword_admin $keyword_extra_value ";
@@ -463,10 +469,15 @@ function get_user_data($from, $number_of_items, $column, $direction)
$user[0], $user[0],
USER_IMAGE_SIZE_SMALL USER_IMAGE_SIZE_SMALL
); );
$personName = htmlspecialchars(
api_get_person_name($user[2], $user[3]),
ENT_QUOTES,
'UTF-8'
);
$photo = '<img $photo = '<img
src="'.$userPicture.'" width="22" height="22" src="'.$userPicture.'" width="22" height="22"
alt="'.api_get_person_name($user[2], $user[3]).'" alt="'.$personName.'"
title="'.api_get_person_name($user[2], $user[3]).'" />'; title="'.$personName.'" />';
if (1 == $user[7] && !empty($user['exp'])) { if (1 == $user[7] && !empty($user['exp'])) {
// check expiration date // check expiration date
@@ -643,8 +654,12 @@ function modify_filter($user_id, $url_params, $row)
if (api_is_platform_admin(true)) { if (api_is_platform_admin(true)) {
$editProfileUrl = Display::getProfileEditionLink($user_id, true); $editProfileUrl = Display::getProfileEditionLink($user_id, true);
if (!$user_is_anonymous && if (!$user_is_anonymous
api_global_admin_can_edit_admin($user_id, null, true) && api_global_admin_can_edit_admin(
$user_id,
null,
true !== api_get_configuration_value('disallow_session_admin_edit_users')
)
) { ) {
$result .= '<a href="'.$editProfileUrl.'">'. $result .= '<a href="'.$editProfileUrl.'">'.
Display::return_icon( Display::return_icon(
@@ -985,6 +1000,9 @@ if (isset($_GET['keyword'])) {
$parameters['keyword_email'] = Security::remove_XSS($_GET['keyword_email']); $parameters['keyword_email'] = Security::remove_XSS($_GET['keyword_email']);
$parameters['keyword_officialcode'] = Security::remove_XSS($_GET['keyword_officialcode']); $parameters['keyword_officialcode'] = Security::remove_XSS($_GET['keyword_officialcode']);
$parameters['keyword_status'] = Security::remove_XSS($_GET['keyword_status']); $parameters['keyword_status'] = Security::remove_XSS($_GET['keyword_status']);
if (isset($_GET['keyword_phone'])) {
$parameters['keyword_phone'] = Security::remove_XSS($_GET['keyword_phone']);
}
if (isset($_GET['keyword_active'])) { if (isset($_GET['keyword_active'])) {
$parameters['keyword_active'] = Security::remove_XSS($_GET['keyword_active']); $parameters['keyword_active'] = Security::remove_XSS($_GET['keyword_active']);
} }
@@ -1014,6 +1032,7 @@ $form->addText('keyword_lastname', get_lang('LastName'), false);
$form->addText('keyword_username', get_lang('LoginName'), false); $form->addText('keyword_username', get_lang('LoginName'), false);
$form->addText('keyword_email', get_lang('Email'), false); $form->addText('keyword_email', get_lang('Email'), false);
$form->addText('keyword_officialcode', get_lang('OfficialCode'), false); $form->addText('keyword_officialcode', get_lang('OfficialCode'), false);
$form->addText('keyword_phone', get_lang('Phone'), false);
$classId = isset($_REQUEST['class_id']) && !empty($_REQUEST['class_id']) ? (int) $_REQUEST['class_id'] : 0; $classId = isset($_REQUEST['class_id']) && !empty($_REQUEST['class_id']) ? (int) $_REQUEST['class_id'] : 0;
$options = []; $options = [];
+11 -3
View File
@@ -37,6 +37,8 @@ if (isset($_REQUEST['load_ajax'])) {
} }
$user_id = (int) $_REQUEST['user_id']; $user_id = (int) $_REQUEST['user_id'];
$new_course_list = SessionManager::get_course_list_by_session_id($new_session_id); $new_course_list = SessionManager::get_course_list_by_session_id($new_session_id);
$coursesInOldSession = SessionManager::get_course_list_by_session_id($origin_session_id);
$countCoursesInOldSession = count($coursesInOldSession);
$course_founded = false; $course_founded = false;
foreach ($new_course_list as $course_item) { foreach ($new_course_list as $course_item) {
@@ -55,13 +57,14 @@ if (isset($_REQUEST['load_ajax'])) {
// Check if the same course exist in the session destination // Check if the same course exist in the session destination
if ($course_founded) { if ($course_founded) {
$result = SessionManager::get_users_by_session($new_session_id); $result = SessionManager::get_users_by_session($new_session_id);
$subscribedToNew = false;
if (empty($result) || !in_array($user_id, array_keys($result))) { if (empty($result) || !in_array($user_id, array_keys($result))) {
if ($debug) { if ($debug) {
echo 'User added to the session'; echo 'User added to the session';
} }
// Registering user to the new session // Registering user to the new session
if ($update_database) { if ($update_database) {
SessionManager::subscribeUsersToSession( $subscribedToNew = SessionManager::subscribeUsersToSession(
$new_session_id, $new_session_id,
[$user_id], [$user_id],
false, false,
@@ -80,6 +83,11 @@ if (isset($_REQUEST['load_ajax'])) {
$update_database, $update_database,
$debug $debug
); );
// If there is only one course in the old session, remove the user from the old session (otherwise leads to confusion)
if ($update_database && $subscribedToNew && $countCoursesInOldSession === 1) {
SessionManager::unsubscribe_user_from_session($origin_session_id, $user_id);
echo get_lang('UserUnsubscribedFromOldSessionAsThereWasOnlyOneCourse');
}
} else { } else {
echo get_lang('CourseDoesNotExistInThisSession'); echo get_lang('CourseDoesNotExistInThisSession');
} }
@@ -227,9 +235,9 @@ if (!empty($user_list)) {
} }
} }
foreach ($course_list_registered as &$course) { foreach ($course_list_registered as $i => $course) {
$courseInfo = api_get_course_info_by_id($course['real_id']); $courseInfo = api_get_course_info_by_id($course['real_id']);
$course['name'] = $courseInfo['name']; $course_list_registered[$i]['name'] = $courseInfo['name'];
} }
$course_list = $course_list_registered; $course_list = $course_list_registered;
@@ -649,7 +649,12 @@ class AttendanceController
// Get data table // Get data table
$data_table = []; $data_table = [];
$addOfficialCode = api_get_configuration_value('attendance_add_official_code');
if ($addOfficialCode) {
$head_table = ['#', get_lang('OfficialCode'), get_lang('Name')];
} else {
$head_table = ['#', get_lang('Name')]; $head_table = ['#', get_lang('Name')];
}
foreach ($data_array['attendant_calendar'] as $class_day) { foreach ($data_array['attendant_calendar'] as $class_day) {
$labelDuration = !empty($class_day['duration']) ? get_lang('Duration').' : '.$class_day['duration'] : ''; $labelDuration = !empty($class_day['duration']) ? get_lang('Duration').' : '.$class_day['duration'] : '';
$head_table[] = $head_table[] =
@@ -667,6 +672,9 @@ class AttendanceController
$cols = 1; $cols = 1;
$result = []; $result = [];
$result['count'] = $count; $result['count'] = $count;
if ($addOfficialCode) {
$result['official_code'] = $user['official_code'];
}
$result['full_name'] = api_get_person_name($user['firstname'], $user['lastname']); $result['full_name'] = api_get_person_name($user['firstname'], $user['lastname']);
foreach ($data_array['attendant_calendar'] as $class_day) { foreach ($data_array['attendant_calendar'] as $class_day) {
if ($class_day['done_attendance'] == 1) { if ($class_day['done_attendance'] == 1) {
+11 -1
View File
@@ -210,6 +210,11 @@ if (api_is_allowed_to_edit(null, true) ||
}); });
</script> </script>
<?php $addOfficialCode = api_get_configuration_value('attendance_add_official_code');
$headerOfficialCode = '';
if ($addOfficialCode) {
$headerOfficialCode = '<th width="100px">'.get_lang('OfficialCode').'</th>';
} ?>
<form method="post" action="index.php?action=attendance_sheet_add&<?php echo api_get_cidreq().$param_filter; ?>&attendance_id=<?php echo $attendance_id; ?>" > <form method="post" action="index.php?action=attendance_sheet_add&<?php echo api_get_cidreq().$param_filter; ?>&attendance_id=<?php echo $attendance_id; ?>" >
<div class="attendance-sheet-content" style="width:100%;background-color:#E1E1E1;margin-top:20px;"> <div class="attendance-sheet-content" style="width:100%;background-color:#E1E1E1;margin-top:20px;">
<div class="divTableWithFloatingHeader attendance-users-table" style="width:45%;float:left;margin:0px;padding:0px;"> <div class="divTableWithFloatingHeader attendance-users-table" style="width:45%;float:left;margin:0px;padding:0px;">
@@ -218,6 +223,7 @@ if (api_is_allowed_to_edit(null, true) ||
<tr class="tableFloatingHeader" style="position: absolute; top: 0px; left: 0px; visibility: hidden; margin:0px;padding:0px" > <tr class="tableFloatingHeader" style="position: absolute; top: 0px; left: 0px; visibility: hidden; margin:0px;padding:0px" >
<th width="10px"><?php echo '#'; ?></th> <th width="10px"><?php echo '#'; ?></th>
<th width="10px"><?php echo get_lang('Photo'); ?></th> <th width="10px"><?php echo get_lang('Photo'); ?></th>
<?php echo $headerOfficialCode; ?>
<th width="100px"><?php echo get_lang('LastName'); ?></th> <th width="100px"><?php echo get_lang('LastName'); ?></th>
<th width="100px"><?php echo get_lang('FirstName'); ?></th> <th width="100px"><?php echo get_lang('FirstName'); ?></th>
<th width="100px"><?php echo get_lang('AttendancesFaults'); ?></th> <th width="100px"><?php echo get_lang('AttendancesFaults'); ?></th>
@@ -225,6 +231,7 @@ if (api_is_allowed_to_edit(null, true) ||
<tr class="tableFloatingHeaderOriginal" > <tr class="tableFloatingHeaderOriginal" >
<th width="10px"><?php echo '#'; ?></th> <th width="10px"><?php echo '#'; ?></th>
<th width="10px"><?php echo get_lang('Photo'); ?></th> <th width="10px"><?php echo get_lang('Photo'); ?></th>
<?php echo $headerOfficialCode; ?>
<th width="150px"><?php echo get_lang('LastName'); ?></th> <th width="150px"><?php echo get_lang('LastName'); ?></th>
<th width="140px"><?php echo get_lang('FirstName'); ?></th> <th width="140px"><?php echo get_lang('FirstName'); ?></th>
<th width="100px"><?php echo get_lang('AttendancesFaults'); ?></th> <th width="100px"><?php echo get_lang('AttendancesFaults'); ?></th>
@@ -248,6 +255,9 @@ if (api_is_allowed_to_edit(null, true) ||
<tr class="<?php echo $class; ?>"> <tr class="<?php echo $class; ?>">
<td><center><?php echo $i; ?></center></td> <td><center><?php echo $i; ?></center></td>
<td><?php echo $data['photo']; ?></td> <td><?php echo $data['photo']; ?></td>
<?php if ($addOfficialCode) {
echo '<td>'.$data['official_code'].'</td>';
} ?>
<td><span title="<?php echo $username; ?>"><?php echo $data['lastname']; ?></span></td> <td><span title="<?php echo $username; ?>"><?php echo $data['lastname']; ?></span></td>
<td><?php echo $data['firstname']; ?></td> <td><?php echo $data['firstname']; ?></td>
<td> <td>
@@ -286,7 +296,7 @@ if (api_is_allowed_to_edit(null, true) ||
if ($allowSignature) { if ($allowSignature) {
$iconFullScreen = Display::url( $iconFullScreen = Display::url(
Display::return_icon('view_fullscreen.png', get_lang('SeeForTablet'), [], ICON_SIZE_SMALL), 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'] . (!empty($groupId) ? '&group_id=' . $groupId : '') 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; $isBlocked = 0;
$iconBlockName = 'eyes.png'; $iconBlockName = 'eyes.png';
+5 -3
View File
@@ -277,8 +277,8 @@ function extldap_get_user_search_string($username)
global $extldap_config; global $extldap_config;
// init // init
$filter = '('.$extldap_config['user_search'].')'; $filter = '('.$extldap_config['user_search'].')';
// replacing %username% by the actual username // replacing %username% by the actual username (escaped to prevent LDAP injection)
$filter = str_replace('%username%', $username, $filter); $filter = str_replace('%username%', ldap_escape($username, "", LDAP_ESCAPE_FILTER), $filter);
// append a global filter if needed // append a global filter if needed
if (isset($extldap_config['filter']) && $extldap_config['filter'] != "") { if (isset($extldap_config['filter']) && $extldap_config['filter'] != "") {
$filter = '(&'.$filter.'('.$extldap_config['filter'].'))'; $filter = '(&'.$filter.'('.$extldap_config['filter'].'))';
@@ -518,5 +518,7 @@ function extldapCasUserLogin($casUser)
} }
// return the value // return the value
return extldapGetUserAttributeValue("($attributeToFilterOn=$casUser)", $attributeToRead); $casUserFilter = ldap_escape($casUser, "", LDAP_ESCAPE_FILTER);
return extldapGetUserAttributeValue("($attributeToFilterOn=$casUserFilter)", $attributeToRead);
} }
+14 -1
View File
@@ -680,7 +680,20 @@ if ($form->validate()) {
$values['username'] = api_substr($values['username'], 0, USERNAME_MAX_LENGTH); $values['username'] = api_substr($values['username'], 0, USERNAME_MAX_LENGTH);
} }
if (api_get_setting('allow_registration_as_teacher') === 'false') { // Security rule: if teacher registration is disabled, force learner status.
$allowTeacherRegistration = api_get_setting('allow_registration_as_teacher') !== 'false';
if (!$allowTeacherRegistration) {
$values['status'] = STUDENT;
}
// Security rule: server-side allow-list on submitted status to prevent
// privilege mass-assignment (CWE-915). The UI only offers STUDENT/COURSEMANAGER;
// any other value (e.g. SESSIONADMIN, DRH, COURSEMANAGERLOWSECURITY) coming
// from a tampered POST must be downgraded to STUDENT.
$allowedSelfRegistrationStatus = $allowTeacherRegistration
? [STUDENT, COURSEMANAGER]
: [STUDENT];
if (!in_array((int) ($values['status'] ?? STUDENT), $allowedSelfRegistrationStatus, true)) {
$values['status'] = STUDENT; $values['status'] = STUDENT;
} }
+12 -7
View File
@@ -117,10 +117,11 @@ function ldap_find_user_info($login)
// Search surname entry // Search surname entry
//OLD: $sr=ldap_search($ldapconnect,"dc=rug, dc=ac, dc=be", "uid=$login"); //OLD: $sr=ldap_search($ldapconnect,"dc=rug, dc=ac, dc=be", "uid=$login");
//echo "<p> ldapDc = '$LDAPbasedn' </p>"; //echo "<p> ldapDc = '$LDAPbasedn' </p>";
$loginFilter = ldap_escape($login, "", LDAP_ESCAPE_FILTER);
if (!empty($ldap_search_dn)) { if (!empty($ldap_search_dn)) {
$sr = ldap_search($ldap_connect, $ldap_search_dn, "uid=$login"); $sr = ldap_search($ldap_connect, $ldap_search_dn, "uid=$loginFilter");
} else { } else {
$sr = ldap_search($ldap_connect, $ldap_basedn, "uid=$login"); $sr = ldap_search($ldap_connect, $ldap_basedn, "uid=$loginFilter");
} }
//echo " Search result is ".$sr; //echo " Search result is ".$sr;
//echo " Number of entries returned is ".ldap_count_entries($ldapconnect,$sr); //echo " Number of entries returned is ".ldap_count_entries($ldapconnect,$sr);
@@ -272,7 +273,8 @@ function ldap_authentication_check($uname, $passwd)
if ($ds !== false) { if ($ds !== false) {
//Creation of filter containing values input by the user //Creation of filter containing values input by the user
// Here it might be necessary to use $filter="(samaccountName=$uname)"; - see http://support.chamilo.org/issues/4675 // Here it might be necessary to use $filter="(samaccountName=$uname)"; - see http://support.chamilo.org/issues/4675
$filter = "(uid=$uname)"; $unameFilter = ldap_escape($uname, "", LDAP_ESCAPE_FILTER);
$filter = "(uid=$unameFilter)";
// Open anonymous LDAP connection // Open anonymous LDAP connection
$result = false; $result = false;
$ldap_bind_res = ldap_handle_bind($ds, $result); $ldap_bind_res = ldap_handle_bind($ds, $result);
@@ -371,10 +373,13 @@ function ldap_get_users()
{ {
global $ldap_basedn, $ldap_host, $ldap_port, $ldap_rdn, $ldap_pass, $ldap_search_dn, $extldap_user_correspondance; global $ldap_basedn, $ldap_host, $ldap_port, $ldap_rdn, $ldap_pass, $ldap_search_dn, $extldap_user_correspondance;
$keyword_firstname = isset($_GET['keyword_firstname']) ? trim(Database::escape_string($_GET['keyword_firstname'])) : ''; // These values are interpolated into LDAP filters, so they must be escaped
$keyword_lastname = isset($_GET['keyword_lastname']) ? trim(Database::escape_string($_GET['keyword_lastname'])) : ''; // with ldap_escape() (LDAP_ESCAPE_FILTER); SQL escaping does not neutralize
$keyword_username = isset($_GET['keyword_username']) ? trim(Database::escape_string($_GET['keyword_username'])) : ''; // LDAP metacharacters and would corrupt the search.
$keyword_type = isset($_GET['keyword_type']) ? Database::escape_string($_GET['keyword_type']) : ''; $keyword_firstname = isset($_GET['keyword_firstname']) ? ldap_escape(trim($_GET['keyword_firstname']), "", LDAP_ESCAPE_FILTER) : '';
$keyword_lastname = isset($_GET['keyword_lastname']) ? ldap_escape(trim($_GET['keyword_lastname']), "", LDAP_ESCAPE_FILTER) : '';
$keyword_username = isset($_GET['keyword_username']) ? ldap_escape(trim($_GET['keyword_username']), "", LDAP_ESCAPE_FILTER) : '';
$keyword_type = isset($_GET['keyword_type']) ? ldap_escape($_GET['keyword_type'], "", LDAP_ESCAPE_FILTER) : '';
$ldap_query = []; $ldap_query = [];
+10 -5
View File
@@ -89,20 +89,25 @@ if ($form->validate()) {
$user = Login::get_user_accounts_by_username($values['user']); $user = Login::get_user_accounts_by_username($values['user']);
if (!$user) { if (!$user) {
$messageText = get_lang('NoUserAccountWithThisEmailAddress'); // Always return the same neutral response regardless of whether the
// username/email exists. Revealing "no account with this email" allows
// user enumeration by observing the differing redirect destination or
// message. Using the same message and redirect as the success path
// prevents that information leak.
$messageText = get_lang('AnEmailToResetYourPasswordHasBeenSent');
if (CustomPages::enabled() && CustomPages::exists(CustomPages::LOST_PASSWORD)) { if (CustomPages::enabled() && CustomPages::exists(CustomPages::INDEX_UNLOGGED)) {
CustomPages::display( CustomPages::display(
CustomPages::LOST_PASSWORD, CustomPages::INDEX_UNLOGGED,
['info' => $messageText] ['info' => $messageText]
); );
exit; exit;
} }
Display::addFlash( Display::addFlash(
Display::return_message($messageText, 'error', false) Display::return_message($messageText, 'info', false)
); );
header('Location: '.api_get_self()); header('Location: '.api_get_path(WEB_PATH));
exit; exit;
} }
+1 -1
View File
@@ -165,7 +165,7 @@ function _openid_parse_message($message) {
*/ */
function _openid_nonce() { function _openid_nonce() {
// YYYY-MM-DDThh:mm:ssTZD UTC, plus some optional extra unique chars // YYYY-MM-DDThh:mm:ssTZD UTC, plus some optional extra unique chars
return gmstrftime('%Y-%m-%dT%H:%M:%S%Z') . return gmdate('Y-m-d\TH:i:sT') .
chr(mt_rand(0, 25) + 65) . chr(mt_rand(0, 25) + 65) .
chr(mt_rand(0, 25) + 65) . chr(mt_rand(0, 25) + 65) .
chr(mt_rand(0, 25) + 65) . chr(mt_rand(0, 25) + 65) .
+38 -31
View File
@@ -2,12 +2,16 @@
/* For licensing terms, see /license.txt */ /* For licensing terms, see /license.txt */
use Symfony\Component\HttpFoundation\Request as HttpRequest;
$cidReset = true; // Flag forcing the 'current course' reset $cidReset = true; // Flag forcing the 'current course' reset
require_once __DIR__.'/../inc/global.inc.php'; require_once __DIR__.'/../inc/global.inc.php';
api_block_anonymous_users(); api_block_anonymous_users();
$httpRequest = HttpRequest::createFromGlobals();
$auth = new Auth(); $auth = new Auth();
$user_course_categories = CourseManager::get_user_course_categories(api_get_user_id()); $user_course_categories = CourseManager::get_user_course_categories(api_get_user_id());
$courses_in_category = $auth->getCoursesInCategory(false); $courses_in_category = $auth->getCoursesInCategory(false);
@@ -21,8 +25,11 @@ $authorizedActions = [
'set_collapsable', 'set_collapsable',
'unsubscribe', 'unsubscribe',
]; ];
if (in_array(trim($_REQUEST['action']), $authorizedActions)) {
$action = trim($_REQUEST['action']); $action = $httpRequest->query->get('action', $httpRequest->request->get('action', ''));
if (!in_array($action, $authorizedActions)) {
$action = '';
} }
$currentUrl = api_get_self(); $currentUrl = api_get_self();
@@ -33,9 +40,9 @@ $interbreadcrumb[] = [
]; ];
// We are moving the course of the user to a different user defined course category (=Sort My Courses). // We are moving the course of the user to a different user defined course category (=Sort My Courses).
if (isset($_POST['submit_change_course_category'])) { if ($httpRequest->request->has('submit_change_course_category')) {
$course2EditCategory = Security::remove_XSS($_POST['course_2_edit_category']); $course2EditCategory = Security::remove_XSS($httpRequest->request->get('course_2_edit_category'));
$courseCategories = Security::remove_XSS($_POST['course_categories']); $courseCategories = Security::remove_XSS($httpRequest->request->get('course_categories'));
$result = $auth->updateCourseCategory($course2EditCategory, $courseCategories); $result = $auth->updateCourseCategory($course2EditCategory, $courseCategories);
if ($result) { if ($result) {
Display::addFlash( Display::addFlash(
@@ -47,28 +54,31 @@ if (isset($_POST['submit_change_course_category'])) {
} }
// We edit course category // We edit course category
if (isset($_POST['submit_edit_course_category']) && if ($httpRequest->request->has('submit_edit_course_category')
isset($_POST['title_course_category']) && $httpRequest->request->has('title_course_category')
&& Security::check_token('post')
) { ) {
$titleCourseCategory = Security::remove_XSS($_POST['title_course_category']); $titleCourseCategory = Security::remove_XSS($httpRequest->request->get('title_course_category'));
$categoryId = Security::remove_XSS($_POST['category_id']); $categoryId = Security::remove_XSS($httpRequest->request->get('category_id'));
$categoryInfo = $auth->getUserCourseCategory($categoryId);
if ($categoryInfo) {
$result = $auth->store_edit_course_category($titleCourseCategory, $categoryId); $result = $auth->store_edit_course_category($titleCourseCategory, $categoryId);
if ($result) { if ($result) {
Display::addFlash( Display::addFlash(
Display::return_message(get_lang('CourseCategoryEditStored')) Display::return_message(get_lang('CourseCategoryEditStored'))
); );
} }
}
header('Location: '.api_get_self()); header('Location: '.api_get_self());
exit; exit;
} }
// We are creating a new user defined course category (= Create Course Category). // We are creating a new user defined course category (= Create Course Category).
if (isset($_POST['create_course_category']) && if ($httpRequest->request->has('create_course_category')
isset($_POST['title_course_category']) && && $titleCourseCategory = $httpRequest->request->get('title_course_category')
strlen(trim($_POST['title_course_category'])) > 0
) { ) {
$titleCourseCategory = Security::remove_XSS($_POST['title_course_category']); $titleCourseCategory = Security::remove_XSS($titleCourseCategory);
$result = $auth->store_course_category($titleCourseCategory); $result = $auth->store_course_category($titleCourseCategory);
if ($result) { if ($result) {
Display::addFlash( Display::addFlash(
@@ -87,10 +97,10 @@ if (isset($_POST['create_course_category']) &&
} }
// We are moving a course or category of the user up/down the list (=Sort My Courses). // We are moving a course or category of the user up/down the list (=Sort My Courses).
if (isset($_GET['move'])) { if ($getMove = $httpRequest->query->get('move')) {
$getCourse = isset($_GET['course']) ? Security::remove_XSS($_GET['course']) : ''; $getCourse = Security::remove_XSS($httpRequest->query->get('course'));
$getMove = Security::remove_XSS($_GET['move']); $getMove = Security::remove_XSS($getMove);
$getCategory = isset($_GET['category']) ? Security::remove_XSS($_GET['category']) : ''; $getCategory = Security::remove_XSS($httpRequest->query->get('category'));
if (!empty($getCourse)) { if (!empty($getCourse)) {
$result = $auth->move_course($getMove, $getCourse, $getCategory); $result = $auth->move_course($getMove, $getCourse, $getCategory);
if ($result) { if ($result) {
@@ -113,7 +123,7 @@ if (isset($_GET['move'])) {
switch ($action) { switch ($action) {
case 'edit_category': case 'edit_category':
$categoryId = isset($_GET['category_id']) ? (int) $_GET['category_id'] : 0; $categoryId = $httpRequest->query->getInt('category_id');
$categoryInfo = $auth->getUserCourseCategory($categoryId); $categoryInfo = $auth->getUserCourseCategory($categoryId);
if ($categoryInfo) { if ($categoryInfo) {
$categoryName = $categoryInfo['title']; $categoryName = $categoryInfo['title'];
@@ -124,15 +134,15 @@ switch ($action) {
); );
$form->addText('title_course_category', get_lang('Name')); $form->addText('title_course_category', get_lang('Name'));
$form->addHidden('category_id', $categoryId); $form->addHidden('category_id', $categoryId);
$form->addHidden('sec_token', Security::get_token());
$form->addButtonSave(get_lang('Edit'), 'submit_edit_course_category'); $form->addButtonSave(get_lang('Edit'), 'submit_edit_course_category');
$form->setDefaults(['title_course_category' => $categoryName]); $form->setDefaults(['title_course_category' => $categoryName]);
$form->display(); $form->display();
} }
exit; exit;
break;
case 'edit_course_category': case 'edit_course_category':
$edit_course = (int) $_GET['course_id']; $edit_course = $httpRequest->query->getInt('course_id');
$defaultCategoryId = isset($_GET['category_id']) ? (int) $_GET['category_id'] : 0; $defaultCategoryId = $httpRequest->query->getInt('category_id');
$courseInfo = api_get_course_info_by_id($edit_course); $courseInfo = api_get_course_info_by_id($edit_course);
if (empty($courseInfo)) { if (empty($courseInfo)) {
@@ -167,12 +177,10 @@ switch ($action) {
$form->addButtonSave(get_lang('Save'), 'submit_change_course_category'); $form->addButtonSave(get_lang('Save'), 'submit_change_course_category');
$form->display(); $form->display();
exit; exit;
break;
case 'deletecoursecategory': case 'deletecoursecategory':
// we are deleting a course category // we are deleting a course category
if (isset($_GET['id'])) { if ($getId = $httpRequest->query->getInt('id')) {
if (Security::check_token('get')) { if (Security::check_token('get')) {
$getId = Security::remove_XSS($_GET['id']);
$result = $auth->delete_course_category($getId); $result = $auth->delete_course_category($getId);
if ($result) { if ($result) {
Display::addFlash( Display::addFlash(
@@ -183,7 +191,6 @@ switch ($action) {
} }
header('Location: '.api_get_self()); header('Location: '.api_get_self());
exit; exit;
break;
case 'createcoursecategory': case 'createcoursecategory':
$form = new FormValidator( $form = new FormValidator(
'create_course_category', 'create_course_category',
@@ -194,16 +201,15 @@ switch ($action) {
$form->addButtonSave(get_lang('AddCategory'), 'create_course_category'); $form->addButtonSave(get_lang('AddCategory'), 'create_course_category');
$form->display(); $form->display();
exit; exit;
break;
case 'set_collapsable': case 'set_collapsable':
if (!api_get_configuration_value('allow_user_course_category_collapsable')) { if (!api_get_configuration_value('allow_user_course_category_collapsable')) {
api_not_allowed(true); api_not_allowed(true);
} }
$userId = api_get_user_id(); $userId = api_get_user_id();
$categoryId = isset($_REQUEST['categoryid']) ? (int) $_REQUEST['categoryid'] : 0; $categoryId = $httpRequest->query->getInt('categoryid', $httpRequest->request->getInt('categoryid'));
$option = isset($_REQUEST['option']) ? (int) $_REQUEST['option'] : 0; $option = $httpRequest->query->get('option', $httpRequest->request->getInt('option'));
$redirect = isset($_REQUEST['redirect']) ? Security::remove_XSS($_REQUEST['redirect']) : 0; $redirect = $httpRequest->query->get('redirect', $httpRequest->request->get('redirect', ''));
if (empty($userId) || empty($categoryId)) { if (empty($userId) || empty($categoryId)) {
api_not_allowed(true); api_not_allowed(true);
@@ -225,7 +231,6 @@ switch ($action) {
$url = api_get_self(); $url = api_get_self();
header('Location: '.$url); header('Location: '.$url);
exit; exit;
break;
} }
function generateUnsubscribeForm(string $courseCode, string $secToken): string function generateUnsubscribeForm(string $courseCode, string $secToken): string
@@ -433,7 +438,9 @@ if (!empty($courses_without_category)) {
); );
} }
echo ''; echo '';
if (isset($_GET['edit']) && $course['code'] == $_GET['edit']) { if ($httpRequest->query->has('edit')
&& $httpRequest->query->get('edit') === $course['code']
) {
echo Display::return_icon('edit_na.png', get_lang('Edit'), '', 22); echo Display::return_icon('edit_na.png', get_lang('Edit'), '', 22);
} else { } else {
echo Display::url( echo Display::url(
+31 -8
View File
@@ -3,6 +3,7 @@
use Chamilo\CoreBundle\Entity\Skill; use Chamilo\CoreBundle\Entity\Skill;
use Skill as SkillManager; use Skill as SkillManager;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
/** /**
* Page for assign skills to a user. * Page for assign skills to a user.
@@ -11,7 +12,12 @@ use Skill as SkillManager;
*/ */
require_once __DIR__.'/../inc/global.inc.php'; require_once __DIR__.'/../inc/global.inc.php';
$userId = isset($_REQUEST['user']) ? (int) $_REQUEST['user'] : 0; $httpRequest = HttpRequest::createFromGlobals();
$userId = $httpRequest->query->getInt(
'user',
$httpRequest->request->getInt('user')
);
if (empty($userId)) { if (empty($userId)) {
api_not_allowed(true); api_not_allowed(true);
@@ -53,11 +59,25 @@ if (empty($skillLevels)) {
$skillsOptions[$skill['data']['id']] = $skill['data']['name']; $skillsOptions[$skill['data']['id']] = $skill['data']['name'];
} }
} }
$skillIdFromGet = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0; $skillIdFromGet = $httpRequest->query->getInt(
$currentValue = isset($_REQUEST['current_value']) ? (int) $_REQUEST['current_value'] : 0; 'id',
$currentLevel = isset($_REQUEST['current']) ? (int) str_replace('sub_skill_id_', '', $_REQUEST['current']) : 0; $httpRequest->request->getInt('id')
);
$currentValue = $httpRequest->query->getInt(
'current_value',
$httpRequest->request->getInt('current_value')
);
$currentLevel = $httpRequest->query->get(
'current',
$httpRequest->request->get('current', '')
);
$currentLevel = (int) str_replace('sub_skill_id_', '', $currentLevel);
$subSkillList = isset($_REQUEST['sub_skill_list']) ? explode(',', $_REQUEST['sub_skill_list']) : []; $subSkillList = $httpRequest->query->get(
'sub_skill_list',
$httpRequest->request->get('sub_skill_list', '')
);
$subSkillList = explode(',', $subSkillList);
$subSkillList = array_unique($subSkillList); $subSkillList = array_unique($subSkillList);
if (!empty($subSkillList)) { if (!empty($subSkillList)) {
@@ -94,7 +114,10 @@ if (!empty($currentLevel)) {
} }
} }
$skillId = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : key($skillsOptions); $skillId = $httpRequest->query->getInt(
'id',
$httpRequest->request->getInt('id', key($skillsOptions))
);
$skill = $skillRepo->find($skillId); $skill = $skillRepo->find($skillId);
$profile = false; $profile = false;
if ($skill) { if ($skill) {
@@ -234,8 +257,7 @@ if ($showLevels) {
//$form->addRule('acquired_level', get_lang('ThisFieldIsRequired'), 'required'); //$form->addRule('acquired_level', get_lang('ThisFieldIsRequired'), 'required');
} }
$form->addTextarea('argumentation', get_lang('Argumentation'), ['rows' => 6]); $form->addTextarea('argumentation', get_lang('Argumentation'), ['rows' => 6], true);
$form->addRule('argumentation', get_lang('ThisFieldIsRequired'), 'required');
$form->addRule( $form->addRule(
'argumentation', 'argumentation',
sprintf(get_lang('ThisTextShouldBeAtLeastXCharsLong'), 10), sprintf(get_lang('ThisTextShouldBeAtLeastXCharsLong'), 10),
@@ -243,6 +265,7 @@ $form->addRule(
10 10
); );
$form->applyFilter('argumentation', 'trim'); $form->applyFilter('argumentation', 'trim');
$form->applyFilter('argumentation', 'html_filter');
$form->addButtonSave(get_lang('Save')); $form->addButtonSave(get_lang('Save'));
$form->setDefaults($formDefaultValues); $form->setDefaults($formDefaultValues);
+8 -2
View File
@@ -4,6 +4,7 @@
use Chamilo\CoreBundle\Entity\SkillRelUser; use Chamilo\CoreBundle\Entity\SkillRelUser;
use Chamilo\CoreBundle\Entity\SkillRelUserComment; use Chamilo\CoreBundle\Entity\SkillRelUserComment;
use SkillRelUser as SkillRelUserManager; use SkillRelUser as SkillRelUserManager;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
/** /**
* Show information about the issued badge. * Show information about the issued badge.
@@ -13,7 +14,12 @@ use SkillRelUser as SkillRelUserManager;
*/ */
require_once __DIR__.'/../inc/global.inc.php'; require_once __DIR__.'/../inc/global.inc.php';
$issue = isset($_REQUEST['issue']) ? (int) $_REQUEST['issue'] : 0; $httpRequest = HttpRequest::createFromGlobals();
$issue = $httpRequest->query->getInt(
'issue',
$httpRequest->request->getInt('issue')
);
if (empty($issue)) { if (empty($issue)) {
api_not_allowed(true); api_not_allowed(true);
@@ -103,7 +109,7 @@ $skillIssueInfo = [
'acquired_level' => $currentSkillLevel, 'acquired_level' => $currentSkillLevel,
'argumentation_author_id' => $skillIssue->getArgumentationAuthorId(), 'argumentation_author_id' => $skillIssue->getArgumentationAuthorId(),
'argumentation_author_name' => $author['complete_name'], 'argumentation_author_name' => $author['complete_name'],
'argumentation' => $skillIssue->getArgumentation(), 'argumentation' => Security::remove_XSS($skillIssue->getArgumentation()),
'source_name' => $skillIssue->getSourceName(), 'source_name' => $skillIssue->getSourceName(),
'user_id' => $skillIssue->getUser()->getId(), 'user_id' => $skillIssue->getUser()->getId(),
'user_complete_name' => UserManager::formatUserFullName($skillIssue->getUser()), 'user_complete_name' => UserManager::formatUserFullName($skillIssue->getUser()),
+6 -3
View File
@@ -4,6 +4,7 @@
use Chamilo\CoreBundle\Entity\SkillRelUser; use Chamilo\CoreBundle\Entity\SkillRelUser;
use Chamilo\CoreBundle\Entity\SkillRelUserComment; use Chamilo\CoreBundle\Entity\SkillRelUserComment;
use SkillRelUser as SkillRelUserManager; use SkillRelUser as SkillRelUserManager;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
/** /**
* Show information about all issued badges with same skill by user. * Show information about all issued badges with same skill by user.
@@ -12,8 +13,10 @@ use SkillRelUser as SkillRelUserManager;
*/ */
require_once __DIR__.'/../inc/global.inc.php'; require_once __DIR__.'/../inc/global.inc.php';
$userId = isset($_GET['user']) ? (int) $_GET['user'] : 0; $httpRequest = HttpRequest::createFromGlobals();
$skillId = isset($_GET['skill']) ? (int) $_GET['skill'] : 0;
$userId = $httpRequest->query->getInt('user');
$skillId = $httpRequest->query->getInt('skill');
if (!$userId || !$skillId) { if (!$userId || !$skillId) {
api_not_allowed(true); api_not_allowed(true);
@@ -90,7 +93,7 @@ foreach ($userSkills as $index => $skillIssue) {
$argumentationAuthor['firstname'], $argumentationAuthor['firstname'],
$argumentationAuthor['lastname'] $argumentationAuthor['lastname']
), ),
'argumentation' => $skillIssue->getArgumentation(), 'argumentation' => Security::remove_XSS($skillIssue->getArgumentation()),
'source_name' => $skillIssue->getSourceName(), 'source_name' => $skillIssue->getSourceName(),
'user_id' => $skillIssue->getUser()->getId(), 'user_id' => $skillIssue->getUser()->getId(),
'user_complete_name' => UserManager::formatUserFullName($skillIssue->getUser()), 'user_complete_name' => UserManager::formatUserFullName($skillIssue->getUser()),
+6 -3
View File
@@ -18,7 +18,7 @@ if ((!api_is_allowed_in_course() || !api_is_allowed_in_course()) && !api_is_allo
} }
$origin = api_get_origin(); $origin = api_get_origin();
$action = isset($_GET['action']) ? $_GET['action'] : ''; $action = $_GET['action'] ?? '';
if (api_is_allowed_to_edit()) { if (api_is_allowed_to_edit()) {
$nameTools = get_lang('blog_management'); $nameTools = get_lang('blog_management');
@@ -58,11 +58,14 @@ if (api_is_allowed_to_edit()) {
echo Display::return_message(get_lang('BlogEdited'), 'confirmation'); echo Display::return_message(get_lang('BlogEdited'), 'confirmation');
} }
} }
if (isset($_GET['action']) && $_GET['action'] == 'visibility') {
$isValidToken = Security::check_token('get', null, 'blog');
if ($action == 'visibility' && $isValidToken) {
Blog::changeBlogVisibility(intval($_GET['blog_id'])); Blog::changeBlogVisibility(intval($_GET['blog_id']));
echo Display::return_message(get_lang('VisibilityChanged'), 'confirmation'); echo Display::return_message(get_lang('VisibilityChanged'), 'confirmation');
} }
if (isset($_GET['action']) && $_GET['action'] == 'delete') { if ($action == 'delete' && $isValidToken) {
Blog::deleteBlog(intval($_GET['blog_id'])); Blog::deleteBlog(intval($_GET['blog_id']));
echo Display::return_message(get_lang('BlogDeleted'), 'confirmation'); echo Display::return_message(get_lang('BlogDeleted'), 'confirmation');
} }
+10 -4
View File
@@ -16,6 +16,12 @@ require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_MYAGENDA; $this_section = SECTION_MYAGENDA;
api_block_anonymous_users(); api_block_anonymous_users();
// Only redirect back to the referrer when it points to this Chamilo portal;
// otherwise fall back to the portal root to prevent an open redirect.
$webPath = api_get_path(WEB_PATH);
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$redirectUrl = (is_string($referer) && 0 === strpos($referer, $webPath)) ? $referer : $webPath;
// setting the name of the tool // setting the name of the tool
$nameTools = get_lang('MyAgenda'); $nameTools = get_lang('MyAgenda');
@@ -77,7 +83,7 @@ if (!empty($event)) {
case 'platform': case 'platform':
$vevent->setProperty('summary', api_convert_encoding($event['title'], 'UTF-8', $charset)); $vevent->setProperty('summary', api_convert_encoding($event['title'], 'UTF-8', $charset));
if (empty($event['start_date'])) { if (empty($event['start_date'])) {
header('location:'.Security::remove_XSS($_SERVER['HTTP_REFERER'])); header('location:'.$redirectUrl);
} }
list($y, $m, $d, $h, $M, $s) = preg_split('/[\s:-]/', $event['start_date']); list($y, $m, $d, $h, $M, $s) = preg_split('/[\s:-]/', $event['start_date']);
$vevent->setProperty( $vevent->setProperty(
@@ -116,7 +122,7 @@ if (!empty($event)) {
case 'course': case 'course':
$vevent->setProperty('summary', api_convert_encoding($event['title'], 'UTF-8', $charset)); $vevent->setProperty('summary', api_convert_encoding($event['title'], 'UTF-8', $charset));
if (empty($event['start_date'])) { if (empty($event['start_date'])) {
header('location:'.Security::remove_XSS($_SERVER['HTTP_REFERER'])); header('location:'.$redirectUrl);
} }
list($y, $m, $d, $h, $M, $s) = preg_split('/[\s:-]/', $event['start_date']); list($y, $m, $d, $h, $M, $s) = preg_split('/[\s:-]/', $event['start_date']);
$vevent->setProperty( $vevent->setProperty(
@@ -160,10 +166,10 @@ if (!empty($event)) {
$ical->returnCalendar(); $ical->returnCalendar();
break; break;
default: default:
header('location:'.Security::remove_XSS($_SERVER['HTTP_REFERER'])); header('location:'.$redirectUrl);
exit(); exit();
} }
} else { } else {
header('location:'.Security::remove_XSS($_SERVER['HTTP_REFERER'])); header('location:'.$redirectUrl);
exit; exit;
} }
@@ -142,7 +142,7 @@ class CourseDescriptionController
if (!empty($_POST['title']) && !empty($_POST['contentDescription'])) { if (!empty($_POST['title']) && !empty($_POST['contentDescription'])) {
if (1) { if (1) {
$title = $_POST['title']; $title = $_POST['title'];
$content = $_POST['contentDescription']; $content = Security::remove_XSS($_POST['contentDescription'], COURSEMANAGERLOWSECURITY);
$description_type = $_POST['description_type']; $description_type = $_POST['description_type'];
$id = $_POST['id']; $id = $_POST['id'];
if (empty($id)) { if (empty($id)) {
@@ -253,7 +253,7 @@ class CourseDescriptionController
if (!empty($_POST['title']) && !empty($_POST['contentDescription'])) { if (!empty($_POST['title']) && !empty($_POST['contentDescription'])) {
if (1) { if (1) {
$title = $_POST['title']; $title = $_POST['title'];
$content = $_POST['contentDescription']; $content = Security::remove_XSS($_POST['contentDescription'], COURSEMANAGERLOWSECURITY);
$description_type = $_POST['description_type']; $description_type = $_POST['description_type'];
if ($description_type >= ADD_BLOCK) { if ($description_type >= ADD_BLOCK) {
$course_description->set_description_type($description_type); $course_description->set_description_type($description_type);
+6 -1
View File
@@ -2,6 +2,7 @@
/* For licensing terms, see /license.txt */ /* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Component\Essence\SafeEssenceHttpClient;
use Chamilo\CoreBundle\Entity\CourseRelUser; use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CoreBundle\Entity\ExtraField; use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\Repository\SequenceResourceRepository; use Chamilo\CoreBundle\Entity\Repository\SequenceResourceRepository;
@@ -195,7 +196,11 @@ if ($hasRequirements) {
$template = new Template($course->getTitle(), true, true, false, true, false); $template = new Template($course->getTitle(), true, true, false, true, false);
$template->assign('course', $courseItem); $template->assign('course', $courseItem);
$essence = Essence\Essence::instance(); // SSRF protection: route Essence's server-side OEmbed/OpenGraph fetches of the
// teacher-set video URL through an IP-filtered HTTP client.
$essence = Essence\Essence::instance(
['Http' => new SafeEssenceHttpClient()]
);
$template->assign('essence', $essence); $template->assign('essence', $essence);
$template->assign('is_premium', $courseIsPremium); $template->assign('is_premium', $courseIsPremium);
$template->assign('allow_subscribe', $allowSubscribe); $template->assign('allow_subscribe', $allowSubscribe);
+119 -6
View File
@@ -41,21 +41,55 @@ Display::display_header($nameTools);
echo Display::page_header($nameTools); echo Display::page_header($nameTools);
$action = isset($_POST['action']) ? $_POST['action'] : ''; $action = isset($_POST['action']) ? $_POST['action'] : '';
$exportOption = isset($_POST['export_option']) ? $_POST['export_option'] : ''; $exportOption = isset($_POST['export_option']) ? $_POST['export_option'] : '';
$debugMoodleExport = MoodleExport::isDebugEnabled();
MoodleExport::registerDebugShutdownHandler();
if ($debugMoodleExport) {
MoodleExport::debugStaticLog('Debug mode enabled from MoodleExport::$debugEnabled', [
'script' => 'export_moodle.php',
'course_id' => api_get_course_id(),
'course_code' => api_get_course_id(),
'cidreq' => api_get_cidreq(),
'request_method' => $_SERVER['REQUEST_METHOD'] ?? '',
'post_action' => $action,
'export_option' => $exportOption,
'post_keys' => array_keys($_POST),
'content_length' => $_SERVER['CONTENT_LENGTH'] ?? '',
'debug_file' => MoodleExport::getDebugFilePath(),
]);
}
// Handle course selection form submission // Handle course selection form submission
MoodleExport::debugStaticLog('Checking export form branch', [
'action' => $action,
'has_post_token' => isset($_POST['sec_token']),
]);
if ($action === 'course_select_form' && Security::check_token('post')) { if ($action === 'course_select_form' && Security::check_token('post')) {
MoodleExport::debugStaticLog('Course selection form submitted');
// Handle the selected resources and continue with export // Handle the selected resources and continue with export
$selectedResources = $_POST['resource'] ?? null; $selectedResources = $_POST['resource'] ?? null;
MoodleExport::debugStaticLog('Selected resources received', [
'resource_groups' => is_array($selectedResources) ? count($selectedResources) : 0,
]);
if (!empty($selectedResources)) { if (!empty($selectedResources)) {
// Rebuild the course object based on selected resources // Rebuild the course object based on selected resources
MoodleExport::debugStaticLog('Building partial course from selected resources');
$cb = new CourseBuilder('partial'); $cb = new CourseBuilder('partial');
$course = $cb->build(0, null, false, array_keys($selectedResources), $selectedResources); $course = $cb->build(0, null, false, array_keys($selectedResources), $selectedResources);
MoodleExport::restoreMainDatabaseConnection();
MoodleExport::debugStaticLog('Partial course from selected resources built');
MoodleExport::debugStaticLog('Normalizing posted course selection');
$course = CourseSelectForm::get_posted_course(null, 0, '', $course); $course = CourseSelectForm::get_posted_course(null, 0, '', $course);
MoodleExport::restoreMainDatabaseConnection();
MoodleExport::debugStaticLog('Posted course selection normalized');
// Get admin details // Get admin details
$adminId = (int) $_POST['admin_id']; $adminId = (int) $_POST['admin_id'];
$adminUsername = filter_var($_POST['admin_username'], FILTER_SANITIZE_STRING); $adminUsername = strip_tags((string) $_POST['admin_username']);
if (!preg_match('/^[a-zA-Z0-9_]+$/', $adminUsername)) { if (!preg_match('/^[a-zA-Z0-9_]+$/', $adminUsername)) {
echo Display::return_message(get_lang('PleaseEnterValidLogin'), 'error'); echo Display::return_message(get_lang('PleaseEnterValidLogin'), 'error');
exit(); exit();
@@ -67,15 +101,32 @@ if ($action === 'course_select_form' && Security::check_token('post')) {
exit(); exit();
} }
MoodleExport::debugStaticLog('Creating MoodleExport instance for selected resources');
$exporter = new MoodleExport($course); $exporter = new MoodleExport($course);
MoodleExport::debugStaticLog('MoodleExport instance created for selected resources');
$exporter->setAdminUserData($adminId, $adminUsername, $adminEmail); $exporter->setAdminUserData($adminId, $adminUsername, $adminEmail);
MoodleExport::debugStaticLog('Admin user data configured', [
'admin_id' => $adminId,
'admin_username' => $adminUsername,
'admin_email' => $adminEmail,
]);
// Perform export // Perform export
$courseId = api_get_course_id(); $courseId = api_get_course_id();
$exportDir = 'moodle_export_'.$courseId; $exportDir = 'moodle_export_'.$courseId;
try { try {
$moodleVersion = isset($_POST['moodle_version']) ? (int) $_POST['moodle_version'] : 3; $moodleVersion = isset($_POST['moodle_version']) ? (int) $_POST['moodle_version'] : 3;
MoodleExport::debugStaticLog('Starting selected resources Moodle export', [
'course_id' => $courseId,
'export_dir' => $exportDir,
'moodle_version' => $moodleVersion,
]);
$mbzFile = $exporter->export($courseId, $exportDir, $moodleVersion); $mbzFile = $exporter->export($courseId, $exportDir, $moodleVersion);
MoodleExport::debugStaticLog('Selected resources Moodle export finished', [
'mbz_file' => $mbzFile,
]);
echo Display::return_message(get_lang('MoodleExportCreated'), 'confirm'); echo Display::return_message(get_lang('MoodleExportCreated'), 'confirm');
echo '<br />'; echo '<br />';
@@ -84,7 +135,14 @@ if ($action === 'course_select_form' && Security::check_token('post')) {
api_get_path(WEB_CODE_PATH).'course_info/download.php?archive_path=1&archive='.basename($mbzFile).'&'.api_get_cidreq(), api_get_path(WEB_CODE_PATH).'course_info/download.php?archive_path=1&archive='.basename($mbzFile).'&'.api_get_cidreq(),
['class' => 'btn btn-primary btn-large'] ['class' => 'btn btn-primary btn-large']
); );
} catch (Exception $e) { } catch (Throwable $e) {
MoodleExport::restoreMainDatabaseConnection();
if ($debugMoodleExport) {
error_log('[MoodleExport] Export failed: '.$e->getMessage().' in '.$e->getFile().':'.$e->getLine());
error_log('[MoodleExport] Stack trace: '.$e->getTraceAsString());
}
echo Display::return_message(get_lang('ErrorCreatingExport').': '.$e->getMessage(), 'error'); echo Display::return_message(get_lang('ErrorCreatingExport').': '.$e->getMessage(), 'error');
} }
exit(); exit();
@@ -92,11 +150,14 @@ if ($action === 'course_select_form' && Security::check_token('post')) {
echo Display::return_message(get_lang('NoResourcesSelected'), 'warning'); echo Display::return_message(get_lang('NoResourcesSelected'), 'warning');
} }
} else { } else {
MoodleExport::debugStaticLog('Displaying initial export form or processing initial form submission');
$form = new FormValidator( $form = new FormValidator(
'create_export_form', 'create_export_form',
'post', 'post',
api_get_self().'?'.api_get_cidreq() api_get_self().'?'.api_get_cidreq()
); );
$form->addElement('radio', 'export_option', '', get_lang('CreateFullBackup'), 'full_export'); $form->addElement('radio', 'export_option', '', get_lang('CreateFullBackup'), 'full_export');
$form->addElement('radio', 'export_option', '', get_lang('LetMeSelectItems'), 'select_items'); $form->addElement('radio', 'export_option', '', get_lang('LetMeSelectItems'), 'select_items');
$form->addElement('select', 'moodle_version', get_lang('MoodleVersion'), [ $form->addElement('select', 'moodle_version', get_lang('MoodleVersion'), [
@@ -119,27 +180,59 @@ if ($action === 'course_select_form' && Security::check_token('post')) {
// Add buttons // Add buttons
$form->addButtonSave(get_lang('CreateExport')); $form->addButtonSave(get_lang('CreateExport'));
$form->addProgress(); MoodleExport::debugStaticLog('Initial export form built');
MoodleExport::debugStaticLog('Initial export form validate call started');
$isInitialExportFormValid = $form->validate();
MoodleExport::debugStaticLog('Initial export form validate call finished', [
'is_valid' => $isInitialExportFormValid,
]);
if ($isInitialExportFormValid) {
MoodleExport::debugStaticLog('Initial export form validated');
if ($form->validate()) {
$values = $form->exportValues(); $values = $form->exportValues();
MoodleExport::debugStaticLog('Initial export form values exported', [
'export_option' => (string) ($values['export_option'] ?? ''),
'moodle_version' => (string) ($values['moodle_version'] ?? ''),
]);
$adminId = (int) $values['admin_id']; $adminId = (int) $values['admin_id'];
$adminUsername = $values['admin_username']; $adminUsername = $values['admin_username'];
$adminEmail = $values['admin_email']; $adminEmail = $values['admin_email'];
if ($values['export_option'] === 'full_export') { if ($values['export_option'] === 'full_export') {
MoodleExport::debugStaticLog('Full export selected, building complete course');
$cb = new CourseBuilder('complete'); $cb = new CourseBuilder('complete');
$course = $cb->build(); $course = $cb->build();
MoodleExport::restoreMainDatabaseConnection();
MoodleExport::debugStaticLog('Complete course built for full export');
MoodleExport::debugStaticLog('Creating MoodleExport instance for full export');
$exporter = new MoodleExport($course); $exporter = new MoodleExport($course);
MoodleExport::debugStaticLog('MoodleExport instance created for full export');
$exporter->setAdminUserData($adminId, $adminUsername, $adminEmail); $exporter->setAdminUserData($adminId, $adminUsername, $adminEmail);
MoodleExport::debugStaticLog('Admin user data configured', [
'admin_id' => $adminId,
'admin_username' => $adminUsername,
'admin_email' => $adminEmail,
]);
$courseId = api_get_course_id(); // Get course ID $courseId = api_get_course_id(); // Get course ID
$exportDir = 'moodle_export_'.$courseId; $exportDir = 'moodle_export_'.$courseId;
try { try {
$moodleVersion = $values['moodle_version'] ?? '3'; $moodleVersion = isset($values['moodle_version']) ? (int) $values['moodle_version'] : 3;
MoodleExport::debugStaticLog('Starting full Moodle export', [
'course_id' => $courseId,
'export_dir' => $exportDir,
'moodle_version' => $moodleVersion,
]);
$mbzFile = $exporter->export($courseId, $exportDir, $moodleVersion); $mbzFile = $exporter->export($courseId, $exportDir, $moodleVersion);
MoodleExport::debugStaticLog('Full Moodle export finished', [
'mbz_file' => $mbzFile,
]);
echo Display::return_message(get_lang('MoodleExportCreated'), 'confirm'); echo Display::return_message(get_lang('MoodleExportCreated'), 'confirm');
echo '<br />'; echo '<br />';
echo Display::url( echo Display::url(
@@ -147,14 +240,25 @@ if ($action === 'course_select_form' && Security::check_token('post')) {
api_get_path(WEB_CODE_PATH).'course_info/download.php?archive_path=1&archive='.basename($mbzFile).'&'.api_get_cidreq(), api_get_path(WEB_CODE_PATH).'course_info/download.php?archive_path=1&archive='.basename($mbzFile).'&'.api_get_cidreq(),
['class' => 'btn btn-primary btn-large'] ['class' => 'btn btn-primary btn-large']
); );
} catch (Exception $e) { } catch (Throwable $e) {
if ($debugMoodleExport) {
error_log('[MoodleExport] Export failed: '.$e->getMessage().' in '.$e->getFile().':'.$e->getLine());
error_log('[MoodleExport] Stack trace: '.$e->getTraceAsString());
}
echo Display::return_message(get_lang('ErrorCreatingExport').': '.$e->getMessage(), 'error'); echo Display::return_message(get_lang('ErrorCreatingExport').': '.$e->getMessage(), 'error');
} }
} elseif ($values['export_option'] === 'select_items') { } elseif ($values['export_option'] === 'select_items') {
// Partial export - go to the item selection step // Partial export - go to the item selection step
MoodleExport::debugStaticLog('Select items selected, building partial course for resource selection form');
$cb = new CourseBuilder('partial'); $cb = new CourseBuilder('partial');
$course = $cb->build(); $course = $cb->build();
MoodleExport::restoreMainDatabaseConnection();
MoodleExport::debugStaticLog('Partial course built for resource selection form');
if ($course->has_resources()) { if ($course->has_resources()) {
MoodleExport::debugStaticLog('Partial course has resources, rendering resource selection form');
// Add token to Course select form // Add token to Course select form
$hiddenFields['sec_token'] = Security::get_token(); $hiddenFields['sec_token'] = Security::get_token();
$hiddenFields['admin_id'] = $adminId; $hiddenFields['admin_id'] = $adminId;
@@ -162,18 +266,27 @@ if ($action === 'course_select_form' && Security::check_token('post')) {
$hiddenFields['admin_email'] = $adminEmail; $hiddenFields['admin_email'] = $adminEmail;
CourseSelectForm::display_form($course, $hiddenFields, false, true); CourseSelectForm::display_form($course, $hiddenFields, false, true);
MoodleExport::debugStaticLog('Resource selection form rendered');
} else { } else {
MoodleExport::debugStaticLog('Partial course has no resources');
echo Display::return_message(get_lang('NoResourcesToExport'), 'warning'); echo Display::return_message(get_lang('NoResourcesToExport'), 'warning');
} }
} }
} else { } else {
MoodleExport::debugStaticLog('Initial export form not submitted or not valid, rendering form');
echo '<div class="row">'; echo '<div class="row">';
echo '<div class="col-md-12">'; echo '<div class="col-md-12">';
echo '<div class="tool-export">'; echo '<div class="tool-export">';
MoodleExport::debugStaticLog('Initial export form display started');
$form->display(); $form->display();
MoodleExport::debugStaticLog('Initial export form display finished');
echo '</div>';
echo '</div>'; echo '</div>';
echo '</div>'; echo '</div>';
} }
} }
MoodleExport::debugStaticLog('Displaying page footer');
Display::display_footer(); Display::display_footer();
MoodleExport::debugStaticLog('Page footer displayed');
+100 -64
View File
@@ -30,10 +30,7 @@ $domain = 'example.com'; // Manually configured domain for generated emails
// Include Chamilo bootstrap and necessary classes // Include Chamilo bootstrap and necessary classes
require_once __DIR__.'/../../main/inc/global.inc.php'; 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/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) // Command-line arguments parsing (without getopt)
$proceed = false; $proceed = false;
@@ -88,8 +85,8 @@ global $database;
// Load XLSX file // Load XLSX file
try { try {
$inputFileType = PHPExcel_IOFactory::identify($xlsxFile); $inputFileType = \PhpOffice\PhpSpreadsheet\IOFactory::identify($xlsxFile);
$reader = PHPExcel_IOFactory::createReader($inputFileType); $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType);
$phpExcel = $reader->load($xlsxFile); $phpExcel = $reader->load($xlsxFile);
$worksheet = $phpExcel->getActiveSheet(); $worksheet = $phpExcel->getActiveSheet();
$xlsxRows = $worksheet->toArray(); $xlsxRows = $worksheet->toArray();
@@ -105,7 +102,7 @@ $xlsxColumnMap = [
'Mail' => 'email', 'Mail' => 'email',
'Matricule' => 'official_code', 'Matricule' => 'official_code',
'N° de badge' => 'password', 'N° de badge' => 'password',
'Tel mobile' => 'phone', 'tel mobile' => 'phone',
'Actif' => 'active', 'Actif' => 'active',
]; ];
@@ -128,6 +125,7 @@ $xlsxEmailCounts = [];
$xlsxNameCounts = []; $xlsxNameCounts = [];
$duplicateEmails = []; $duplicateEmails = [];
$duplicateNames = []; $duplicateNames = [];
$generatedEmails = []; // username -> generatedEmail
$xlsxUsernames = []; // Store usernames from XLSX $xlsxUsernames = []; // Store usernames from XLSX
// Output columns for missing field and duplicate files // Output columns for missing field and duplicate files
@@ -146,8 +144,8 @@ function normalizeName($name)
function removeAccents($str) function removeAccents($str)
{ {
$str = str_replace( $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'], ['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 $str
); );
@@ -177,37 +175,22 @@ function generateProposedLogin($xlsxLastname, $xlsxFirstname, $isActive, &$usedL
$lastFirstnamePart = end($firstnameParts); $lastFirstnamePart = end($firstnameParts);
$lastPartLetters = strtolower(preg_replace('/[\s-]+/', '', $lastFirstnamePart)); $lastPartLetters = strtolower(preg_replace('/[\s-]+/', '', $lastFirstnamePart));
// Increment occurrence count for this login // Handle duplicates by incrementally adding letters from the last firstname part if active
$usedLogins['counts'][$login] = isset($usedLogins['counts'][$login]) ? $usedLogins['counts'][$login] + 1 : 1; if ($isActive) {
$occurrence = $usedLogins['counts'][$login]; $letterCount = 0;
while (isset($usedLogins['logins'][$login]) && $usedLogins['logins'][$login]['active']) {
// Handle duplicates $letterCount++;
if (isset($usedLogins['logins'][$login])) { if ($letterCount > strlen($lastPartLetters) - 1) {
// Only modify if both current and previous users are active break; // No more letters available. Will append a number below
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
}
} }
$login = $baseLogin.substr($lastPartLetters, 1, $letterCount);
} }
} }
// Ensure uniqueness by appending a number if still conflicting // Ensure uniqueness by appending a number if still conflicting
$suffix = 1; $suffix = 1;
$originalLogin = $login; $originalLogin = $login;
while (isset($usedLogins['logins'][$login])) { while (isset($usedLogins['logins'][$login]) && $usedLogins['logins'][$login]['active']) {
$login = $originalLogin.$suffix; $login = $originalLogin.$suffix;
$suffix++; $suffix++;
} }
@@ -227,21 +210,21 @@ function createMissingFieldFile($filename, $rows, $columns)
return; return;
} }
$phpExcel = new PHPExcel(); $phpExcel = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$worksheet = $phpExcel->getActiveSheet(); $worksheet = $phpExcel->getActiveSheet();
foreach ($columns as $colIndex => $column) { foreach ($columns as $colIndex => $column) {
$worksheet->setCellValueByColumnAndRow($colIndex, 1, $column); $worksheet->setCellValueByColumnAndRow($colIndex + 1, 1, $column);
} }
foreach ($rows as $rowIndex => $rowData) { foreach ($rows as $rowIndex => $rowData) {
foreach ($columns as $colIndex => $column) { foreach ($columns as $colIndex => $column) {
$worksheet->setCellValueByColumnAndRow($colIndex, $rowIndex + 2, $rowData[$column]); $worksheet->setCellValueByColumnAndRow($colIndex + 1, $rowIndex + 2, $rowData[$column]);
} }
} }
try { try {
$writer = PHPExcel_IOFactory::createWriter($phpExcel, 'Excel2007'); $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpExcel, 'Xlsx');
$writer->save($filename); $writer->save($filename);
echo "Generated $filename with ".count($rows)." rows\n"; echo "Generated $filename with ".count($rows)." rows\n";
} catch (Exception $e) { } catch (Exception $e) {
@@ -249,6 +232,19 @@ function createMissingFieldFile($filename, $rows, $columns)
} }
} }
/**
* Generate a tentative e-mail address from firstname and lastname.
*/
function generateMailFromFirstAndLastNames(string $firstname, string $lastname, string $domain): string
{
$emailLastnameParts = preg_split('/[\s-]+/', trim(removeAccents($lastname)), -1, PREG_SPLIT_NO_EMPTY);
$emailLastname = !empty($emailLastnameParts[0]) ? strtolower($emailLastnameParts[0]) : '';
$emailFirstnameParts = preg_split('/[\s-]+/', trim(removeAccents($firstname)), -1, PREG_SPLIT_NO_EMPTY);
$emailFirstname = !empty($emailFirstnameParts[0]) ? strtolower($emailFirstnameParts[0]) : '';
return "$emailLastname.$emailFirstname@$domain";
}
// Detect potential issues in XLSX file // Detect potential issues in XLSX file
$usedLogins = ['logins' => [], 'counts' => []]; $usedLogins = ['logins' => [], 'counts' => []];
$generatedEmailCounts = []; $generatedEmailCounts = [];
@@ -295,27 +291,21 @@ foreach ($xlsxRows as $rowIndex => $xlsxRow) {
]; ];
if ($isActive) { if ($isActive) {
if (empty($xlsxUserData['email']) && strpos($xlsxUserData['official_code'], '0009') !== false) { if (empty($xlsxUserData['email'])) {
$emailLastnameParts = preg_split('/[\s-]+/', trim(removeAccents($xlsxUserData['lastname'])), -1, PREG_SPLIT_NO_EMPTY); $generatedEmail = $baseEmail = generateMailFromFirstAndLastNames($xlsxUserData['firstname'], $xlsxUserData['lastname'], $domain);
$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; $suffix = isset($generatedEmailCounts[$baseEmail]) ? count($generatedEmailCounts[$baseEmail]) + 1 : 1;
if ($suffix > 1) { if ($suffix > 1) {
$generatedEmail = "{$emailLastname}.{$emailFirstname}{$suffix}@{$domain}"; $generatedEmail = preg_replace('/^([^@]+)@(.+)/', '${1}'.$suffix.'@${2}', $baseEmail);
} }
$generatedEmail = strtoupper($generatedEmail); $generatedEmail = strtoupper($generatedEmail);
$generatedEmailCounts[$baseEmail][] = $rowData; $generatedEmailCounts[$baseEmail][] = $rowData;
$rowData['Mail'] = $generatedEmail; $rowData['Mail'] = $generatedEmail;
$xlsxUserData['email'] = $generatedEmail; $xlsxUserData['email'] = $generatedEmail;
$xlsxUserData['emailSource'] = 'Generated during import';
$emailMissing[] = $rowData; $emailMissing[] = $rowData;
$xlsxEmailCounts[$generatedEmail][] = $rowData; $xlsxEmailCounts[$generatedEmail][] = $rowData;
} elseif (empty($xlsxUserData['email'])) { $generatedEmails[$xlsxUserData['official_code']] = [$generatedEmail];
$emailMissing[] = $rowData;
} }
if (empty($xlsxUserData['lastname'])) { if (empty($xlsxUserData['lastname'])) {
@@ -362,7 +352,7 @@ createMissingFieldFile($outputDir.'duplicate_name.xlsx', $duplicateNames, $outpu
// Generate unmatched_db_users.xlsx // Generate unmatched_db_users.xlsx
$unmatchedUsers = []; $unmatchedUsers = [];
$sql = "SELECT id, username, official_code, email FROM user"; $sql = "SELECT id, username, official_code, email, active FROM user";
$stmt = $database->query($sql); $stmt = $database->query($sql);
while ($dbUser = $stmt->fetch()) { while ($dbUser = $stmt->fetch()) {
if (!in_array($dbUser['username'], $xlsxUsernames) && !empty($dbUser['username'])) { if (!in_array($dbUser['username'], $xlsxUsernames) && !empty($dbUser['username'])) {
@@ -371,10 +361,11 @@ while ($dbUser = $stmt->fetch()) {
'Username' => $dbUser['username'], 'Username' => $dbUser['username'],
'User ID' => $dbUser['id'], 'User ID' => $dbUser['id'],
'E-mail' => $dbUser['email'], 'E-mail' => $dbUser['email'],
'Active' => $dbUser['active'] ? 'Yes' : 'No',
]; ];
} }
} }
$unmatchedColumns = ['Matricule', 'Username', 'User ID', 'E-mail']; $unmatchedColumns = ['Matricule', 'Username', 'User ID', 'E-mail', 'Active'];
createMissingFieldFile($outputDir.'unmatched_db_users.xlsx', $unmatchedUsers, $unmatchedColumns); createMissingFieldFile($outputDir.'unmatched_db_users.xlsx', $unmatchedUsers, $unmatchedColumns);
// Process users: compare with database, log decisions, and update/insert if --proceed // Process users: compare with database, log decisions, and update/insert if --proceed
@@ -383,8 +374,10 @@ $userManager = new UserManager();
$usedLogins = ['logins' => [], 'counts' => []]; // Reset usedLogins to avoid false duplicates $usedLogins = ['logins' => [], 'counts' => []]; // Reset usedLogins to avoid false duplicates
$emptyRowCount = 0; $emptyRowCount = 0;
$userActions = []; // Initialize array to store user actions $userActions = []; // Initialize array to store user actions
$userSkippedWhileActive = []; // Initialize array to store special cases
foreach ($xlsxRows as $rowIndex => $rowData) { foreach ($xlsxRows as $rowIndex => $rowData) {
// Check for empty row // Check for empty row
$emailSource = 'SAP';
$isEmpty = true; $isEmpty = true;
foreach ($rowData as $cell) { foreach ($rowData as $cell) {
if (!empty(trim($cell))) { if (!empty(trim($cell))) {
@@ -413,26 +406,36 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
$xlsxUserData['username'] = generateProposedLogin($xlsxUserData['lastname'], $xlsxUserData['firstname'], $isActive, $usedLogins); $xlsxUserData['username'] = generateProposedLogin($xlsxUserData['lastname'], $xlsxUserData['firstname'], $isActive, $usedLogins);
$dbUsername = Database::escape_string($xlsxUserData['username']); $dbUsername = Database::escape_string($xlsxUserData['username']);
if (!empty($xlsxUserData['official_code']) && !empty($generatedEmails[$xlsxUserData['official_code']])) {
$emailSource = 'E-mail generated during import';
$xlsxUserData['email'] = $generatedEmails[$xlsxUserData['official_code']];
} elseif (!empty($rowData['emailSource'])) {
$emailSource = $rowData['emailSource'];
}
// Get current time for row logging // Get current time for row logging
$rowTime = new DateTime(); $rowTime = new DateTime();
// Skip users with Matricule starting with 0009 // Skip users with Matricule starting with 0009
if (strpos($xlsxUserData['official_code'], '0009') === 0) { if (strpos($xlsxUserData['official_code'], '0009') === 0) {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - Matricule starts with 0009 (username: $dbUsername)\n"; echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - Matricule starts with 0009 (username: $dbUsername)\n";
$userActions[] = [ $logRow = [
'Action Type' => 'skipped', 'Action Type' => 'skipped',
'User ID' => '', 'User ID' => '',
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxUserData['official_code'], 'External User ID' => $xlsxUserData['official_code'],
'Updated Fields' => 'Matricule starts with 0009', 'Updated Fields' => 'Matricule starts with 0009',
]; ];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
continue; continue;
} }
// Check for existing user by username // Check for existing user by username
$sql = "SELECT id, firstname, lastname, email, official_code, phone, active $sql = "SELECT id, firstname, lastname, email, official_code, phone, active, status, picture_uri, expiration_date, language, creator_id
FROM user FROM user
WHERE username = '$dbUsername'"; WHERE username = '$dbUsername'";
$stmt = $database->query($sql); $stmt = $database->query($sql);
@@ -445,14 +448,16 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
// Decision logic // Decision logic
if (empty($dbUser) && empty($xlsxUserData['active'])) { 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"; echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - 'Actif' is empty and no matching user in database (username: $dbUsername)\n";
$emailSource = 'Not relevant (user ignored)';
$userActions[] = [ $userActions[] = [
'Action Type' => 'skipped', 'Action Type' => 'skipped',
'User ID' => '', 'User ID' => '',
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Actif is empty and no matching user in database', 'Updated Fields' => '"Active" field is empty and no matching user in database',
]; ];
continue; continue;
} }
@@ -463,23 +468,30 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
foreach ($requiredFields as $field) { foreach ($requiredFields as $field) {
if (empty($xlsxUserData[$field])) { if (empty($xlsxUserData[$field])) {
$missingFields[] .= $field; $missingFields[] .= $field;
if ($field == 'email') {
$emailSource = 'EMPTY IN SAP';
}
} }
} }
if (!empty($missingFields)) { if (!empty($missingFields)) {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).': Skipped - missing fields: '.implode(', ', $missingFields)." (username: $dbUsername)\n"; echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).': Skipped - missing fields: '.implode(', ', $missingFields)." (username: $dbUsername)\n";
$userActions[] = [ $logRow = [
'Action Type' => 'skipped', 'Action Type' => 'skipped',
'User ID' => '', 'User ID' => '',
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Missing fields: '.implode(', ', $missingFields), 'Updated Fields' => 'Missing fields: '.implode(', ', $missingFields),
]; ];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
continue; continue;
} }
// If the user was found/existed in the local database
if ($dbUser) { if ($dbUser) {
// Check for updates // Check for updates
$updates = []; $updates = [];
@@ -515,12 +527,16 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
null, // password not updated null, // password not updated
null, // auth_source null, // auth_source
$xlsxUserData['email'], $xlsxUserData['email'],
null, // status $dbUser['status'], // status
$xlsxUserData['official_code'], $xlsxUserData['official_code'],
$xlsxUserData['phone'], $xlsxUserData['phone'],
null, // picture_uri $dbUser['picture_uri'], // picture_uri
null, // expiration_date $dbUser['expiration_date'], // expiration_date
$xlsxActive $xlsxActive,
$dbUser['creator_id'],
0,
null,
$dbUser['language']
); );
if ($user) { if ($user) {
// Update extra field 'external_user_id' // Update extra field 'external_user_id'
@@ -532,32 +548,39 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => implode(', ', array_map(function ($update) { return trim(explode(':', $update)[0]); }, $updates)), 'Updated Fields' => implode(', ', array_map(function ($update) { return trim(explode(':', $update)[0]); }, $updates)),
]; ];
} else { } else {
echo " Error: Could not update user (username: $dbUsername)\n"; echo " Error: Could not update user (username: $dbUsername)\n";
$userActions[] = [ $logRow = [
'Action Type' => 'skipped', 'Action Type' => 'skipped',
'User ID' => $dbUser['id'], 'User ID' => $dbUser['id'],
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Could not update user', 'Updated Fields' => 'Could not update user',
]; ];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
} }
} catch (Exception $e) { } catch (Exception $e) {
echo " Error: Failed to update user (username: $dbUsername): {$e->getMessage()}\n"; echo " Error: Failed to update user (username: $dbUsername): {$e->getMessage()}\n";
$userActions[] = [ $logRow = [
'Action Type' => 'skipped', 'Action Type' => 'skipped',
'User ID' => $dbUser['id'], 'User ID' => $dbUser['id'],
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Failed to update user: '.$e->getMessage(), 'Updated Fields' => 'Failed to update user: '.$e->getMessage(),
]; ];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
} }
} else { } else {
echo " Sim mode: Updated user and external_user_id (username: $dbUsername)\n"; echo " Sim mode: Updated user and external_user_id (username: $dbUsername)\n";
@@ -567,21 +590,25 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => implode(', ', array_map(function ($update) { return trim(explode(':', $update)[0]); }, $updates)), 'Updated Fields' => implode(', ', array_map(function ($update) { return trim(explode(':', $update)[0]); }, $updates)),
]; ];
} }
} else { } else {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": No action - no changes needed (username: $dbUsername)\n"; echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": No action - no changes needed (username: $dbUsername)\n";
$userActions[] = [ $logRow = [
'Action Type' => 'skipped', 'Action Type' => 'skipped',
'User ID' => $dbUser['id'], 'User ID' => $dbUser['id'],
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => 'No changes needed', 'Updated Fields' => 'No changes needed',
]; ];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
} }
} else { } else {
echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Insert new user - No existing user found (username: $dbUsername)\n"; echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Insert new user - No existing user found (username: $dbUsername)\n";
@@ -602,7 +629,7 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
null, null,
null, null,
$xlsxActive, $xlsxActive,
null, 0,
null // creator_id null // creator_id
); );
if ($userId) { if ($userId) {
@@ -615,32 +642,39 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => '', 'Updated Fields' => '',
]; ];
} else { } else {
echo " Error: Could not create user (username: $dbUsername)\n"; echo " Error: Could not create user (username: $dbUsername)\n";
$userActions[] = [ $logRow = [
'Action Type' => 'skipped', 'Action Type' => 'skipped',
'User ID' => '', 'User ID' => '',
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Could not create user', 'Updated Fields' => 'Could not create user',
]; ];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
} }
} catch (Exception $e) { } catch (Exception $e) {
echo " Error: Failed to insert user (username: $dbUsername): {$e->getMessage()}\n"; echo " Error: Failed to insert user (username: $dbUsername): {$e->getMessage()}\n";
$userActions[] = [ $logRow = [
'Action Type' => 'skipped', 'Action Type' => 'skipped',
'User ID' => '', 'User ID' => '',
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => 'Failed to insert user: '.$e->getMessage(), 'Updated Fields' => 'Failed to insert user: '.$e->getMessage(),
]; ];
$userActions[] = $logRow;
$userSkippedWhileActive[] = $logRow;
} }
} else { } else {
echo " Sim mode: Inserted user and external_user_id (username: $dbUsername)\n"; echo " Sim mode: Inserted user and external_user_id (username: $dbUsername)\n";
@@ -650,6 +684,7 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
'Username' => $dbUsername, 'Username' => $dbUsername,
'Official Code' => $xlsxUserData['official_code'], 'Official Code' => $xlsxUserData['official_code'],
'E-mail' => $xlsxUserData['email'], 'E-mail' => $xlsxUserData['email'],
'E-mail source' => $emailSource,
'External User ID' => $xlsxMatricule, 'External User ID' => $xlsxMatricule,
'Updated Fields' => '', 'Updated Fields' => '',
]; ];
@@ -658,8 +693,9 @@ foreach ($xlsxRows as $rowIndex => $rowData) {
} }
// Generate user actions XLSX file // Generate user actions XLSX file
$actionColumns = ['Action Type', 'User ID', 'Username', 'Official Code', 'E-mail', 'External User ID', 'Updated Fields']; $actionColumns = ['Action Type', 'User ID', 'Username', 'Official Code', 'E-mail', 'E-mail source', 'External User ID', 'Updated Fields'];
createMissingFieldFile($outputDir.'user_actions.xlsx', $userActions, $actionColumns); createMissingFieldFile($outputDir.'user_actions.xlsx', $userActions, $actionColumns);
createMissingFieldFile($outputDir.'skipped_user_actions.xlsx', $userSkippedWhileActive, $actionColumns);
if (!$proceed) { if (!$proceed) {
echo "\nUse --proceed to apply changes to the database.\n"; echo "\nUse --proceed to apply changes to the database.\n";
+1 -1
View File
@@ -28,7 +28,7 @@ foreach ($langs as $lang) {
$out = []; $out = [];
if (is_file($file)) { 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,true));
@exec('php -l '.$file, $out); @exec('php -l '.escapeshellarg($file), $out);
if (substr($out[0], 0, 2) != 'No') { if (substr($out[0], 0, 2) != 'No') {
echo $out[0]."\n"; echo $out[0]."\n";
$ok = false; $ok = false;
+2 -2
View File
@@ -1094,8 +1094,8 @@ function store_add_dropbox($file = [], $work = null)
$dropbox_filename = add_ext_on_mime($dropbox_filename, $dropbox_filetype); $dropbox_filename = add_ext_on_mime($dropbox_filename, $dropbox_filetype);
// Replace dangerous characters // Replace dangerous characters
$dropbox_filename = api_replace_dangerous_char($dropbox_filename); $dropbox_filename = api_replace_dangerous_char($dropbox_filename);
// Transform any .php file in .phps fo security // Transform any .php file in .phps and any .htaccess in htaccess.txt for security
$dropbox_filename = php2phps($dropbox_filename); $dropbox_filename = disable_dangerous_file($dropbox_filename);
//filter extension //filter extension
if (!filter_extension($dropbox_filename)) { if (!filter_extension($dropbox_filename)) {
+1 -7
View File
@@ -215,15 +215,9 @@ class Draggable extends Question
{ {
$header = parent::return_header($exercise, $counter, $score); $header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'"><tr>'; $header .= '<table class="'.$this->question_table_class.'"><tr>';
if ($exercise->showExpectedChoice()) {
$header .= '<th>'.get_lang('YourChoice').'</th>';
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
} else {
$header .= '<th>'.get_lang('ElementList').'</th>'; $header .= '<th>'.get_lang('ElementList').'</th>';
$header .= '<th>'.get_lang('YourChoice').'</th>'; $header .= '<th>'.get_lang('YourChoice').'</th>';
if ($exercise->showExpectedChoice() || $exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>'; $header .= '<th>'.get_lang('ExpectedChoice').'</th>';
} }
$header .= '<th>'.get_lang('Status').'</th>'; $header .= '<th>'.get_lang('Status').'</th>';
+1 -1
View File
@@ -1050,7 +1050,7 @@ class Answer
} }
// Fix correct answers // Fix correct answers
if (in_array($newQuestion->type, [DRAGGABLE, MATCHING, MATCHING_DRAGGABLE])) { if (in_array($newQuestion->type, [DRAGGABLE, MATCHING, MATCHING_DRAGGABLE, MATCHING_COMBINATION, MATCHING_DRAGGABLE_COMBINATION])) {
$onlyAnswersFlip = array_flip($onlyAnswers); $onlyAnswersFlip = array_flip($onlyAnswers);
foreach ($correctAnswers as $answer_id => $correct_answer) { foreach ($correctAnswers as $answer_id => $correct_answer) {
$params = []; $params = [];
+25 -82
View File
@@ -1465,56 +1465,6 @@ class Exercise
} }
} }
/**
* changes the exercise sound file.
*
* @author Olivier Brouckaert
*
* @param string $sound - exercise sound file
* @param string $delete - ask to delete the file
*/
public function updateSound($sound, $delete)
{
global $audioPath, $documentPath;
$TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
if ($sound['size'] && (strstr($sound['type'], 'audio') || strstr($sound['type'], 'video'))) {
$this->sound = $sound['name'];
if (@move_uploaded_file($sound['tmp_name'], $audioPath.'/'.$this->sound)) {
$sql = "SELECT 1 FROM $TBL_DOCUMENT
WHERE
c_id = ".$this->course_id." AND
path = '".str_replace($documentPath, '', $audioPath).'/'.$this->sound."'";
$result = Database::query($sql);
if (!Database::num_rows($result)) {
$id = add_document(
$this->course,
str_replace($documentPath, '', $audioPath).'/'.$this->sound,
'file',
$sound['size'],
$sound['name']
);
api_item_property_update(
$this->course,
TOOL_DOCUMENT,
$id,
'DocumentAdded',
api_get_user_id()
);
item_property_update_on_folder(
$this->course,
str_replace($documentPath, '', $audioPath),
api_get_user_id()
);
}
}
} elseif ($delete && is_file($audioPath.'/'.$this->sound)) {
$this->sound = '';
}
}
/** /**
* changes the exercise type. * changes the exercise type.
* *
@@ -4516,7 +4466,7 @@ class Exercise
if (!$switchableAnswerSet) { if (!$switchableAnswerSet) {
// not switchable answer, must be in the same place than teacher order // not switchable answer, must be in the same place than teacher order
for ($i = 0; $i < count($listCorrectAnswers['words']); $i++) { for ($i = 0; $i < count($listCorrectAnswers['words']); $i++) {
$studentAnswer = isset($choice[$i]) ? $choice[$i] : ''; $studentAnswer = $choice[$i] ?? '';
$correctAnswer = $listCorrectAnswers['words'][$i]; $correctAnswer = $listCorrectAnswers['words'][$i];
if ($debug) { if ($debug) {
@@ -4527,16 +4477,16 @@ class Exercise
// This value is the user input, not escaped while correct answer is escaped by ckeditor // This value is the user input, not escaped while correct answer is escaped by ckeditor
// Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618 // Works with cyrillic alphabet and when using ">" chars see #7718 #7610 #7618
// ENT_QUOTES is used in order to transform ' to &#039; // ENT_QUOTES is used in order to transform ' to &#039;
if (!$from_database) { //if (!$from_database) {
$studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer); $studentAnswer = FillBlanks::clearStudentAnswer($studentAnswer);
if ($debug) { if ($debug) {
error_log('Student answer cleaned:'); error_log('Student answer cleaned:');
error_log($studentAnswer); error_log($studentAnswer);
} }
} //}
$isAnswerCorrect = 0; $isAnswerCorrect = 0;
if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database)) { if (FillBlanks::isStudentAnswerGood($studentAnswer, $correctAnswer, $from_database, true)) {
// gives the related weighting to the student // gives the related weighting to the student
$questionScore += $answerWeighting[$i]; $questionScore += $answerWeighting[$i];
// increments total score // increments total score
@@ -4594,7 +4544,7 @@ class Exercise
$found = false; $found = false;
for ($j = 0; $j < count($listTeacherAnswerTemp); $j++) { for ($j = 0; $j < count($listTeacherAnswerTemp); $j++) {
$correctAnswer = isset($listTeacherAnswerTemp[$j]) ? $listTeacherAnswerTemp[$j] : ''; $correctAnswer = $listTeacherAnswerTemp[$j] ?? '';
if (is_array($listTeacherAnswerTemp)) { if (is_array($listTeacherAnswerTemp)) {
$correctAnswer = implode('||', $listTeacherAnswerTemp); $correctAnswer = implode('||', $listTeacherAnswerTemp);
} }
@@ -4995,7 +4945,7 @@ class Exercise
if (false === $this->showExpectedChoice() && if (false === $this->showExpectedChoice() &&
false === $showTotalScoreAndUserChoicesInLastAttempt false === $showTotalScoreAndUserChoicesInLastAttempt
) { ) {
$user_answer = ''; $this->hideExpectedAnswer = true;
} }
switch ($answerType) { switch ($answerType) {
case MATCHING: case MATCHING:
@@ -5057,9 +5007,6 @@ class Exercise
echo '</tr>'; echo '</tr>';
break; break;
case DRAGGABLE: case DRAGGABLE:
if (false == $showTotalScoreAndUserChoicesInLastAttempt) {
$s_answer_label = '';
}
if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) { if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $this->results_disabled) {
if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) { if (false === $showTotalScoreAndUserChoicesInLastAttempt && empty($s_user_answer)) {
break; break;
@@ -5067,35 +5014,15 @@ class Exercise
} }
echo '<tr>'; echo '<tr>';
if ($this->showExpectedChoice()) { if ($this->showExpectedChoice() || $this->showExpectedChoiceColumn()) {
if (!in_array($this->results_disabled, [
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
//RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
])
) {
echo '<td>'.$user_answer.'</td>';
} else {
$status = Display::label(get_lang('Correct'), 'success');
}
echo '<td>'.$s_answer_label.'</td>'; echo '<td>'.$s_answer_label.'</td>';
echo '<td>'.$user_answer.'</td>';
echo '<td>'.$real_list[$i_answer_correct_answer].'</td>';
echo '<td>'.$status.'</td>'; echo '<td>'.$status.'</td>';
} else { } else {
echo '<td>'.$s_answer_label.'</td>'; echo '<td>'.$s_answer_label.'</td>';
echo '<td>'.$user_answer.'</td>'; echo '<td>'.$user_answer.'</td>';
echo '<td>'.$counterAnswer.'</td>';
echo '<td>'.$status.'</td>'; echo '<td>'.$status.'</td>';
echo '<td>';
if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
if (isset($real_list[$i_answer_correct_answer]) &&
$showTotalScoreAndUserChoicesInLastAttempt === true
) {
echo Display::span(
$real_list[$i_answer_correct_answer],
['style' => 'color: #008000; font-weight: bold;']
);
}
}
echo '</td>';
} }
echo '</tr>'; echo '</tr>';
break; break;
@@ -10043,6 +9970,14 @@ class Exercise
); );
} else { } else {
if ($row['active'] == 0 || $visibility == 0) { if ($row['active'] == 0 || $visibility == 0) {
$visibleOnBaseCourse = api_get_item_visibility(
$courseInfo,
TOOL_QUIZ,
$row['iid'],
0
);
if ($visibleOnBaseCourse) {
$visibility = Display::url( $visibility = Display::url(
Display::return_icon( Display::return_icon(
'invisible.png', 'invisible.png',
@@ -10052,6 +9987,14 @@ class Exercise
), ),
'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['iid'] 'exercise.php?'.api_get_cidreq().'&choice=enable&sec_token='.$token.'&exerciseId='.$row['iid']
); );
} else {
$visibility = Display::return_icon(
'invisible.png',
get_lang('Activate'),
'',
ICON_SIZE_SMALL
);
}
} else { } else {
// else if not active // else if not active
$visibility = Display::url( $visibility = Display::url(
+38
View File
@@ -266,6 +266,22 @@ if (!empty($action) && $is_allowedToEdit) {
break; break;
} }
if (!empty($sessionId)) {
$visibleOnBaseCourse = api_get_item_visibility(
$courseInfo,
TOOL_QUIZ,
$objExerciseTmp->iid,
0
);
if (!$visibleOnBaseCourse) {
Display::addFlash(Display::return_message(
sprintf(get_lang('CannotChangeVisibilityOfBaseCourseResourceX'), $objExerciseTmp->name),
'error'
));
break;
}
}
// enables an exercise // enables an exercise
if (empty($sessionId)) { if (empty($sessionId)) {
$objExerciseTmp->enable(); $objExerciseTmp->enable();
@@ -368,6 +384,22 @@ if ($is_allowedToEdit) {
break; break;
} }
if (!empty($sessionId)) {
$visibleOnBaseCourse = api_get_item_visibility(
$courseInfo,
TOOL_QUIZ,
$objExerciseTmp->iid,
0
);
if (!$visibleOnBaseCourse) {
Display::addFlash(Display::return_message(
sprintf(get_lang('CannotChangeVisibilityOfBaseCourseResourceX'), $objExerciseTmp->name),
'error'
));
break;
}
}
// Enables an exercise // Enables an exercise
if (empty($sessionId)) { if (empty($sessionId)) {
$objExerciseTmp->enable(); $objExerciseTmp->enable();
@@ -520,6 +552,12 @@ if ($is_allowedToEdit) {
// Teacher change exercise // Teacher change exercise
break; break;
} }
// Security: reject path traversal attempts (CWE-22)
if (!Security::check_abs_path($documentPath.$file, $documentPath.'/')) {
api_not_allowed(true);
}
// deletes an exercise // deletes an exercise
$imgparams = []; $imgparams = [];
$imgcount = 0; $imgcount = 0;
+2 -2
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 = Database::get_course_table(TABLE_QUIZ_TEST);
$TBL_EXERCISES_QUESTION = Database::get_course_table(TABLE_QUIZ_QUESTION); $TBL_EXERCISES_QUESTION = Database::get_course_table(TABLE_QUIZ_QUESTION);
$TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING); $TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
Display::display_header($nameTools, 'Exercise'); Display::display_header(get_lang('ViewHistoryChange'), 'Exercise');
if (isset($_GET['message'])) { if (isset($_GET['message'])) {
if (in_array($_GET['message'], ['ExerciseEdited'])) { if (in_array($_GET['message'], ['ExerciseEdited'])) {
@@ -79,7 +79,7 @@ while ($row = Database::fetch_array($query)) {
echo '<td>'.$row['question'].'</td>'; echo '<td>'.$row['question'].'</td>';
echo '<td>'.$row['marks'].'</td>'; echo '<td>'.$row['marks'].'</td>';
if (!empty($row['teacher_comment'])) { if (!empty($row['teacher_comment'])) {
echo '<td>'.$row['teacher_comment'].'</td>'; echo '<td>'.Security::remove_XSS($row['teacher_comment']).'</td>';
} else { } else {
echo '<td>'.get_lang('WithoutComment').'</td>'; echo '<td>'.get_lang('WithoutComment').'</td>';
} }
+5 -5
View File
@@ -491,12 +491,12 @@ class ExerciseResult
$filename = 'exercise_results_user_'.$user_id.'_'.$now.'.xlsx'; $filename = 'exercise_results_user_'.$user_id.'_'.$now.'.xlsx';
} }
$spreadsheet = new PHPExcel(); $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$spreadsheet->setActiveSheetIndex(0); $spreadsheet->setActiveSheetIndex(0);
$worksheet = $spreadsheet->getActiveSheet(); $worksheet = $spreadsheet->getActiveSheet();
$line = 1; // Skip first line $line = 1;
$column = 0; //skip the first column (row titles) $column = 1;
// check if exists column 'user' // check if exists column 'user'
$with_column_user = false; $with_column_user = false;
@@ -584,7 +584,7 @@ class ExerciseResult
$line++; $line++;
foreach ($this->results as $row) { foreach ($this->results as $row) {
$column = 0; $column = 1;
if ($with_column_user) { if ($with_column_user) {
if (api_is_western_name_order()) { if (api_is_western_name_order()) {
$worksheet->setCellValueByColumnAndRow( $worksheet->setCellValueByColumnAndRow(
@@ -738,7 +738,7 @@ class ExerciseResult
} }
$file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename); $file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename);
$writer = new PHPExcel_Writer_Excel2007($spreadsheet); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save($file); $writer->save($file);
DocumentManager::file_send_for_download($file, true, $filename); DocumentManager::file_send_for_download($file, true, $filename);
+5 -6
View File
@@ -111,11 +111,10 @@ if (api_is_course_admin() && !in_array($origin, ['learnpath', 'embeddable', 'ifr
); );
} }
$exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId); $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
$learnpath_id = isset($exercise_stat_info['orig_lp_id']) ? $exercise_stat_info['orig_lp_id'] : 0; $learnpath_id = $exercise_stat_info['orig_lp_id'] ?? 0;
$learnpath_item_id = isset($exercise_stat_info['orig_lp_item_id']) ? $exercise_stat_info['orig_lp_item_id'] : 0; $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'] ?? 0;
$learnpath_item_view_id = isset($exercise_stat_info['orig_lp_item_view_id']) $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'] ?? 0;
? $exercise_stat_info['orig_lp_item_view_id'] : 0; $exerciseId = $exercise_stat_info['exe_exo_id'] ?? 0;
$exerciseId = isset($exercise_stat_info['exe_exo_id']) ? $exercise_stat_info['exe_exo_id'] : 0;
$logInfo = [ $logInfo = [
'tool' => TOOL_QUIZ, 'tool' => TOOL_QUIZ,
@@ -357,7 +356,7 @@ $template->assign('actions', $pageActions);
$template->assign('content', $template->fetch($template->get_template('exercise/result.tpl'))); $template->assign('content', $template->fetch($template->get_template('exercise/result.tpl')));
$template->display_one_col_template(); $template->display_one_col_template();
function showEmbeddableFinishButton() function showEmbeddableFinishButton(): string
{ {
$js = '<script> $js = '<script>
$(function () { $(function () {
+7 -1
View File
@@ -974,9 +974,15 @@ if ('export' === $action) {
$content = Security::remove_XSS($content); $content = Security::remove_XSS($content);
$includeOfficialCode = "";
if (true === api_get_configuration_value('quiz_result_pdf_export_include_official_code_in_file_name')) {
$includeOfficialCode = $user_info['official_code'].' ';
}
$params = [ $params = [
'filename' => api_replace_dangerous_char( 'filename' => api_replace_dangerous_char(
$objExercise->name.' '. $objExercise->name.' '.
$includeOfficialCode.
$user_info['complete_name'].' '. $user_info['complete_name'].' '.
api_get_local_time() api_get_local_time()
), ),
@@ -999,7 +1005,7 @@ if ('export' === $action) {
if (!is_dir($exportFolderPath)) { if (!is_dir($exportFolderPath)) {
@mkdir($exportFolderPath); @mkdir($exportFolderPath);
} }
$pdfFileName = $user_info['firstname'].' '.$user_info['lastname'].'-attemptId'.$id.'.pdf'; $pdfFileName = $includeOfficialCode.$user_info['firstname'].' '.$user_info['lastname'].'-attemptId'.$id.'.pdf';
$pdfFileName = api_replace_dangerous_char($pdfFileName); $pdfFileName = api_replace_dangerous_char($pdfFileName);
$fileNameToSave = $exportFolderPath.'/'.$pdfFileName; $fileNameToSave = $exportFolderPath.'/'.$pdfFileName;
$pdf->html_to_pdf_with_template($content, true, false, true, [], 'F', $fileNameToSave); $pdf->html_to_pdf_with_template($content, true, false, true, [], 'F', $fileNameToSave);
+6 -6
View File
@@ -123,11 +123,11 @@ $learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_R
$reminder = isset($_REQUEST['reminder']) ? (int) $_REQUEST['reminder'] : 0; $reminder = isset($_REQUEST['reminder']) ? (int) $_REQUEST['reminder'] : 0;
$remind_question_id = isset($_REQUEST['remind_question_id']) ? (int) $_REQUEST['remind_question_id'] : 0; $remind_question_id = isset($_REQUEST['remind_question_id']) ? (int) $_REQUEST['remind_question_id'] : 0;
$exerciseId = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : 0; $exerciseId = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : 0;
$formSent = isset($_REQUEST['formSent']) ? $_REQUEST['formSent'] : null; $formSent = $_REQUEST['formSent'] ?? null;
$exerciseResult = isset($_REQUEST['exerciseResult']) ? $_REQUEST['exerciseResult'] : null; $exerciseResult = $_REQUEST['exerciseResult'] ?? null;
$exerciseResultCoordinates = isset($_REQUEST['exerciseResultCoordinates']) ? $_REQUEST['exerciseResultCoordinates'] : null; $exerciseResultCoordinates = $_REQUEST['exerciseResultCoordinates'] ?? null;
$choice = isset($_REQUEST['choice']) ? $_REQUEST['choice'] : null; $choice = $_REQUEST['choice'] ?? null;
$choice = empty($choice) ? isset($_REQUEST['choice2']) ? $_REQUEST['choice2'] : null : null; $choice = empty($choice) ? $_REQUEST['choice2'] ?? null : null;
$current_question = $currentQuestionFromUrl = isset($_REQUEST['num']) ? (int) $_REQUEST['num'] : null; $current_question = $currentQuestionFromUrl = isset($_REQUEST['num']) ? (int) $_REQUEST['num'] : null;
$currentAnswer = isset($_REQUEST['num_answer']) ? (int) $_REQUEST['num_answer'] : null; $currentAnswer = isset($_REQUEST['num_answer']) ? (int) $_REQUEST['num_answer'] : null;
$logInfo = [ $logInfo = [
@@ -1081,7 +1081,7 @@ if (!api_is_allowed_to_session_edit()) {
} }
$exercise_timeover = false; $exercise_timeover = false;
$limit_time_exists = !empty($objExercise->start_time) || !empty($objExercise->end_time) ? true : false; $limit_time_exists = !empty($objExercise->start_time) || !empty($objExercise->end_time);
if ($limit_time_exists) { if ($limit_time_exists) {
$exercise_start_time = api_strtotime($objExercise->start_time, 'UTC'); $exercise_start_time = api_strtotime($objExercise->start_time, 'UTC');
$exercise_end_time = api_strtotime($objExercise->end_time, 'UTC'); $exercise_end_time = api_strtotime($objExercise->end_time, 'UTC');
+3 -3
View File
@@ -3,7 +3,6 @@
/* For licensing terms, see /license.txt */ /* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Component\Utils\ChamiloApi; use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
use Symfony\Component\DomCrawler\Crawler;
/** /**
* @copyright (c) 2001-2006 Universite catholique de Louvain (UCL) * @copyright (c) 2001-2006 Universite catholique de Louvain (UCL)
@@ -350,7 +349,7 @@ function parseQti2($xmlData)
global $questionTempDir; global $questionTempDir;
global $resourcesLinks; global $resourcesLinks;
$crawler = new Crawler($xmlData); $crawler = Import::xmlFromString($xmlData);
$nodes = $crawler->filter('*'); $nodes = $crawler->filter('*');
$currentQuestionIdent = ''; $currentQuestionIdent = '';
@@ -688,7 +687,8 @@ function isQtiManifest($filePath)
*/ */
function qtiProcessManifest($filePath) function qtiProcessManifest($filePath)
{ {
$xml = simplexml_load_file($filePath); libxml_use_internal_errors(true);
$xml = simplexml_load_file($filePath, SimpleXMLElement::class, LIBXML_NONET);
$course = api_get_course_info(); $course = api_get_course_info();
$sessionId = api_get_session_id(); $sessionId = api_get_session_id();
$courseDir = $course['path']; $courseDir = $course['path'];
+27 -15
View File
@@ -404,6 +404,8 @@ class FillBlanks extends Question
// remove starting and ending space and &nbsp; // remove starting and ending space and &nbsp;
$answer = api_preg_replace("/\xc2\xa0/", " ", $answer); $answer = api_preg_replace("/\xc2\xa0/", " ", $answer);
// remove invisible Unicode characters introduced by word processors
$answer = self::stripInvisibleChars($answer);
// start and end separator // start and end separator
$blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator')); $blankStartSeparator = self::getStartSeparator($form->getSubmitValue('select_separator'));
@@ -748,13 +750,13 @@ class FillBlanks extends Question
$listSeveral $listSeveral
); );
//$studentAnswer = htmlspecialchars($studentAnswer); //$studentAnswer = htmlspecialchars($studentAnswer);
$result = in_array($studentAnswer, $listSeveral); $result = in_array(self::trimOption($studentAnswer), $listSeveral);
break; break;
case self::FILL_THE_BLANK_STANDARD: case self::FILL_THE_BLANK_STANDARD:
default: default:
$correctAnswer = api_html_entity_decode($correctAnswer); $correctAnswer = api_html_entity_decode($correctAnswer);
//$studentAnswer = htmlspecialchars($studentAnswer); //$studentAnswer = htmlspecialchars($studentAnswer);
$result = $studentAnswer == self::trimOption($correctAnswer); $result = self::trimOption($studentAnswer) == self::trimOption($correctAnswer);
break; break;
} }
@@ -824,15 +826,14 @@ class FillBlanks extends Question
$listDetails = explode(':', $listArobaseSplit[0]); $listDetails = explode(':', $listArobaseSplit[0]);
// < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3 // < number of item after the ::[score]:[size]:[separator_id]@ , here there are 3
if (count($listDetails) < 3) {
$listWeightings = explode(',', $listDetails[0]); $listWeightings = explode(',', $listDetails[0]);
if (count($listDetails) < 3) {
$listSizeOfInput = []; $listSizeOfInput = [];
for ($i = 0; $i < count($listWeightings); $i++) { for ($i = 0; $i < count($listWeightings); $i++) {
$listSizeOfInput[] = 200; $listSizeOfInput[] = 200;
} }
$blankSeparatorNumber = 0; // 0 is [...] $blankSeparatorNumber = 0; // 0 is [...]
} else { } else {
$listWeightings = explode(',', $listDetails[0]);
$listSizeOfInput = explode(',', $listDetails[1]); $listSizeOfInput = explode(',', $listDetails[1]);
$blankSeparatorNumber = $listDetails[2]; $blankSeparatorNumber = $listDetails[2];
} }
@@ -1258,8 +1259,6 @@ class FillBlanks extends Question
* @param int $feedbackType * @param int $feedbackType
* @param bool $resultsDisabled * @param bool $resultsDisabled
* @param bool $showTotalScoreAndUserChoices * @param bool $showTotalScoreAndUserChoices
*
* @return string
*/ */
public static function getHtmlAnswer( public static function getHtmlAnswer(
$answer, $answer,
@@ -1269,7 +1268,7 @@ class FillBlanks extends Question
$resultsDisabled = false, $resultsDisabled = false,
$showTotalScoreAndUserChoices = false, $showTotalScoreAndUserChoices = false,
$exercise $exercise
) { ): string {
$hideExpectedAnswer = false; $hideExpectedAnswer = false;
$hideUserSelection = false; $hideUserSelection = false;
if (!$exercise->showExpectedChoiceColumn()) { if (!$exercise->showExpectedChoiceColumn()) {
@@ -1411,10 +1410,8 @@ class FillBlanks extends Question
* Check if a answer is correct by its text. * Check if a answer is correct by its text.
* *
* @param string $answerText * @param string $answerText
*
* @return bool
*/ */
public static function isCorrect($answerText) public static function isCorrect($answerText): bool
{ {
$answerInfo = self::getAnswerInfo($answerText, true); $answerInfo = self::getAnswerInfo($answerText, true);
$correctAnswerList = $answerInfo['words']; $correctAnswerList = $answerInfo['words'];
@@ -1433,10 +1430,8 @@ class FillBlanks extends Question
* Clear the answer entered by student. * Clear the answer entered by student.
* *
* @param string $answer * @param string $answer
*
* @return string
*/ */
public static function clearStudentAnswer($answer) public static function clearStudentAnswer($answer): string
{ {
$answer = htmlentities(api_utf8_encode($answer), ENT_QUOTES); $answer = htmlentities(api_utf8_encode($answer), ENT_QUOTES);
$answer = str_replace('&#039;', '&#39;', $answer); // fix apostrophe $answer = str_replace('&#039;', '&#39;', $answer); // fix apostrophe
@@ -1447,7 +1442,23 @@ class FillBlanks extends Question
} }
/** /**
* Removes double spaces between words. * Strips invisible/problematic Unicode characters introduced by word
* processors such as Microsoft Word.
* U+00A0 is normalised to a regular space; all others are removed.
*/
private static function stripInvisibleChars(string $text): string
{
$text = str_replace("\u{00A0}", ' ', $text); // Non-Breaking Space → space
return str_replace(
["\u{00AD}", "\u{200B}", "\u{200C}", "\u{200D}", "\u{2060}", "\u{FEFF}"],
'',
$text
);
}
/**
* Strips invisible characters and normalises whitespace.
* *
* @param string $text * @param string $text
* *
@@ -1456,7 +1467,8 @@ class FillBlanks extends Question
private static function trimOption($text) private static function trimOption($text)
{ {
$text = trim($text); $text = trim($text);
$text = self::stripInvisibleChars($text);
return preg_replace("/\s+/", ' ', $text); return preg_replace("/\s+/", ' ', trim($text));
} }
} }
@@ -213,12 +213,12 @@ class HotpotatoesExerciseResult
$filename = 'exercise_results_user_'.$user_id.'_'.api_get_local_time().'.xls'; $filename = 'exercise_results_user_'.$user_id.'_'.api_get_local_time().'.xls';
} }
$spreadsheet = new PHPExcel(); $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$spreadsheet->setActiveSheetIndex(0); $spreadsheet->setActiveSheetIndex(0);
$worksheet = $spreadsheet->getActiveSheet(); $worksheet = $spreadsheet->getActiveSheet();
$line = 0; $line = 1;
$column = 0; //skip the first column (row titles) $column = 1;
// check if exists column 'user' // check if exists column 'user'
$with_column_user = false; $with_column_user = false;
@@ -335,7 +335,7 @@ class HotpotatoesExerciseResult
$line++; $line++;
foreach ($this->results as $row) { foreach ($this->results as $row) {
$column = 0; $column = 1;
if ($with_column_user) { if ($with_column_user) {
$worksheet->setCellValueByColumnAndRow( $worksheet->setCellValueByColumnAndRow(
@@ -426,7 +426,7 @@ class HotpotatoesExerciseResult
} }
$file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename); $file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename);
$writer = new PHPExcel_Writer_Excel2007($spreadsheet); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save($file); $writer->save($file);
DocumentManager::file_send_for_download($file, true, $filename); DocumentManager::file_send_for_download($file, true, $filename);
+12 -6
View File
@@ -13,8 +13,12 @@ use Symfony\Component\HttpFoundation\Request;
* @author Toon Keppens * @author Toon Keppens
*/ */
$modifyAnswers = (int) $_GET['hotspotadmin']; $modifyAnswers = (int) $_GET['hotspotadmin'];
if (!is_object($objQuestion)) { if (!is_object($objQuestion) || empty($objQuestion->iid) || (int) $objQuestion->iid !== $modifyAnswers) {
$objQuestion = Question::read($modifyAnswers); $objQuestion = Question::read($modifyAnswers);
if (!$objQuestion) {
api_not_allowed();
}
Session::write('objQuestion', $objQuestion);
} }
$questionName = $objQuestion->selectTitle(); $questionName = $objQuestion->selectTitle();
@@ -330,14 +334,16 @@ if ($submitAnswers || $buttonBack) {
); );
$objAnswer->save(); $objAnswer->save();
// sets the total weighting of the question $objQuestion->iid = (int) $modifyAnswers;
$objQuestion->updateWeighting($questionWeighting); $objQuestion->course = api_get_course_info();
$objQuestion->updateWeighting((float) $questionWeighting);
$objQuestion->save($objExercise); $objQuestion->save($objExercise);
$editQuestion = $questionId; $editQuestion = $objQuestion->iid;
unset($modifyAnswers); unset($modifyAnswers);
echo '<script type="text/javascript">window.location.href="'.$hotspot_admin_url echo '<script type="text/javascript">window.location.href="'.
.'&message=ItemUpdated"</script>'; $hotspot_admin_url.'&message=ItemUpdated"</script>';
} }
} }
} }
+3
View File
@@ -337,6 +337,7 @@ if (!empty($attempts)) {
RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK, RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
RESULT_DISABLE_RANKING, RESULT_DISABLE_RANKING,
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER, RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
RESULT_DISABLE_RADAR,
] ]
)) { )) {
$row['result'] = $score; $row['result'] = $score;
@@ -353,6 +354,7 @@ if (!empty($attempts)) {
RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK, RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
RESULT_DISABLE_RANKING, RESULT_DISABLE_RANKING,
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER, RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
RESULT_DISABLE_RADAR,
] ]
) || ( ) || (
$objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ONLY && $objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ONLY &&
@@ -419,6 +421,7 @@ if (!empty($attempts)) {
case RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES: case RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES:
case RESULT_DISABLE_RANKING: case RESULT_DISABLE_RANKING:
case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER: case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER:
case RESULT_DISABLE_RADAR:
$header_names = [ $header_names = [
get_lang('Attempt'), get_lang('Attempt'),
get_lang('StartDate'), get_lang('StartDate'),
+48 -15
View File
@@ -16,6 +16,8 @@ $statusId = isset($_REQUEST['status']) ? (int) $_REQUEST['status'] : 0;
$questionTypeId = isset($_REQUEST['questionTypeId']) ? (int) $_REQUEST['questionTypeId'] : 0; $questionTypeId = isset($_REQUEST['questionTypeId']) ? (int) $_REQUEST['questionTypeId'] : 0;
$exportXls = isset($_REQUEST['export_xls']) && !empty($_REQUEST['export_xls']) ? (int) $_REQUEST['export_xls'] : 0; $exportXls = isset($_REQUEST['export_xls']) && !empty($_REQUEST['export_xls']) ? (int) $_REQUEST['export_xls'] : 0;
$action = $_REQUEST['a'] ?? null; $action = $_REQUEST['a'] ?? null;
$startDate = isset($_REQUEST['start_date']) ? $_REQUEST['start_date'] : '';
$endDate = isset($_REQUEST['end_date']) ? $_REQUEST['end_date'] : '';
api_block_anonymous_users(); api_block_anonymous_users();
@@ -170,6 +172,14 @@ $htmlHeadXtra[] = '<script>
} }
</script>'; </script>';
$htmlHeadXtra[] = '<script>
$(function() {
$(".datepicker").datepicker({
dateFormat: "yy-mm-dd"
});
});
</script>';
if ($exportXls) { if ($exportXls) {
ExerciseLib::exportPendingAttemptsToExcel($_REQUEST); ExerciseLib::exportPendingAttemptsToExcel($_REQUEST);
} }
@@ -286,6 +296,22 @@ $form->addSelect(
] ]
); );
$userOptions = [];
if (!empty($filter_user)) {
$userInfo = api_get_user_info($filter_user);
if (!empty($userInfo)) {
$userOptions[$filter_user] = $userInfo['complete_name_with_username'];
}
}
$form->addSelectAjax(
'filter_by_user',
get_lang('User'),
$userOptions,
[
'url' => api_get_path(WEB_AJAX_PATH).'user_manager.ajax.php?a=get_user_like',
]
);
$status = [ $status = [
1 => get_lang('All'), 1 => get_lang('All'),
2 => get_lang('Validated'), 2 => get_lang('Validated'),
@@ -293,16 +319,27 @@ $status = [
4 => get_lang('Unclosed'), 4 => get_lang('Unclosed'),
5 => get_lang('Ongoing'), 5 => get_lang('Ongoing'),
]; ];
$form->addSelect('status', get_lang('Status'), $status); $form->addSelect('status', get_lang('Status'), $status);
$questionType = [ $questionType = [
0 => get_lang('All'), 0 => get_lang('All'),
1 => get_lang('QuestionsWithNoAutomaticCorrection'), 1 => get_lang('QuestionsWithNoAutomaticCorrection'),
]; ];
$form->addSelect('questionTypeId', get_lang('QuestionType'), $questionType); $form->addSelect('questionTypeId', get_lang('QuestionType'), $questionType);
$form->addElement(
'text',
'start_date',
get_lang('StartDate'),
['id' => 'start_date', 'class' => 'datepicker', 'autocomplete' => 'off', 'style' => 'width:120px']
);
$form->addElement(
'text',
'end_date',
get_lang('EndDate'),
['id' => 'end_date', 'class' => 'datepicker', 'autocomplete' => 'off', 'style' => 'width:120px']
);
$form->addButtonSearch(get_lang('Search'), 'pendingSubmit'); $form->addButtonSearch(get_lang('Search'), 'pendingSubmit');
$content = $form->returnForm(); $content = $form->returnForm();
@@ -315,7 +352,9 @@ if (empty($statusId)) {
$url = api_get_path(WEB_AJAX_PATH). $url = api_get_path(WEB_AJAX_PATH).
'model.ajax.php?a=get_exercise_pending_results&filter_by_user='.$filter_user. 'model.ajax.php?a=get_exercise_pending_results&filter_by_user='.$filter_user.
'&course_id='.$courseId.'&exercise_id='.$exerciseId.'&status='.$statusId.'&questionType='.$questionTypeId.'&showAttemptsInSessions='.$showAttemptsInSessions; '&course_id='.$courseId.'&exercise_id='.$exerciseId.'&status='.$statusId.'&questionType='.$questionTypeId.
'&showAttemptsInSessions='.$showAttemptsInSessions.
'&start_date='.$startDate.'&end_date='.$endDate;
$action_links = ''; $action_links = '';
$officialCodeInList = api_get_setting('show_official_code_exercise_result_list'); $officialCodeInList = api_get_setting('show_official_code_exercise_result_list');
@@ -375,16 +414,6 @@ $column_model = [
'align' => 'left', 'align' => 'left',
'search' => 'false', 'search' => 'false',
'sortable' => 'false', 'sortable' => 'false',
//'stype' => 'select',
//for the bottom bar
/*'searchoptions' => [
'defaultValue' => '',
'value' => ':'.get_lang('All').';1:'.get_lang('Validated').';0:'.get_lang('NotValidated'),
],*/
//for the top bar
/*'editoptions' => [
'value' => ':'.get_lang('All').';1:'.get_lang('Validated').';0:'.get_lang('NotValidated'),
],*/
], ],
[ [
'name' => 'qualificator_fullname', 'name' => 'qualificator_fullname',
@@ -432,8 +461,12 @@ function action_formatter(cellvalue, options, rowObject) {
return "<span title=\""+tabLoginx[0]+rowObject[2]+tabLoginx[1]+"\">"+cellvalue+"</span>"; return "<span title=\""+tabLoginx[0]+rowObject[2]+tabLoginx[1]+"\">"+cellvalue+"</span>";
}'; }';
$extra_params['autowidth'] = 'true'; $extra_params = [
$extra_params['height'] = 'auto'; 'autowidth' => 'true',
'height' => 'auto',
'sortname' => 'exe_date',
'sortorder' => 'asc',
];
$gridJs = Display::grid_js( $gridJs = Display::grid_js(
'results', 'results',
$url, $url,
+3
View File
@@ -66,6 +66,9 @@ if ($student_id === $current_user_id && ExerciseSignaturePlugin::exerciseHasSign
} }
} }
if (RESULT_DISABLE_RADAR === (int) $objExercise->results_disabled) {
$htmlHeadXtra[] = api_get_js('chartjs/Chart.min.js');
}
$htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/css/hotspot.css">'; $htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/css/hotspot.css">';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>'; $htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'annotation/js/annotation.js"></script>'; $htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'annotation/js/annotation.js"></script>';
+10 -3
View File
@@ -16,15 +16,20 @@ $_user = api_get_user_info();
$this_section = SECTION_COURSES; $this_section = SECTION_COURSES;
$documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path']."/document"; $documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path']."/document";
$test = $_REQUEST['test']; $test = $_REQUEST['test'] ?? '';
$full_file_path = $documentPath.$test; $full_file_path = $documentPath.$test;
$fileToDelete = $full_file_path.$_user['user_id'].".t.html";
my_delete($full_file_path.$_user['user_id'].".t.html"); if (!Security::check_abs_path($fileToDelete, $documentPath.'/')) {
api_not_allowed(true);
}
my_delete($fileToDelete);
$TABLETRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES); $TABLETRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
$TABLE_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW); $TABLE_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
$score = $_REQUEST['score']; $score = isset($_REQUEST['score']) ? Security::remove_XSS($_REQUEST['score']) : '';
$origin = api_get_origin(); $origin = api_get_origin();
$learnpath_item_id = intval($_REQUEST['learnpath_item_id']); $learnpath_item_id = intval($_REQUEST['learnpath_item_id']);
$lpViewId = isset($_REQUEST['lp_view_id']) ? intval($_REQUEST['lp_view_id']) : null; $lpViewId = isset($_REQUEST['lp_view_id']) ? intval($_REQUEST['lp_view_id']) : null;
@@ -45,6 +50,8 @@ function save_scores($file, $score)
global $origin; global $origin;
$TABLETRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES); $TABLETRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
$_user = api_get_user_info(); $_user = api_get_user_info();
$file = Security::remove_XSS($file);
$score = intval($score);
// if tracking is disabled record nothing // if tracking is disabled record nothing
$weighting = 100; // 100% $weighting = 100; // 100%
$date = api_get_utc_datetime(); $date = api_get_utc_datetime();
+8 -1
View File
@@ -28,7 +28,14 @@ $lpViewId = isset($_REQUEST['lp_view_id']) ? $_REQUEST['lp_view_id'] : null;
$user_id = api_get_user_id(); $user_id = api_get_user_id();
$full_file_path = $document_path.$doc_url; $full_file_path = $document_path.$doc_url;
my_delete($full_file_path.$user_id.'.t.html');
// Security: reject path traversal attempts (CWE-22)
if (!Security::check_abs_path($full_file_path, $document_path.'/')) {
api_not_allowed(true);
}
$fileToDelete = $full_file_path.$user_id.'.t.html';
my_delete($fileToDelete);
$content = ReadFileCont($full_file_path.$user_id.'.t.html'); $content = ReadFileCont($full_file_path.$user_id.'.t.html');
if ($content == '') { if ($content == '') {
+9 -9
View File
@@ -160,7 +160,7 @@ function lp_upload_quiz_action_handling()
$questionTypeList = []; $questionTypeList = [];
$answerList = []; $answerList = [];
$quizTitle = ''; $quizTitle = '';
$objPHPExcel = PHPExcel_IOFactory::load($_FILES['user_upload_quiz']['tmp_name']); $objPHPExcel = \PhpOffice\PhpSpreadsheet\IOFactory::load($_FILES['user_upload_quiz']['tmp_name']);
$objPHPExcel->setActiveSheetIndex(0); $objPHPExcel->setActiveSheetIndex(0);
$worksheet = $objPHPExcel->getActiveSheet(); $worksheet = $objPHPExcel->getActiveSheet();
$highestRow = $worksheet->getHighestRow(); // e.g. 10 $highestRow = $worksheet->getHighestRow(); // e.g. 10
@@ -171,9 +171,9 @@ function lp_upload_quiz_action_handling()
$useCustomScore = isset($_POST['user_custom_score']) ? true : false; $useCustomScore = isset($_POST['user_custom_score']) ? true : false;
for ($row = 1; $row <= $highestRow; $row++) { for ($row = 1; $row <= $highestRow; $row++) {
$cellTitleInfo = $worksheet->getCellByColumnAndRow(0, $row); $cellTitleInfo = $worksheet->getCellByColumnAndRow(1, $row);
$cellDataInfo = $worksheet->getCellByColumnAndRow(1, $row); $cellDataInfo = $worksheet->getCellByColumnAndRow(2, $row);
$cellScoreInfo = $worksheet->getCellByColumnAndRow(2, $row); $cellScoreInfo = $worksheet->getCellByColumnAndRow(3, $row);
$title = $cellTitleInfo->getValue(); $title = $cellTitleInfo->getValue();
switch ($title) { switch ($title) {
@@ -188,9 +188,9 @@ function lp_upload_quiz_action_handling()
$answerIndex = 0; $answerIndex = 0;
while ($continue) { while ($continue) {
$answerRow++; $answerRow++;
$answerInfoTitle = $worksheet->getCellByColumnAndRow(0, $answerRow); $answerInfoTitle = $worksheet->getCellByColumnAndRow(1, $answerRow);
$answerInfoData = $worksheet->getCellByColumnAndRow(1, $answerRow); $answerInfoData = $worksheet->getCellByColumnAndRow(2, $answerRow);
$answerInfoExtra = $worksheet->getCellByColumnAndRow(2, $answerRow); $answerInfoExtra = $worksheet->getCellByColumnAndRow(3, $answerRow);
$answerInfoTitle = $answerInfoTitle->getValue(); $answerInfoTitle = $answerInfoTitle->getValue();
if (strpos($answerInfoTitle, 'Answer') !== false) { if (strpos($answerInfoTitle, 'Answer') !== false) {
$answerList[$numberQuestions][$answerIndex]['data'] = $answerInfoData->getValue(); $answerList[$numberQuestions][$answerIndex]['data'] = $answerInfoData->getValue();
@@ -212,8 +212,8 @@ function lp_upload_quiz_action_handling()
$questionTypeIndex = 0; $questionTypeIndex = 0;
while ($continue) { while ($continue) {
$answerRow++; $answerRow++;
$questionTypeTitle = $worksheet->getCellByColumnAndRow(0, $answerRow); $questionTypeTitle = $worksheet->getCellByColumnAndRow(1, $answerRow);
$questionTypeExtra = $worksheet->getCellByColumnAndRow(2, $answerRow); $questionTypeExtra = $worksheet->getCellByColumnAndRow(3, $answerRow);
$title = $questionTypeTitle->getValue(); $title = $questionTypeTitle->getValue();
if ($title === 'QuestionType') { if ($title === 'QuestionType') {
$questionTypeList[$numberQuestions] = $questionTypeExtra->getValue(); $questionTypeList[$numberQuestions] = $questionTypeExtra->getValue();
+99 -84
View File
@@ -44,7 +44,9 @@ function show_image(image,width,height) {
$export = isset($_GET['export']) ? $_GET['export'] : false; $export = isset($_GET['export']) ? $_GET['export'] : false;
$sessionId = isset($_GET['id_session']) ? intval($_GET['id_session']) : 0; $sessionId = isset($_GET['id_session']) ? intval($_GET['id_session']) : 0;
$origin = isset($_GET['origin']) ? Security::remove_XSS($_GET['origin']) : ''; // Use the canonical helper, which URL-encodes "origin" so it cannot break out
// of the href attribute (XSS) when later reflected into links.
$origin = api_get_origin();
$studentId = (int) $_GET['student']; $studentId = (int) $_GET['student'];
$coachId = isset($_GET['id_coach']) ? (int) $_GET['id_coach'] : 0; $coachId = isset($_GET['id_coach']) ? (int) $_GET['id_coach'] : 0;
@@ -326,35 +328,48 @@ $token = Security::get_token();
if (!empty($studentId)) { if (!empty($studentId)) {
// Actions bar // Actions bar
echo '<div class="actions">'; echo '<div class="actions">';
echo '<a href="javascript: window.history.go(-1);">'. echo Display::url(
Display::return_icon('back.png', get_lang('Back'), '', ICON_SIZE_MEDIUM).'</a>'; Display::return_icon('back.png', get_lang('Back'), '', ICON_SIZE_MEDIUM),
'javascript: window.history.go(-1);'
);
echo '<a href="javascript: void(0);" onclick="javascript: window.print();">'. echo Display::url(
Display::return_icon('printer.png', get_lang('Print'), '', ICON_SIZE_MEDIUM).'</a>'; Display::return_icon('printer.png', get_lang('Print'), '', ICON_SIZE_MEDIUM),
'javascript: void(0);',
['onclick' => 'javascript: window.print();']
);
echo '<a href="'.api_get_self().'?'.Security::remove_XSS($_SERVER['QUERY_STRING']).'&export=csv">'. echo Display::url(
Display::return_icon('export_csv.png', get_lang('ExportAsCSV'), '', ICON_SIZE_MEDIUM).'</a> '; Display::return_icon('export_csv.png', get_lang('ExportAsCSV'), '', ICON_SIZE_MEDIUM),
api_get_self().'?'.Security::remove_XSS($_SERVER['QUERY_STRING']).'&export=csv'
).' ';
echo '<a href="'.api_get_self().'?'.Security::remove_XSS($_SERVER['QUERY_STRING']).'&export=xls">'. echo Display::url(
Display::return_icon('export_excel.png', get_lang('ExportAsXLS'), '', ICON_SIZE_MEDIUM).'</a> '; Display::return_icon('export_excel.png', get_lang('ExportAsXLS'), '', ICON_SIZE_MEDIUM),
api_get_self().'?'.Security::remove_XSS($_SERVER['QUERY_STRING']).'&export=xls'
).' ';
if (!empty($user_info['email'])) { if (!empty($user_info['email'])) {
$send_mail = '<a href="mailto:'.$user_info['email'].'">'. $send_mail = Display::url(
Display::return_icon('mail_send.png', get_lang('SendMail'), '', ICON_SIZE_MEDIUM).'</a>'; Display::return_icon('mail_send.png', get_lang('SendMail'), '', ICON_SIZE_MEDIUM),
'mailto:'.$user_info['email']
);
} else { } else {
$send_mail = Display::return_icon('mail_send_na.png', get_lang('SendMail'), '', ICON_SIZE_MEDIUM); $send_mail = Display::return_icon('mail_send_na.png', get_lang('SendMail'), '', ICON_SIZE_MEDIUM);
} }
echo $send_mail; echo $send_mail;
if (!empty($studentId) && !empty($course_code)) { if (!empty($studentId) && !empty($course_code)) {
// Only show link to connection details if course and student were defined in the URL // Only show link to connection details if course and student were defined in the URL
echo '<a href="access_details.php?student='.$studentId.'&course='.$course_code.'&origin='.$origin.'&cidReq='.$course_code.'&id_session='.$sessionId.'">'. echo Display::url(
Display::return_icon('statistics.png', get_lang('AccessDetails'), '', ICON_SIZE_MEDIUM).'</a>'; Display::return_icon('statistics.png', get_lang('AccessDetails'), '', ICON_SIZE_MEDIUM),
'access_details.php?student='.$studentId.'&course='.$course_code.'&origin='.$origin.'&cidReq='.$course_code.'&id_session='.$sessionId
);
} }
if (api_can_login_as($studentId)) { if (api_can_login_as($studentId)) {
echo '<a href="'.api_get_path( echo Display::url(
WEB_CODE_PATH Display::return_icon('login_as.png', get_lang('LoginAs'), null, ICON_SIZE_MEDIUM),
).'admin/user_list.php?action=login_as&user_id='.$studentId.'&sec_token='.$token.'">'. api_get_path(WEB_CODE_PATH).'admin/user_list.php?action=login_as&user_id='.$studentId.'&sec_token='.$token
Display::return_icon('login_as.png', get_lang('LoginAs'), null, ICON_SIZE_MEDIUM).'</a>&nbsp;&nbsp;'; ).'&nbsp;&nbsp;';
} }
echo Display::url( echo Display::url(
@@ -478,7 +493,7 @@ if (!empty($studentId)) {
<tr> <tr>
<td><?php echo get_lang('Email').' : '; <td><?php echo get_lang('Email').' : ';
if (!empty($user_info['email'])) { if (!empty($user_info['email'])) {
echo '<a href="mailto:'.$user_info['email'].'">'.$user_info['email'].'</a>'; echo Display::url($user_info['email'], 'mailto:'.$user_info['email']);
} else { } else {
echo get_lang('NoEmail'); echo get_lang('NoEmail');
} ?> } ?>
@@ -770,8 +785,10 @@ if (!empty($studentId)) {
]; ];
echo '<tr> echo '<tr>
<td ><a href="'.$courseInfoItem['course_public_url'].'?id_session='.$sId.'">'. <td >'.Display::url(
$courseInfoItem['title'].'</a></td> $courseInfoItem['title'],
$courseInfoItem['course_public_url'].'?id_session='.$sId
).'</td>
<td >'.$time_spent_on_course.'</td> <td >'.$time_spent_on_course.'</td>
<td >'.$progress.'</td> <td >'.$progress.'</td>
<td >'.$score.'</td> <td >'.$score.'</td>
@@ -779,14 +796,15 @@ if (!empty($studentId)) {
<td >'.$scoretotal_display.'</td>'; <td >'.$scoretotal_display.'</td>';
if (!empty($coachId)) { if (!empty($coachId)) {
echo '<td width="10"><a href="'.api_get_self(). echo '<td width="10">'.Display::url(
'?student='.$user_info['user_id'].'&details=true&course='.$courseInfoItem['code'].'&id_coach='.$coachId. Display::return_icon('2rightarrow.png', get_lang('Details')),
'&origin='.$origin.'&id_session='.$sId.'#infosStudent"> api_get_self().'?student='.$user_info['user_id'].'&details=true&course='.$courseInfoItem['code'].'&id_coach='.$coachId.'&origin='.$origin.'&id_session='.$sId.'#infosStudent'
'.Display::return_icon('2rightarrow.png', get_lang('Details')).'</a></td>'; ).'</td>';
} else { } else {
echo '<td width="10"><a href="'.api_get_self( echo '<td width="10">'.Display::url(
).'?student='.$user_info['user_id'].'&details=true&course='.$courseInfoItem['code'].'&origin='.$origin.'&id_session='.$sId.'#infosStudent"> Display::return_icon('2rightarrow.png', get_lang('Details')),
'.Display::return_icon('2rightarrow.png', get_lang('Details')).'</a></td>'; api_get_self().'?student='.$user_info['user_id'].'&details=true&course='.$courseInfoItem['code'].'&origin='.$origin.'&id_session='.$sId.'#infosStudent'
).'</td>';
} }
echo '</tr>'; echo '</tr>';
} }
@@ -1033,21 +1051,22 @@ if (!empty($studentId)) {
if (api_is_allowed_to_edit()) { if (api_is_allowed_to_edit()) {
echo '<td>'; echo '<td>';
if ($any_result === true) { if ($any_result === true) {
echo '<a href="myStudents.php?action=reset_lp&sec_token='.$token. echo Display::url(
Display::return_icon(
'clean.png',
get_lang('Clean'),
'',
ICON_SIZE_SMALL
),
'myStudents.php?action=reset_lp&sec_token='.$token.
'&cidReq='.$course_code. '&cidReq='.$course_code.
'&course='.$course_code. '&course='.$course_code.
'&details='.Security::remove_XSS($_GET['details']). '&details='.Security::remove_XSS($_GET['details']).
'&origin='.$origin. '&origin='.$origin.
'&lp_id='.$learnpath->getId(). '&lp_id='.$learnpath->getId().
'&student='.$user_info['user_id']. '&student='.$user_info['user_id'].
'&details=true&id_session='.$sessionId.'">'; '&details=true&id_session='.$sessionId
echo Display::return_icon( );
'clean.png',
get_lang('Clean'),
'',
ICON_SIZE_SMALL
).'</a>';
echo '</a>';
} }
echo '</td>'; echo '</td>';
echo '</tr>'; echo '</tr>';
@@ -1186,14 +1205,14 @@ if (!empty($studentId)) {
if (Database::num_rows($result_last_attempt) > 0) { if (Database::num_rows($result_last_attempt) > 0) {
$id_last_attempt = Database::result($result_last_attempt, 0, 0); $id_last_attempt = Database::result($result_last_attempt, 0, 0);
if ($count_attempts > 0) { if ($count_attempts > 0) {
echo '<a href="../exercise/exercise_show.php?id='.$id_last_attempt. echo Display::url(
Display::return_icon('quiz.gif'),
'../exercise/exercise_show.php?id='.$id_last_attempt.
'&cidReq='.$course_code. '&cidReq='.$course_code.
'&session_id='.$sessionId. '&session_id='.$sessionId.
'&student='.$studentId. '&student='.$studentId.
'&origin='.(empty($origin) ? 'tracking' : $origin). '&origin='.(empty($origin) ? 'tracking' : $origin)
'">'; );
echo Display::return_icon('quiz.gif');
echo '</a>';
} }
} }
echo '</td>'; echo '</td>';
@@ -1690,14 +1709,17 @@ if ($end_date < '2010-01-01') {
"; ";
$exe_id = $a_inter['exe_id']; ?> $exe_id = $a_inter['exe_id']; ?>
<td class="highlight"> <td class="highlight">
<a href="edit_intervention.php?num=<?php echo $exe_id; ?>&student_id=<?php echo $studentId; ?> "> <?php
<?php echo Display::return_icon('edit.png', get_lang('Edit')); ?></a> echo Display::url(
&nbsp;&nbsp; <a Display::return_icon('edit.png', get_lang('Edit')),
href="delete_exam.php?num=<?php echo $exe_id; ?>&student_id=<?php echo $studentId; ?>"> 'edit_intervention.php?num='.$exe_id.'&student_id='.$studentId
<img );
src="../img/delete.gif" border="0" echo '&nbsp;&nbsp;';
onClick='return confirmDelete2()'> echo Display::url(
</a> Display::img('../img/delete.gif', get_lang('Delete'), ['border' => '0', 'onclick' => 'return confirmDelete2()']),
'delete_exam.php?num='.$exe_id.'&student_id='.$studentId
);
?>
<?php <?php
} }
?> ?>
@@ -1925,16 +1947,15 @@ if (empty($_GET['details'])) {
<td >'.$progress.'</td> <td >'.$progress.'</td>
<td >'.$end_date_module.'</td>'; <td >'.$end_date_module.'</td>';
if (!empty($coachId)) { if (!empty($coachId)) {
echo '<td width="10"> echo '<td width="10">'.Display::url(
<a href="'.api_get_self().'?student='.$user_info['user_id'].'&details=true&course='.$course_info['code'].'&id_coach='.$coachId. Display::return_icon('2rightarrow.png', get_lang('Details')),
'&origin='.Security::remove_XSS($_GET['origin']).'&id_session='.$session_id.'#infosStudent">'. api_get_self().'?student='.$user_info['user_id'].'&details=true&course='.$course_info['code'].'&id_coach='.$coachId.'&origin='.$origin.'&id_session='.$session_id.'#infosStudent'
Display::return_icon('2rightarrow.png', get_lang('Details')).'</a></td>'; ).'</td>';
} else { } else {
echo '<td width="10"><a href="'.api_get_self( echo '<td width="10">'.Display::url(
).'?student='.$user_info['user_id'].'&details=true&course='.$course_info['code'].'&origin='.Security::remove_XSS( Display::return_icon('2rightarrow.png', get_lang('Details')),
$_GET['origin'] api_get_self().'?student='.$user_info['user_id'].'&details=true&course='.$course_info['code'].'&origin='.$origin.'&id_session='.$session_id.'#infosStudent'
).'&id_session='.$session_id.'#infosStudent">'. ).'</td>';
Display::return_icon('2rightarrow.png', get_lang('Details')).'</a></td>';
} }
echo '</tr>'; echo '</tr>';
} }
@@ -2153,11 +2174,7 @@ if (empty($_GET['details'])) {
} }
$link = Display::url( $link = Display::url(
Display::return_icon('2rightarrow.png', get_lang('Details')), Display::return_icon('2rightarrow.png', get_lang('Details')),
'lp_tracking.php?cidReq='.Security::remove_XSS($_GET['course']).'&course='.Security::remove_XSS( 'lp_tracking.php?cidReq='.$course_code.'&course='.$course_code.$from.'&origin='.$origin.'&lp_id='.$learnpath['id'].'&student_id='.$user_info['user_id'].'&id_session='.$session_id
$_GET['course']
).$from.'&origin='.Security::remove_XSS(
$_GET['origin']
).'&lp_id='.$learnpath['id'].'&student_id='.$user_info['user_id'].'&id_session='.$session_id
); );
echo Display::tag('td', $link); echo Display::tag('td', $link);
} }
@@ -2165,17 +2182,10 @@ if (empty($_GET['details'])) {
if (api_is_allowed_to_edit()) { if (api_is_allowed_to_edit()) {
echo '<td>'; echo '<td>';
if ($any_result === true) { if ($any_result === true) {
echo '<a href="myStudents.php?action=reset_lp&sec_token='.$token.'&cidReq='.Security::remove_XSS( echo Display::url(
$_GET['course'] Display::return_icon('clean.png', get_lang('Clean'), '', ICON_SIZE_SMALL),
).'&course='.Security::remove_XSS($_GET['course']).'&details='.Security::remove_XSS( 'myStudents.php?action=reset_lp&sec_token='.$token.'&cidReq='.$course_code.'&course='.$course_code.'&details='.Security::remove_XSS($_GET['details']).'&origin='.$origin.'&lp_id='.$learnpath['id'].'&student='.$user_info['user_id'].'&details=true&id_session='.$session_id
$_GET['details'] );
).'&origin='.Security::remove_XSS(
$_GET['origin']
).'&lp_id='.$learnpath['id'].'&student='.$user_info['user_id'].'&details=true&id_session='.Security::remove_XSS(
$_GET['id_session']
).'">';
echo Display::return_icon('clean.png', get_lang('Clean'), '', ICON_SIZE_SMALL).'</a>';
echo '</a>';
} }
echo '</td>'; echo '</td>';
echo '</tr>'; echo '</tr>';
@@ -2297,9 +2307,10 @@ if (empty($_GET['details'])) {
if (Database::num_rows($result_last_attempt) > 0) { if (Database::num_rows($result_last_attempt) > 0) {
$id_last_attempt = Database::result($result_last_attempt, 0, 0); $id_last_attempt = Database::result($result_last_attempt, 0, 0);
if ($count_attempts > 0) { if ($count_attempts > 0) {
echo '<a href="../exercice/exercise_show.php?id='.$id_last_attempt.'&cidReq='.$course_code.'&session_id='.$session_id.'&student='.$studentId.'&origin='.(empty($_GET['origin']) ? 'tracking' : Security::remove_XSS( echo Display::url(
$_GET['origin'] Display::return_icon('quiz.gif'),
)).'"> <img src="'.api_get_path(WEB_IMG_PATH).'quiz.gif" border="0" /> </a>'; '../exercice/exercise_show.php?id='.$id_last_attempt.'&cidReq='.$course_code.'&session_id='.$session_id.'&student='.$studentId.'&origin='.(empty($origin) ? 'tracking' : $origin)
);
} }
} }
echo '</td>'; echo '</td>';
@@ -2572,13 +2583,17 @@ if ($export_csv) {
$exe_idd = $a_exam['exe_id']; ?> $exe_idd = $a_exam['exe_id']; ?>
<INPUT type=hidden name=ex_idd value= <?php echo "$exe_idd"; ?>> <INPUT type=hidden name=ex_idd value= <?php echo "$exe_idd"; ?>>
<td class="highlight">&nbsp; <td class="highlight">&nbsp;
<a href="edit_exam.php?num=<?php echo $exe_idd; ?>&student_id=<?php echo $studentId; ?>"> <?php
<?php echo Display::return_icon('edit.png', get_lang('Edit')); ?> echo Display::url(
</a> Display::return_icon('edit.png', get_lang('Edit')),
&nbsp;&nbsp;<a href="delete_exam.php?num=<?php echo $exe_idd; ?>&student_id=<?php echo $studentId; ?>"> 'edit_exam.php?num='.$exe_idd.'&student_id='.$studentId
<img );
src="../img/delete.gif" border="0" echo '&nbsp;&nbsp;';
onClick='return confirmDelete2()'></a> echo Display::url(
Display::img('../img/delete.gif', get_lang('Delete'), ['border' => '0', 'onclick' => 'return confirmDelete2()']),
'delete_exam.php?num='.$exe_idd.'&student_id='.$studentId
);
?>
</td> </td>
</tr> </tr>
<?php <?php
+1
View File
@@ -67,6 +67,7 @@ if ($languageId) {
$form->addHidden('sub_language_id', $languageInfo['id']); $form->addHidden('sub_language_id', $languageInfo['id']);
$form->addHidden('redirect', true); $form->addHidden('redirect', true);
$form->addHidden('extra_field_type', $extraField->getExtraFieldType()); $form->addHidden('extra_field_type', $extraField->getExtraFieldType());
$form->addHidden('sec_token', Security::get_token());
$form->addButtonSave(get_lang('Save')); $form->addButtonSave(get_lang('Save'));
} }
@@ -16,7 +16,7 @@ if (PHP_SAPI !== 'cli') {
$courseCode = $argv[1]; $courseCode = $argv[1];
$sessionId = $argv[2]; $sessionId = $argv[2];
$categoryId = $argv[3]; $categoryId = $argv[3];
$userList = isset($argv[4]) ? explode(',', $argv[4]) : []; $userList = !empty($argv[4]) ? explode(',', $argv[4]) : [];
$date = api_get_utc_datetime(null, false, true); $date = api_get_utc_datetime(null, false, true);
+11
View File
@@ -9,6 +9,17 @@ api_protect_course_script(true);
api_block_anonymous_users(); api_block_anonymous_users();
GradebookUtils::block_students(); GradebookUtils::block_students();
if (isset($_GET['import'])) {
$queryString = $_SERVER['QUERY_STRING'];
$webPath = api_get_path(WEB_CODE_PATH);
$newUrl = $webPath.'gradebook/gradebook_view_result.php';
if (!empty($queryString)) {
$newUrl .= '?'.$queryString;
}
header("Location: $newUrl");
exit;
}
$selectEval = isset($_GET['selecteval']) ? (int) $_GET['selecteval'] : 0; $selectEval = isset($_GET['selecteval']) ? (int) $_GET['selecteval'] : 0;
$resultadd = new Result(); $resultadd = new Result();
@@ -179,6 +179,7 @@ switch ($action) {
$learner['username'], $learner['username'],
$learner['firstname'], $learner['firstname'],
$learner['lastname'], $learner['lastname'],
$learner['email'],
]; ];
}, $certificate_list); }, $certificate_list);
@@ -188,9 +189,11 @@ switch ($action) {
$csvHeaders[] = get_lang('UserName'); $csvHeaders[] = get_lang('UserName');
$csvHeaders[] = get_lang('FirstName'); $csvHeaders[] = get_lang('FirstName');
$csvHeaders[] = get_lang('LastName'); $csvHeaders[] = get_lang('LastName');
$csvHeaders[] = get_lang('Email');
$csvHeaders[] = get_lang('Score'); $csvHeaders[] = get_lang('Score');
$csvHeaders[] = get_lang('Date'); $csvHeaders[] = get_lang('Date');
$usersProfileInfo = [];
$extraFields = []; $extraFields = [];
$extraFieldsFromSettings = []; $extraFieldsFromSettings = [];
$extraFieldsFromSettings = api_get_configuration_value('certificate_export_report_user_extra_fields'); $extraFieldsFromSettings = api_get_configuration_value('certificate_export_report_user_extra_fields');
@@ -213,7 +216,7 @@ switch ($action) {
); );
} }
} }
}
foreach ($exportData as $key => $row) { foreach ($exportData as $key => $row) {
$list = GradebookUtils::get_list_gradebook_certificates_by_user_id( $list = GradebookUtils::get_list_gradebook_certificates_by_user_id(
$row[0], $row[0],
@@ -231,7 +234,6 @@ switch ($action) {
$csvContent[] = $row; $csvContent[] = $row;
} }
}
array_unshift($csvContent, $csvHeaders); array_unshift($csvContent, $csvHeaders);
+19 -2
View File
@@ -10,8 +10,19 @@ api_block_anonymous_users();
GradebookUtils::block_students(); GradebookUtils::block_students();
$evaledit = Evaluation::load($_GET['editeval']); $evaledit = Evaluation::load($_GET['editeval']);
if ($evaledit[0]->is_locked() && !api_is_platform_admin()) { if (empty($evaledit[0])) {
api_not_allowed(); api_not_allowed(true);
}
if (!api_is_platform_admin()) {
$currentCourseCode = api_get_course_id();
if ($evaledit[0]->get_course_code() && $evaledit[0]->get_course_code() != $currentCourseCode) {
api_not_allowed(true);
}
if ($evaledit[0]->is_locked()) {
api_not_allowed(true);
}
} }
$form = new EvalForm( $form = new EvalForm(
EvalForm::TYPE_EDIT, EvalForm::TYPE_EDIT,
@@ -23,6 +34,12 @@ $form = new EvalForm(
); );
if ($form->validate()) { if ($form->validate()) {
$values = $form->exportValues(); $values = $form->exportValues();
$evaluationId = (int) $values['hid_id'];
if ($evaluationId !== (int) $evaledit[0]->get_id()) {
api_not_allowed(true);
}
$eval = new Evaluation(); $eval = new Evaluation();
$eval->set_id($values['hid_id']); $eval->set_id($values['hid_id']);
$eval->set_name($values['name']); $eval->set_name($values['name']);
+11 -5
View File
@@ -10,12 +10,18 @@ $current_course_tool = TOOL_GRADEBOOK;
api_protect_course_script(true); api_protect_course_script(true);
api_block_anonymous_users(); api_block_anonymous_users();
$currentUserId = api_get_user_id();
$sessionId = api_get_session_id();
$isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh( $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh(
api_get_user_id(), $currentUserId,
api_get_course_info() api_get_course_info()
); );
if (!$isDrhOfCourse) { $isDrhOfSession = $sessionId && !empty(SessionManager::getSessionFollowedByDrh($currentUserId, $sessionId));
if (!$isDrhOfCourse && !$isDrhOfSession) {
GradebookUtils::block_students(); GradebookUtils::block_students();
} }
@@ -75,7 +81,7 @@ $simple_search_form = new UserForm(
$values = $simple_search_form->exportValues(); $values = $simple_search_form->exportValues();
$keyword = ''; $keyword = '';
if (isset($_GET['search']) && !empty($_GET['search'])) { if (!empty($_GET['search'])) {
$keyword = Security::remove_XSS($_GET['search']); $keyword = Security::remove_XSS($_GET['search']);
} }
if ($simple_search_form->validate() && empty($keyword)) { if ($simple_search_form->validate() && empty($keyword)) {
@@ -90,7 +96,7 @@ if (!empty($keyword)) {
$users = GradebookUtils::get_all_users($alleval, $alllinks); $users = GradebookUtils::get_all_users($alleval, $alllinks);
} }
} }
$offset = isset($_GET['offset']) ? $_GET['offset'] : '0'; $offset = $_GET['offset'] ?? '0';
$addparams = ['selectcat' => $cat[0]->get_id()]; $addparams = ['selectcat' => $cat[0]->get_id()];
if (isset($_GET['search'])) { if (isset($_GET['search'])) {
@@ -128,7 +134,7 @@ if (isset($_GET['export_pdf']) && 'category' === $_GET['export_pdf']) {
$params['join_firstname_lastname'] = true; $params['join_firstname_lastname'] = true;
$params['show_official_code'] = true; $params['show_official_code'] = true;
$params['export_pdf'] = true; $params['export_pdf'] = true;
if ($cat[0]->is_locked() == true || api_is_platform_admin()) { if ($cat[0]->is_locked() || api_is_platform_admin()) {
Display::set_header(null, false, false); Display::set_header(null, false, false);
GradebookUtils::export_pdf_flatview( GradebookUtils::export_pdf_flatview(
$flatViewTable, $flatViewTable,
+11 -3
View File
@@ -40,7 +40,7 @@ if ($eval[0]->get_category_id() < 0) {
//load the result with the evaluation id //load the result with the evaluation id
if (isset($_GET['delete_mark'])) { if (isset($_GET['delete_mark'])) {
$result = Result::load($_GET['delete_mark']); $result = Result::load($_GET['delete_mark']);
if (!empty($result[0])) { if (!empty($result[0]) && $result[0]->get_evaluation_id() == $select_eval) {
$result[0]->delete(); $result[0]->delete();
} }
} }
@@ -56,7 +56,9 @@ if (isset($_GET['action'])) {
switch ($_GET['action']) { switch ($_GET['action']) {
case 'delete_attempt': case 'delete_attempt':
$result = Result::load($_GET['editres']); $result = Result::load($_GET['editres']);
if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()) { if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()
&& $result[0]->get_evaluation_id() == $select_eval
) {
/** @var Result $result */ /** @var Result $result */
$result = $result[0]; $result = $result[0];
$url = api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq().'&editres='.$result->get_id(); $url = api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq().'&editres='.$result->get_id();
@@ -74,7 +76,9 @@ if (isset($_GET['action'])) {
break; break;
case 'add_attempt': case 'add_attempt':
$result = Result::load($_GET['editres']); $result = Result::load($_GET['editres']);
if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()) { if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()
&& $result[0]->get_evaluation_id() == $select_eval
) {
/** @var Result $result */ /** @var Result $result */
$result = $result[0]; $result = $result[0];
$backUrl = api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq(); $backUrl = api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq();
@@ -512,8 +516,10 @@ if (isset($_GET['export'])) {
if (isset($_GET['resultdelete'])) { if (isset($_GET['resultdelete'])) {
$result = Result::load($_GET['resultdelete']); $result = Result::load($_GET['resultdelete']);
if (!empty($result[0]) && $result[0]->get_evaluation_id() == $select_eval) {
$result[0]->delete(); $result[0]->delete();
Display::addFlash(Display::return_message(get_lang('ResultDeleted'))); Display::addFlash(Display::return_message(get_lang('ResultDeleted')));
}
header('Location: gradebook_view_result.php?selecteval='.$select_eval.'&'.api_get_cidreq()); header('Location: gradebook_view_result.php?selecteval='.$select_eval.'&'.api_get_cidreq());
exit; exit;
} }
@@ -534,9 +540,11 @@ if (isset($_POST['action'])) {
$number_of_deleted_results = 0; $number_of_deleted_results = 0;
foreach ($_POST['id'] as $indexstr) { foreach ($_POST['id'] as $indexstr) {
$result = Result::load($indexstr); $result = Result::load($indexstr);
if (!empty($result[0]) && $result[0]->get_evaluation_id() == $select_eval) {
$result[0]->delete(); $result[0]->delete();
$number_of_deleted_results++; $number_of_deleted_results++;
} }
}
Display::addFlash(Display::return_message(get_lang('ResultsDeleted'), 'confirmation', false)); Display::addFlash(Display::return_message(get_lang('ResultsDeleted'), 'confirmation', false));
header('Location: gradebook_view_result.php?massdelete=&selecteval='.$select_eval.'&'.api_get_cidreq()); header('Location: gradebook_view_result.php?massdelete=&selecteval='.$select_eval.'&'.api_get_cidreq());
exit; exit;
+28
View File
@@ -56,11 +56,19 @@ switch ($action) {
exit; exit;
break; break;
case 'lock': case 'lock':
if (!Security::check_token('get')) {
api_not_allowed(true);
}
Security::clear_token();
$category_to_lock = Category::load($_GET['category_id']); $category_to_lock = Category::load($_GET['category_id']);
$category_to_lock[0]->lockAllItems(1); $category_to_lock[0]->lockAllItems(1);
$confirmation_message = get_lang('GradebookLockedAlert'); $confirmation_message = get_lang('GradebookLockedAlert');
break; break;
case 'unlock': case 'unlock':
if (!Security::check_token('get')) {
api_not_allowed(true);
}
Security::clear_token();
if (api_is_platform_admin()) { if (api_is_platform_admin()) {
$category_to_lock = Category::load($_GET['category_id']); $category_to_lock = Category::load($_GET['category_id']);
$category_to_lock[0]->lockAllItems(0); $category_to_lock[0]->lockAllItems(0);
@@ -360,6 +368,10 @@ if (isset($_GET['visiblecat'])) {
} }
if (isset($_GET['deletecat'])) { if (isset($_GET['deletecat'])) {
if (!Security::check_token('get')) {
api_not_allowed(true);
}
Security::clear_token();
GradebookUtils::block_students(); GradebookUtils::block_students();
$cats = Category::load($_GET['deletecat']); $cats = Category::load($_GET['deletecat']);
if (isset($cats[0])) { if (isset($cats[0])) {
@@ -397,6 +409,10 @@ if (isset($_GET['visibleeval'])) {
// Parameters for evaluations. // Parameters for evaluations.
if (isset($_GET['lockedeval'])) { if (isset($_GET['lockedeval'])) {
if (!Security::check_token('get')) {
api_not_allowed(true);
}
Security::clear_token();
GradebookUtils::block_students(); GradebookUtils::block_students();
$locked = (int) $_GET['lockedeval']; $locked = (int) $_GET['lockedeval'];
$type_locked = 1; $type_locked = 1;
@@ -414,6 +430,10 @@ if (isset($_GET['lockedeval'])) {
} }
if (isset($_GET['deleteeval'])) { if (isset($_GET['deleteeval'])) {
if (!Security::check_token('get')) {
api_not_allowed(true);
}
Security::clear_token();
GradebookUtils::block_students(); GradebookUtils::block_students();
$eval = Evaluation::load($_GET['deleteeval']); $eval = Evaluation::load($_GET['deleteeval']);
if ($eval[0] != null) { if ($eval[0] != null) {
@@ -446,6 +466,10 @@ if (isset($_GET['visiblelink'])) {
} }
if (isset($_GET['deletelink'])) { if (isset($_GET['deletelink'])) {
if (!Security::check_token('get')) {
api_not_allowed(true);
}
Security::clear_token();
GradebookUtils::block_students(); GradebookUtils::block_students();
$get_delete_link = (int) $_GET['deletelink']; $get_delete_link = (int) $_GET['deletelink'];
//fixing #5229 //fixing #5229
@@ -495,6 +519,10 @@ if (!empty($course_to_crsind) && !isset($_GET['confirm'])) {
// Actions on the sortabletable. // Actions on the sortabletable.
if (isset($_POST['action'])) { if (isset($_POST['action'])) {
if (!Security::check_token('post')) {
api_not_allowed(true);
}
Security::clear_token();
GradebookUtils::block_students(); GradebookUtils::block_students();
$number_of_selected_items = count($_POST['id']); $number_of_selected_items = count($_POST['id']);
+9 -7
View File
@@ -263,11 +263,13 @@ class GradebookUtils
); );
if (api_is_allowed_to_edit(null, true)) { if (api_is_allowed_to_edit(null, true)) {
// Shared CSRF token for the destructive GET links below.
$token = Security::get_existing_token();
// Locking button // Locking button
if (api_get_setting('gradebook_locking_enabled') === 'true') { if (api_get_setting('gradebook_locking_enabled') === 'true') {
if ($cat->is_locked()) { if ($cat->is_locked()) {
if (api_is_platform_admin()) { if (api_is_platform_admin()) {
$modify_icons .= '&nbsp;<a onclick="javascript:if (!confirm(\''.addslashes(get_lang('ConfirmToUnlockElement')).'\')) return false;" href="'.api_get_self().'?'.api_get_cidreq().'&category_id='.$cat->get_id().'&action=unlock">'. $modify_icons .= '&nbsp;<a onclick="javascript:if (!confirm(\''.addslashes(get_lang('ConfirmToUnlockElement')).'\')) return false;" href="'.api_get_self().'?'.api_get_cidreq().'&category_id='.$cat->get_id().'&action=unlock&sec_token='.$token.'">'.
Display::return_icon('lock.png', get_lang('UnLockEvaluation'), '', ICON_SIZE_SMALL).'</a>'; Display::return_icon('lock.png', get_lang('UnLockEvaluation'), '', ICON_SIZE_SMALL).'</a>';
} else { } else {
$modify_icons .= '&nbsp;<a href="#">'. $modify_icons .= '&nbsp;<a href="#">'.
@@ -275,7 +277,7 @@ class GradebookUtils
} }
$modify_icons .= '&nbsp;<a href="gradebook_flatview.php?export_pdf=category&selectcat='.$cat->get_id().'" >'.Display::return_icon('pdf.png', get_lang('ExportToPDF'), '', ICON_SIZE_SMALL).'</a>'; $modify_icons .= '&nbsp;<a href="gradebook_flatview.php?export_pdf=category&selectcat='.$cat->get_id().'" >'.Display::return_icon('pdf.png', get_lang('ExportToPDF'), '', ICON_SIZE_SMALL).'</a>';
} else { } else {
$modify_icons .= '&nbsp;<a onclick="javascript:if (!confirm(\''.addslashes(get_lang('ConfirmToLockElement')).'\')) return false;" href="'.api_get_self().'?'.api_get_cidreq().'&category_id='.$cat->get_id().'&action=lock">'. $modify_icons .= '&nbsp;<a onclick="javascript:if (!confirm(\''.addslashes(get_lang('ConfirmToLockElement')).'\')) return false;" href="'.api_get_self().'?'.api_get_cidreq().'&category_id='.$cat->get_id().'&action=lock&sec_token='.$token.'">'.
Display::return_icon('unlock.png', get_lang('LockEvaluation'), '', ICON_SIZE_SMALL).'</a>'; Display::return_icon('unlock.png', get_lang('LockEvaluation'), '', ICON_SIZE_SMALL).'</a>';
$modify_icons .= '&nbsp;<a href="#" >'. $modify_icons .= '&nbsp;<a href="#" >'.
Display::return_icon('pdf_na.png', get_lang('ExportToPDF'), '', ICON_SIZE_SMALL).'</a>'; Display::return_icon('pdf_na.png', get_lang('ExportToPDF'), '', ICON_SIZE_SMALL).'</a>';
@@ -332,7 +334,7 @@ class GradebookUtils
ICON_SIZE_SMALL ICON_SIZE_SMALL
); );
} else { } else {
$modify_icons .= '&nbsp;<a href="'.api_get_self().'?deletecat='.$cat->get_id().'&selectcat='.$selectcat.'&'.$courseParams.'" onclick="return confirmation();">'. $modify_icons .= '&nbsp;<a href="'.api_get_self().'?deletecat='.$cat->get_id().'&selectcat='.$selectcat.'&sec_token='.$token.'&'.$courseParams.'" onclick="return confirmation();">'.
Display::return_icon( Display::return_icon(
'delete.png', 'delete.png',
get_lang('DeleteAll'), get_lang('DeleteAll'),
@@ -421,7 +423,7 @@ class GradebookUtils
ICON_SIZE_SMALL ICON_SIZE_SMALL
); );
} else { } else {
$modify_icons .= '&nbsp;<a href="'.api_get_self().'?deleteeval='.$eval->get_id().'&selectcat='.$selectcat.' &'.$courseParams.'" onclick="return confirmation();">'. $modify_icons .= '&nbsp;<a href="'.api_get_self().'?deleteeval='.$eval->get_id().'&selectcat='.$selectcat.'&sec_token='.Security::get_existing_token().'&'.$courseParams.'" onclick="return confirmation();">'.
Display::return_icon( Display::return_icon(
'delete.png', 'delete.png',
get_lang('Delete'), get_lang('Delete'),
@@ -518,7 +520,7 @@ class GradebookUtils
} else { } else {
$modify_icons .= '&nbsp; $modify_icons .= '&nbsp;
<a <a
href="'.api_get_self().'?deletelink='.$link->get_id().'&selectcat='.$selectcat.' &'.$courseParams.'" href="'.api_get_self().'?deletelink='.$link->get_id().'&selectcat='.$selectcat.'&sec_token='.Security::get_existing_token().'&'.$courseParams.'"
onclick="return confirmation();">'. onclick="return confirmation();">'.
Display::return_icon( Display::return_icon(
'delete.png', 'delete.png',
@@ -743,7 +745,7 @@ class GradebookUtils
{ {
$table_certificate = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE); $table_certificate = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
$table_user = Database::get_main_table(TABLE_MAIN_USER); $table_user = Database::get_main_table(TABLE_MAIN_USER);
$sql = 'SELECT DISTINCT u.user_id, u.lastname, u.firstname, u.username, gc.created_at $sql = 'SELECT DISTINCT u.user_id, u.lastname, u.firstname, u.username, u.email, gc.created_at
FROM '.$table_user.' u FROM '.$table_user.' u
INNER JOIN '.$table_certificate.' gc INNER JOIN '.$table_certificate.' gc
ON u.user_id=gc.user_id '; ON u.user_id=gc.user_id ';
@@ -974,7 +976,7 @@ class GradebookUtils
/** /**
* @param FlatViewTable $flatviewtable * @param FlatViewTable $flatviewtable
* @param Category $cat * @param array<int, Category> $cat
* @param $users * @param $users
* @param $alleval * @param $alleval
* @param $alllinks * @param $alllinks
+10 -5
View File
@@ -633,18 +633,23 @@ abstract class AbstractLink implements GradebookItem
*/ */
public static function getCurrentUserRanking($userId, $studentList) public static function getCurrentUserRanking($userId, $studentList)
{ {
$previousScore = null;
$ranking = null; $ranking = null;
$position = null;
$currentUserId = $userId; $currentUserId = $userId;
if (!empty($studentList) && !empty($currentUserId)) { if (!empty($studentList) && !empty($currentUserId)) {
$studentList = array_map('floatval', $studentList); $studentList = array_map('floatval', $studentList);
asort($studentList); arsort($studentList);
$ranking = $count = count($studentList); $count = count($studentList);
foreach ($studentList as $userId => $score) {
foreach ($studentList as $userId => $position) { $position++;
if ($previousScore === null || $score < $previousScore) {
$ranking = $position;
}
$previousScore = $score;
if ($currentUserId == $userId) { if ($currentUserId == $userId) {
break; break;
} }
$ranking--;
} }
// If no ranking was detected. // If no ranking was detected.
+7 -13
View File
@@ -128,12 +128,9 @@ class Category implements GradebookItem
return $this->weight; return $this->weight;
} }
/** public function is_locked(): bool
* @return bool
*/
public function is_locked()
{ {
return isset($this->locked) && $this->locked == 1 ? true : false; return isset($this->locked) && $this->locked == 1;
} }
/** /**
@@ -429,7 +426,7 @@ class Category implements GradebookItem
* @param bool $order_by Whether to show all "session" * @param bool $order_by Whether to show all "session"
* categories (true) or hide them (false) in case there is no session id * categories (true) or hide them (false) in case there is no session id
* *
* @return array * @return array<int, Category>
*/ */
public static function load( public static function load(
$id = null, $id = null,
@@ -439,7 +436,7 @@ class Category implements GradebookItem
$visible = null, $visible = null,
$session_id = null, $session_id = null,
$order_by = null $order_by = null
) { ): array {
//if the category given is explicitly 0 (not null), then create //if the category given is explicitly 0 (not null), then create
// a root category object (in memory) // a root category object (in memory)
if (isset($id) && (int) $id === 0) { if (isset($id) && (int) $id === 0) {
@@ -2681,10 +2678,7 @@ class Category implements GradebookItem
return $this->weight - $subWeight; return $this->weight - $subWeight;
} }
/** private static function create_root_category(): Category
* @return Category
*/
private static function create_root_category()
{ {
$cat = new Category(); $cat = new Category();
$cat->set_id(0); $cat->set_id(0);
@@ -2704,9 +2698,9 @@ class Category implements GradebookItem
/** /**
* @param Doctrine\DBAL\Driver\Statement|null $result * @param Doctrine\DBAL\Driver\Statement|null $result
* *
* @return array * @return array<int, Category>
*/ */
private static function create_category_objects_from_sql_result($result) private static function create_category_objects_from_sql_result($result): array
{ {
$categories = []; $categories = [];
$allow = api_get_configuration_value('allow_gradebook_stats'); $allow = api_get_configuration_value('allow_gradebook_stats');
+4 -4
View File
@@ -221,7 +221,7 @@ class Evaluation implements GradebookItem
* @param int $category_id parent category * @param int $category_id parent category
* @param int $visible visible * @param int $visible visible
* *
* @return array * @return array<int, Evaluation>
*/ */
public static function load( public static function load(
$id = null, $id = null,
@@ -230,7 +230,7 @@ class Evaluation implements GradebookItem
$category_id = null, $category_id = null,
$visible = null, $visible = null,
$locked = null $locked = null
) { ): array {
$table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_EVALUATION); $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_EVALUATION);
$sql = 'SELECT * FROM '.$table; $sql = 'SELECT * FROM '.$table;
$paramcount = 0; $paramcount = 0;
@@ -935,9 +935,9 @@ class Evaluation implements GradebookItem
/** /**
* @param array $result * @param array $result
* *
* @return array * @return array<int, Evaluation>
*/ */
private static function create_evaluation_objects_from_sql_result($result) private static function create_evaluation_objects_from_sql_result($result): array
{ {
$alleval = []; $alleval = [];
$allow = api_get_configuration_value('allow_gradebook_stats'); $allow = api_get_configuration_value('allow_gradebook_stats');
@@ -281,8 +281,8 @@ class ForumThreadLink extends AbstractLink
//it was extracts the forum id //it was extracts the forum id
$sql = 'SELECT * FROM '.$this->get_forum_thread_table()." $sql = 'SELECT * FROM '.$this->get_forum_thread_table()."
WHERE WHERE
c_id = '.$this->course_id.' AND c_id = ".$this->course_id." AND
thread_id = '".$this->get_ref_id()."' AND thread_id = ".$this->get_ref_id()." AND
session_id = $sessionId "; session_id = $sessionId ";
$result = Database::query($sql); $result = Database::query($sql);
$row = Database::fetch_array($result, 'ASSOC'); $row = Database::fetch_array($result, 'ASSOC');
+2 -2
View File
@@ -85,9 +85,9 @@ class Result
* @param $user_id user id (student) * @param $user_id user id (student)
* @param $evaluation_id evaluation where this is a result for * @param $evaluation_id evaluation where this is a result for
* *
* @return array * @return array<int, Result>
*/ */
public static function load($id = null, $user_id = null, $evaluation_id = null) public static function load($id = null, $user_id = null, $evaluation_id = null): array
{ {
$tbl_user = Database::get_main_table(TABLE_MAIN_USER); $tbl_user = Database::get_main_table(TABLE_MAIN_USER);
$tbl_grade_results = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT); $tbl_grade_results = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT);
+1
View File
@@ -53,6 +53,7 @@ class CatForm extends FormValidator
break; break;
} }
$this->protect();
$this->setDefaults(); $this->setDefaults();
} }
+5
View File
@@ -34,6 +34,11 @@ class DataForm extends FormValidator
$this->form_type = $form_type; $this->form_type = $form_type;
if ($this->form_type == self::TYPE_IMPORT) { if ($this->form_type == self::TYPE_IMPORT) {
$this->build_import_form(); $this->build_import_form();
// Only the import is state-changing (overwrites scores). The export
// branches call validate() twice on the same request, which is
// incompatible with protect() (validate() clears the token), so they
// are intentionally left without CSRF protection here.
$this->protect();
} elseif ($this->form_type == self::TYPE_EXPORT) { } elseif ($this->form_type == self::TYPE_EXPORT) {
if ($locked_status == 0) { if ($locked_status == 0) {
$this->build_export_form_option(false); $this->build_export_form_option(false);
+15 -4
View File
@@ -429,6 +429,7 @@ class DisplayGradebook
} }
// for course admin & platform admin add item buttons are added to the header // for course admin & platform admin add item buttons are added to the header
$toolbarActions = [];
$actionsLeft = ''; $actionsLeft = '';
$actionsRight = ''; $actionsRight = '';
$my_api_cidreq = api_get_cidreq(); $my_api_cidreq = api_get_cidreq();
@@ -562,11 +563,13 @@ class DisplayGradebook
} }
$isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh( $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh(
api_get_user_id(), $userId,
api_get_course_info() api_get_course_info()
); );
if ($isDrhOfCourse) { $isDrhOfSession = $sessionId && !empty(SessionManager::getSessionFollowedByDrh($userId, $sessionId));
if ($isDrhOfCourse || $isDrhOfSession) {
$actionsLeft .= '<a href="gradebook_flatview.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">' $actionsLeft .= '<a href="gradebook_flatview.php?'.$my_api_cidreq.'&selectcat='.$catobj->get_id().'">'
.Display::return_icon( .Display::return_icon(
'statistics.png', 'statistics.png',
@@ -578,9 +581,17 @@ class DisplayGradebook
} }
if ($isCoach || api_is_allowed_to_edit(null, true)) { if ($isCoach || api_is_allowed_to_edit(null, true)) {
echo $toolbar = Display::toolbarAction( $toolbarActions = [$actionsLeft, $actionsRight];
}
if (empty($toolbarActions) && ($isDrhOfCourse || $isDrhOfSession)) {
$toolbarActions = [$actionsLeft];
}
if ($toolbarActions) {
echo Display::toolbarAction(
'gradebook-actions', 'gradebook-actions',
[$actionsLeft, $actionsRight] $toolbarActions
); );
} }
@@ -205,6 +205,7 @@ class LinkAddEditForm extends FormValidator
$defaults['visible'] = $visibility_default; $defaults['visible'] = $visibility_default;
} }
$this->protect();
// set default values // set default values
$this->setDefaults($defaults); $this->setDefaults($defaults);
} }
+1
View File
@@ -55,6 +55,7 @@ class LinkForm extends FormValidator
} elseif (self::TYPE_MOVE == $form_type) { } elseif (self::TYPE_MOVE == $form_type) {
$this->build_move(); $this->build_move();
} }
$this->protect();
} }
protected function build_move() protected function build_move()
@@ -151,6 +151,7 @@ class ScoreDisplayForm extends FormValidator
if ($displayscore->is_custom()) { if ($displayscore->is_custom()) {
$this->addButtonSave(get_lang('Ok')); $this->addButtonSave(get_lang('Ok'));
} }
$this->protect();
} }
public function validate() public function validate()
+8 -11
View File
@@ -21,11 +21,11 @@ class GradeBookResult
/** /**
* Exports the complete report as a CSV file. * Exports the complete report as a CSV file.
* *
* @param string $dato Document path inside the document tool * @param array $dato Document path inside the document tool
* *
* @return bool False on error * @return bool False on error
*/ */
public function exportCompleteReportCSV($dato) public function exportCompleteReportCSV(array $dato)
{ {
$filename = 'gradebook_results_'.gmdate('YmdGis').'.csv'; $filename = 'gradebook_results_'.gmdate('YmdGis').'.csv';
$data = ''; $data = '';
@@ -80,19 +80,16 @@ class GradeBookResult
* Exports the complete report as an XLS file. * Exports the complete report as an XLS file.
* *
* @param array $data * @param array $data
*
* @throws PHPExcel_Exception
* @throws PHPExcel_Writer_Exception
*/ */
public function exportCompleteReportXLS($data) public function exportCompleteReportXLS($data)
{ {
$filename = 'gradebook-results-'.api_get_local_time().'.xlsx'; $filename = 'gradebook-results-'.api_get_local_time().'.xlsx';
$spreadsheet = new PHPExcel(); $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$spreadsheet->setActiveSheetIndex(0); $spreadsheet->setActiveSheetIndex(0);
$worksheet = $spreadsheet->getActiveSheet(); $worksheet = $spreadsheet->getActiveSheet();
$line = 1; $line = 1;
$column = 0; $column = 1;
// headers. // headers.
foreach ($data[0] as $headerData) { foreach ($data[0] as $headerData) {
$title = $headerData; $title = $headerData;
@@ -100,7 +97,7 @@ class GradeBookResult
$title = $headerData['header']; $title = $headerData['header'];
} }
$title = html_entity_decode(strip_tags($title)); $title = html_entity_decode(strip_tags($title));
$worksheet->SetCellValueByColumnAndRow( $worksheet->setCellValueByColumnAndRow(
$column, $column,
$line, $line,
$title $title
@@ -110,9 +107,9 @@ class GradeBookResult
$line++; $line++;
$cant_students = count($data[1]); $cant_students = count($data[1]);
for ($i = 0; $i < $cant_students; $i++) { for ($i = 0; $i < $cant_students; $i++) {
$column = 0; $column = 1;
foreach ($data[1][$i] as $col_name) { foreach ($data[1][$i] as $col_name) {
$worksheet->SetCellValueByColumnAndRow( $worksheet->setCellValueByColumnAndRow(
$column, $column,
$line, $line,
html_entity_decode(strip_tags($col_name)) html_entity_decode(strip_tags($col_name))
@@ -123,7 +120,7 @@ class GradeBookResult
} }
$file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename); $file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename);
$writer = new PHPExcel_Writer_Excel2007($spreadsheet); $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save($file); $writer->save($file);
DocumentManager::file_send_for_download($file, true, $filename); DocumentManager::file_send_for_download($file, true, $filename);
exit; exit;
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+3 -36
View File
@@ -145,43 +145,10 @@ switch ($action) {
break; break;
case 'search_course': case 'search_course':
if (api_is_teacher() || api_is_platform_admin()) { if (api_is_teacher() || api_is_platform_admin()) {
if (isset($_GET['session_id']) && !empty($_GET['session_id'])) { $courseList = CourseManager::searchCourse(
//if session is defined, lets find only courses of this session $_REQUEST['q'],
$courseList = SessionManager::get_course_list_by_session_id( isset($_GET['session_id']) ? (int) $_GET['session_id'] : 0
$_GET['session_id'],
$_GET['q']
); );
} else {
//if session is not defined lets search all courses STARTING with $_GET['q']
//TODO change this function to search not only courses STARTING with $_GET['q']
if (api_is_platform_admin()) {
$courseList = CourseManager::get_courses_list(
0,
0,
'title',
'ASC',
-1,
$_GET['q'],
null,
true
);
} elseif (api_is_teacher()) {
$courseList = CourseManager::get_course_list_of_user_as_course_admin(api_get_user_id(), $_GET['q']);
$category = api_get_configuration_value('course_category_code_to_use_as_model');
if (!empty($category)) {
$alreadyAdded = [];
if (!empty($courseList)) {
$alreadyAdded = array_column($courseList, 'id');
}
$coursesInCategory = CourseCategory::getCoursesInCategory($category, $_GET['q']);
foreach ($coursesInCategory as $course) {
if (!in_array($course['id'], $alreadyAdded)) {
$courseList[] = $course;
}
}
}
}
}
$results = []; $results = [];
if (empty($courseList)) { if (empty($courseList)) {
+12 -9
View File
@@ -1,6 +1,7 @@
<?php <?php
/* For licensing terms, see /license.txt */ /* For licensing terms, see /license.txt */
use Chamilo\CourseBundle\Entity\CCourseDescription;
use Chamilo\CourseBundle\Entity\CTool; use Chamilo\CourseBundle\Entity\CTool;
use ChamiloSession as Session; use ChamiloSession as Session;
@@ -290,15 +291,17 @@ switch ($action) {
echo get_lang('PrivateAccess'); echo get_lang('PrivateAccess');
break; break;
} }
$table = Database::get_course_table(TABLE_COURSE_DESCRIPTION);
$sql = "SELECT * FROM $table /** @var array<int, CCourseDescription> $courseDescriptions */
WHERE c_id = ".$course_info['real_id']." AND session_id = 0 $courseDescriptions = Database::getManager()
ORDER BY id"; ->getRepository(CCourseDescription::class)
$result = Database::query($sql); ->findBy(['cId' => $course_info['real_id'], 'sessionId' => 0])
if (Database::num_rows($result) > 0) { ;
while ($description = Database::fetch_object($result)) {
$descriptions[$description->id] = $description; $descriptions = [];
}
foreach ($courseDescriptions as $courseDescription) {
$descriptions[$courseDescription->getIid()] = $courseDescription;
// Function that displays the details of the course description in html. // Function that displays the details of the course description in html.
$content = CourseManager::get_details_course_description_html( $content = CourseManager::get_details_course_description_html(
$descriptions, $descriptions,
+32 -1
View File
@@ -6,6 +6,7 @@
*/ */
use Chamilo\CoreBundle\Component\Editor\Driver\Driver; use Chamilo\CoreBundle\Component\Editor\Driver\Driver;
use Chamilo\CoreBundle\Component\Editor\Driver\PersonalDriver;
require_once __DIR__.'/../global.inc.php'; require_once __DIR__.'/../global.inc.php';
@@ -217,6 +218,20 @@ switch ($action) {
$data = []; $data = [];
$fileUpload = $_FILES['upload']; $fileUpload = $_FILES['upload'];
try {
new Image($fileUpload['tmp_name']);
} catch (Exception $e) {
echo json_encode([
'uploaded' => 0,
'error' => [
'message' => get_lang('MissingImagesDetected'),
],
]);
exit;
}
$mimeType = mime_content_type($fileUpload['tmp_name']); $mimeType = mime_content_type($fileUpload['tmp_name']);
$isMimeAccepted = (new Driver())->mimeAccepted($mimeType, ['image']); $isMimeAccepted = (new Driver())->mimeAccepted($mimeType, ['image']);
@@ -225,6 +240,22 @@ switch ($action) {
exit; exit;
} }
try {
$fileUpload['size'] = DocumentManager::autoResizeImageIfNeeded(
$fileUpload['size'],
$fileUpload['tmp_name']
);
} catch (Exception $e) {
echo json_encode([
'uploaded' => 0,
'error' => [
'message' => $e->getMessage(),
],
]);
exit;
}
$isAllowedToEdit = api_is_allowed_to_edit(null, true); $isAllowedToEdit = api_is_allowed_to_edit(null, true);
if ($isAllowedToEdit) { if ($isAllowedToEdit) {
$globalFile = ['files' => $fileUpload]; $globalFile = ['files' => $fileUpload];
@@ -257,7 +288,7 @@ switch ($action) {
mkdir($syspath, api_get_permissions_for_new_directories(), true); mkdir($syspath, api_get_permissions_for_new_directories(), true);
} }
$webpath = UserManager::getUserPathById($userId, 'web').'my_files'; $webpath = UserManager::getUserPathById($userId, 'web').'my_files';
$fileUploadName = $fileUpload['name']; $fileUploadName = disable_dangerous_file(api_replace_dangerous_char($fileUpload['name']));
if (file_exists($syspath.$fileUploadName)) { if (file_exists($syspath.$fileUploadName)) {
$extension = pathinfo($fileUploadName, PATHINFO_EXTENSION); $extension = pathinfo($fileUploadName, PATHINFO_EXTENSION);
$fileName = pathinfo($fileUploadName, PATHINFO_FILENAME); $fileName = pathinfo($fileUploadName, PATHINFO_FILENAME);
+17 -1
View File
@@ -56,6 +56,10 @@ switch ($action) {
} }
break;*/ break;*/
case 'export_all_certificates': case 'export_all_certificates':
if (!api_is_allowed_to_edit() && !api_is_student_boss()) {
exit;
}
$categoryId = (int) $_GET['cat_id']; $categoryId = (int) $_GET['cat_id'];
$filterOfficialCodeGet = isset($_GET['filter']) ? Security::remove_XSS($_GET['filter']) : null; $filterOfficialCodeGet = isset($_GET['filter']) ? Security::remove_XSS($_GET['filter']) : null;
@@ -76,7 +80,19 @@ switch ($action) {
$userList = implode(',', $userList); $userList = implode(',', $userList);
shell_exec("php $commandScript $courseCode $sessionId $categoryId $userList > /dev/null &"); $command = sprintf(
"php %s %s %s %s",
escapeshellarg($commandScript),
escapeshellarg($courseCode),
escapeshellarg((string) $sessionId),
escapeshellarg((string) $categoryId)
);
if ('' !== $userList) {
$command .= ' '.escapeshellarg($userList);
}
shell_exec($command.' > /dev/null &');
break; break;
case 'verify_export_all_certificates': case 'verify_export_all_certificates':
$categoryId = (int) $_GET['cat_id']; $categoryId = (int) $_GET['cat_id'];
+16
View File
@@ -17,6 +17,11 @@ switch ($action) {
echo api_get_language_translate_html(); echo api_get_language_translate_html();
break; break;
case 'translate_portfolio_category': case 'translate_portfolio_category':
// Only platform admins may write to language files
if (!api_is_platform_admin()) {
api_not_allowed(true);
exit;
}
if (false === Security::check_token('get')) { if (false === Security::check_token('get')) {
exit; exit;
} }
@@ -31,6 +36,11 @@ switch ($action) {
$languageId = (int) $_REQUEST['id']; $languageId = (int) $_REQUEST['id'];
$subLanguageId = (int) $_REQUEST['sub']; $subLanguageId = (int) $_REQUEST['sub'];
// Validate variable name is a safe PHP identifier
if (!SubLanguageManager::isValidLanguageVariable($langVariable)) {
exit;
}
$langFilesToLoad = SubLanguageManager::get_lang_folder_files_list( $langFilesToLoad = SubLanguageManager::get_lang_folder_files_list(
api_get_path(SYS_LANG_PATH).'english', api_get_path(SYS_LANG_PATH).'english',
true true
@@ -39,6 +49,12 @@ switch ($action) {
$fileLanguage = $langFilesToLoad[0].'.inc.php'; $fileLanguage = $langFilesToLoad[0].'.inc.php';
$allDataOfLanguage = SubLanguageManager::get_all_information_of_sub_language($languageId, $subLanguageId); $allDataOfLanguage = SubLanguageManager::get_all_information_of_sub_language($languageId, $subLanguageId);
if (empty($allDataOfLanguage) ||
!SubLanguageManager::isValidLanguageFolderName($allDataOfLanguage['dokeos_folder'])
) {
exit;
}
$pathFolder = api_get_path(SYS_LANG_PATH).$allDataOfLanguage['dokeos_folder'].'/'.$fileLanguage; $pathFolder = api_get_path(SYS_LANG_PATH).$allDataOfLanguage['dokeos_folder'].'/'.$fileLanguage;
$allFileOfDirectory = SubLanguageManager::get_all_language_variable_in_file($pathFolder); $allFileOfDirectory = SubLanguageManager::get_all_language_variable_in_file($pathFolder);
$returnValue = SubLanguageManager::add_file_in_language_directory($pathFolder); $returnValue = SubLanguageManager::add_file_in_language_directory($pathFolder);
+27 -11
View File
@@ -166,6 +166,11 @@ if (($search || $forceSearch) && ($search !== 'false')) {
$filters = json_decode($_REQUEST['filters2']); $filters = json_decode($_REQUEST['filters2']);
} }
if (!empty($filters) && isset($filters->groupOp)) {
$op = strtoupper((string) $filters->groupOp);
$filters->groupOp = in_array($op, ['AND', 'OR'], true) ? $op : 'AND';
}
if (!empty($filters)) { if (!empty($filters)) {
if (in_array($action, if (in_array($action,
[ [
@@ -251,10 +256,6 @@ if (($search || $forceSearch) && ($search !== 'false')) {
} }
$whereCondition .= $extraQuestionCondition; $whereCondition .= $extraQuestionCondition;
if (isset($filters->custom_dates)) {
$whereCondition .= $filters->custom_dates;
}
} }
} elseif (!empty($filters->rules)) { } elseif (!empty($filters->rules)) {
$whereCondition .= ' AND ( '; $whereCondition .= ' AND ( ';
@@ -569,10 +570,15 @@ switch ($action) {
break; break;
case 'get_work_pending_list': case 'get_work_pending_list':
require_once api_get_path(SYS_CODE_PATH).'work/work.lib.php'; require_once api_get_path(SYS_CODE_PATH).'work/work.lib.php';
$courseId = $_REQUEST['course'] ?? 0; $courseId = (int) ($_REQUEST['course'] ?? 0);
$status = $_REQUEST['status'] ?? 0; $status = $_REQUEST['status'] ?? 0;
if (isset($_REQUEST['work_parent_ids'])) { if (isset($_REQUEST['work_parent_ids'])) {
$whereCondition = ' parent_id IN('.Security::remove_XSS($_REQUEST['work_parent_ids']).')'; $workParentIds = array_filter(
array_map('intval', explode(',', (string) $_REQUEST['work_parent_ids']))
);
if (!empty($workParentIds)) {
$whereCondition = ' parent_id IN('.implode(',', $workParentIds).')';
}
} }
$count = getAllWork( $count = getAllWork(
null, null,
@@ -643,17 +649,19 @@ switch ($action) {
true true
); );
break; break;
case 'get_exercise_pending_results': case 'get_exercise_pending_results':
if ((false === api_is_teacher()) && (false === api_is_session_admin())) { if ((false === api_is_teacher()) && (false === api_is_session_admin())) {
exit; exit;
} }
$search_start_date = isset($_REQUEST['start_date']) && !empty($_REQUEST['start_date']) ? $_REQUEST['start_date'] : null;
$courseId = $_REQUEST['course_id'] ?? 0; $search_end_date = isset($_REQUEST['end_date']) && !empty($_REQUEST['end_date']) ? $_REQUEST['end_date'] : null;
$courseId = (int) ($_REQUEST['course_id'] ?? 0);
$exerciseId = $_REQUEST['exercise_id'] ?? 0; $exerciseId = $_REQUEST['exercise_id'] ?? 0;
$status = $_REQUEST['status'] ?? 0; $status = $_REQUEST['status'] ?? 0;
$questionType = $_REQUEST['questionType'] ?? 0; $questionType = $_REQUEST['questionType'] ?? 0;
$showAttemptsInSessions = (bool) $_REQUEST['showAttemptsInSessions']; $showAttemptsInSessions = $_REQUEST['showAttemptsInSessions'] ? true : false;
if (!empty($_GET['filter_by_user'])) { if (isset($_GET['filter_by_user']) && !empty($_GET['filter_by_user'])) {
$filter_user = (int) $_GET['filter_by_user']; $filter_user = (int) $_GET['filter_by_user'];
if (empty($whereCondition)) { if (empty($whereCondition)) {
$whereCondition .= " te.exe_user_id = '$filter_user'"; $whereCondition .= " te.exe_user_id = '$filter_user'";
@@ -662,7 +670,7 @@ switch ($action) {
} }
} }
if (!empty($_GET['group_id_in_toolbar'])) { if (isset($_GET['group_id_in_toolbar']) && !empty($_GET['group_id_in_toolbar'])) {
$groupIdFromToolbar = (int) $_GET['group_id_in_toolbar']; $groupIdFromToolbar = (int) $_GET['group_id_in_toolbar'];
if (!empty($groupIdFromToolbar)) { if (!empty($groupIdFromToolbar)) {
if (empty($whereCondition)) { if (empty($whereCondition)) {
@@ -681,6 +689,14 @@ switch ($action) {
$whereCondition .= " AND te.c_id = $courseId"; $whereCondition .= " AND te.c_id = $courseId";
} }
// Filtrage sur la date de fin d'exercice (exe_date)
if (!empty($search_start_date)) {
$whereCondition .= " AND te.exe_date >= '".Database::escape_string($search_start_date)." 00:00:00'";
}
if (!empty($search_end_date)) {
$whereCondition .= " AND te.exe_date <= '".Database::escape_string($search_end_date)." 23:59:59'";
}
$count = ExerciseLib::get_count_exam_results( $count = ExerciseLib::get_count_exam_results(
$exerciseId, $exerciseId,
$whereCondition, $whereCondition,
+37
View File
@@ -36,9 +36,46 @@ if (!isset($_FILES['audio_blob'], $_REQUEST['audio_dir'])) {
} }
$file = isset($_FILES['audio_blob']) ? $_FILES['audio_blob'] : []; $file = isset($_FILES['audio_blob']) ? $_FILES['audio_blob'] : [];
// Only allow audio uploads: sanitize the filename and reject any non-audio
// extension (e.g. .html) so the result cannot be served as HTML/JS by
// document.php (stored XSS). The RecordRTC StereoAudioRecorder only ever
// produces WAV, so .wav is the single legitimate extension here.
$fileName = disable_dangerous_file(api_replace_dangerous_char($file['name']));
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$allowedAudioExtensions = ['wav'];
if (!in_array($fileExtension, $allowedAudioExtensions, true)) {
if ($tool === 'exercise') {
header('Content-Type: application/json');
echo json_encode([
'error' => true,
'message' => Display::return_message(get_lang('UploadError'), 'error'),
]);
Display::cleanFlashMessages();
exit;
}
Display::addFlash(Display::return_message(get_lang('UploadError'), 'error'));
exit;
}
$file['name'] = $fileName;
$file['file'] = $file; $file['file'] = $file;
$audioDir = Security::remove_XSS($_REQUEST['audio_dir']); $audioDir = Security::remove_XSS($_REQUEST['audio_dir']);
// Neutralize path traversal: audio_dir must be a relative path inside the
// course document tree, so drop any traversal or absolute-path segments
// before it reaches mkdir() and DocumentManager::upload_document().
$audioDir = str_replace("\\", "/", $audioDir);
$cleanSegments = [];
foreach (explode("/", $audioDir) as $segment) {
if ("" === $segment || "." === $segment || ".." === $segment) {
continue;
}
$cleanSegments[] = $segment;
}
$audioDir = empty($cleanSegments) ? "" : "/".implode("/", $cleanSegments);
switch ($type) { switch ($type) {
case 'document': case 'document':
$dirBaseDocuments = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document'; $dirBaseDocuments = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
+14
View File
@@ -39,6 +39,20 @@ $waminame = Database::escape_string($waminame);
$waminame = api_replace_dangerous_char($waminame); $waminame = api_replace_dangerous_char($waminame);
$waminame = disable_dangerous_file($waminame); $waminame = disable_dangerous_file($waminame);
$wamidir = Security::remove_XSS($wamidir); $wamidir = Security::remove_XSS($wamidir);
// Neutralize path traversal: wamidir must be a relative path inside the course
// document tree, so drop any traversal or absolute-path segments before it
// reaches fopen()/DocumentManager::upload_document().
$wamidir = str_replace("\\", "/", $wamidir);
$cleanSegments = [];
foreach (explode("/", $wamidir) as $segment) {
if ("" === $segment || "." === $segment || ".." === $segment) {
continue;
}
$cleanSegments[] = $segment;
}
$wamidir = empty($cleanSegments) ? "" : "/".implode("/", $cleanSegments);
$content = file_get_contents('php://input'); $content = file_get_contents('php://input');
if (empty($content)) { if (empty($content)) {

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