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
5544 changed files with 742615 additions and 66096 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}
+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) {
+7 -3
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",
@@ -96,7 +96,7 @@
"phpoffice/phpspreadsheet": "^1.28", "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
+213 -205
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f85de82f07063e8ad5963e4816bd3eb5", "content-hash": "07abb2f1f2575b4339f7eb5c2e96702d",
"packages": [ "packages": [
{ {
"name": "alchemy/binary-driver", "name": "alchemy/binary-driver",
@@ -128,16 +128,16 @@
}, },
{ {
"name": "apereo/phpcas", "name": "apereo/phpcas",
"version": "1.6.1", "version": "1.6.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/apereo/phpCAS.git", "url": "https://github.com/apereo/phpCAS.git",
"reference": "c129708154852656aabb13d8606cd5b12dbbabac" "reference": "b413d2981947fd4f97079f715e0571d0c8f2a2c5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/apereo/phpCAS/zipball/c129708154852656aabb13d8606cd5b12dbbabac", "url": "https://api.github.com/repos/apereo/phpCAS/zipball/b413d2981947fd4f97079f715e0571d0c8f2a2c5",
"reference": "c129708154852656aabb13d8606cd5b12dbbabac", "reference": "b413d2981947fd4f97079f715e0571d0c8f2a2c5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -193,9 +193,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/apereo/phpCAS/issues", "issues": "https://github.com/apereo/phpCAS/issues",
"source": "https://github.com/apereo/phpCAS/tree/1.6.1" "source": "https://github.com/apereo/phpCAS/tree/1.6.2"
}, },
"time": "2023-02-19T19:52:35+00:00" "time": "2026-04-22T09:20:37+00:00"
}, },
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@@ -612,32 +612,34 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"abandoned": true,
"time": "2022-01-17T14:14:24+00:00" "time": "2022-01-17T14:14:24+00:00"
}, },
{ {
"name": "composer/pcre", "name": "composer/pcre",
"version": "3.3.2", "version": "3.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/pcre.git", "url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" "reference": "d5a341b3fb61f3001970940afb1d332968a183ed"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "reference": "d5a341b3fb61f3001970940afb1d332968a183ed",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.4 || ^8.0" "php": "^7.4 || ^8.0"
}, },
"conflict": { "conflict": {
"phpstan/phpstan": "<1.11.10" "phpstan/phpstan": "<2.2.2"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.12 || ^2", "phpstan/phpstan": "^2",
"phpstan/phpstan-strict-rules": "^1 || ^2", "phpstan/phpstan-deprecation-rules": "^2",
"phpunit/phpunit": "^8 || ^9" "phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^9"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@@ -675,7 +677,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/composer/pcre/issues", "issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2" "source": "https://github.com/composer/pcre/tree/3.4.0"
}, },
"funding": [ "funding": [
{ {
@@ -685,13 +687,9 @@
{ {
"url": "https://github.com/composer", "url": "https://github.com/composer",
"type": "github" "type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
} }
], ],
"time": "2024-11-12T16:29:46+00:00" "time": "2026-06-07T11:47:49+00:00"
}, },
{ {
"name": "container-interop/container-interop", "name": "container-interop/container-interop",
@@ -2314,26 +2312,25 @@
}, },
{ {
"name": "enshrined/svg-sanitize", "name": "enshrined/svg-sanitize",
"version": "0.16.0", "version": "0.22.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/darylldoyle/svg-sanitizer.git", "url": "https://github.com/darylldoyle/svg-sanitizer.git",
"reference": "239e257605e2141265b429e40987b2ee51bba4b4" "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/239e257605e2141265b429e40987b2ee51bba4b4", "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/0afa95ea74be155a7bcd6c6fb60c276c39984500",
"reference": "239e257605e2141265b429e40987b2ee51bba4b4", "reference": "0afa95ea74be155a7bcd6c6fb60c276c39984500",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ezyang/htmlpurifier": "^4.16", "php": "^7.1 || ^8.0"
"php": "^5.6 || ^7.0 || ^8.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^5.7 || ^6.5 || ^8.5" "phpunit/phpunit": "^6.5 || ^8.5"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@@ -2354,9 +2351,9 @@
"description": "An SVG sanitizer for PHP", "description": "An SVG sanitizer for PHP",
"support": { "support": {
"issues": "https://github.com/darylldoyle/svg-sanitizer/issues", "issues": "https://github.com/darylldoyle/svg-sanitizer/issues",
"source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.16.0" "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.22.0"
}, },
"time": "2023-03-20T10:51:12+00:00" "time": "2025-08-12T10:13:48+00:00"
}, },
{ {
"name": "essence/essence", "name": "essence/essence",
@@ -2588,12 +2585,12 @@
"version": "v5.5.1", "version": "v5.5.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/firebase/php-jwt.git", "url": "https://github.com/googleapis/php-jwt.git",
"reference": "83b609028194aa042ea33b5af2d41a7427de80e6" "reference": "83b609028194aa042ea33b5af2d41a7427de80e6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/83b609028194aa042ea33b5af2d41a7427de80e6", "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/83b609028194aa042ea33b5af2d41a7427de80e6",
"reference": "83b609028194aa042ea33b5af2d41a7427de80e6", "reference": "83b609028194aa042ea33b5af2d41a7427de80e6",
"shasum": "" "shasum": ""
}, },
@@ -2635,8 +2632,8 @@
"php" "php"
], ],
"support": { "support": {
"issues": "https://github.com/firebase/php-jwt/issues", "issues": "https://github.com/googleapis/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v5.5.1" "source": "https://github.com/googleapis/php-jwt/tree/v5.5.1"
}, },
"time": "2021-11-08T20:18:51+00:00" "time": "2021-11-08T20:18:51+00:00"
}, },
@@ -4973,27 +4970,27 @@
}, },
{ {
"name": "onelogin/php-saml", "name": "onelogin/php-saml",
"version": "3.4.1", "version": "3.8.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/onelogin/php-saml.git", "url": "https://github.com/SAML-Toolkits/php-saml.git",
"reference": "5fbf3486704ac9835b68184023ab54862c95f213" "reference": "8732729ea91d1764a8d39062647845bba4bb3c29"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/onelogin/php-saml/zipball/5fbf3486704ac9835b68184023ab54862c95f213", "url": "https://api.github.com/repos/SAML-Toolkits/php-saml/zipball/8732729ea91d1764a8d39062647845bba4bb3c29",
"reference": "5fbf3486704ac9835b68184023ab54862c95f213", "reference": "8732729ea91d1764a8d39062647845bba4bb3c29",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.4", "php": ">=7.2",
"robrichards/xmlseclibs": ">=3.0.4" "robrichards/xmlseclibs": "^3.1.5"
}, },
"require-dev": { "require-dev": {
"pdepend/pdepend": "^2.5.0", "pdepend/pdepend": "^2.5.0",
"php-coveralls/php-coveralls": "^1.0.2 || ^2.0", "php-coveralls/php-coveralls": "^1.0.2 || ^2.0",
"phploc/phploc": "^2.1 || ^3.0 || ^4.0", "phploc/phploc": "^2.1 || ^3.0 || ^4.0",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1", "phpunit/phpunit": ">8.5.51",
"sebastian/phpcpd": "^2.0 || ^3.0 || ^4.0", "sebastian/phpcpd": "^2.0 || ^3.0 || ^4.0",
"squizlabs/php_codesniffer": "^3.1.1" "squizlabs/php_codesniffer": "^3.1.1"
}, },
@@ -5012,19 +5009,27 @@
"license": [ "license": [
"MIT" "MIT"
], ],
"description": "OneLogin PHP SAML Toolkit", "description": "PHP SAML Toolkit",
"homepage": "https://developers.onelogin.com/saml/php", "homepage": "https://github.com/SAML-Toolkits/php-saml",
"keywords": [ "keywords": [
"Federation",
"SAML2", "SAML2",
"onelogin", "SSO",
"identity",
"saml" "saml"
], ],
"support": { "support": {
"email": "sixto.garcia@onelogin.com", "email": "sixto.martin.garcia@gmail.com",
"issues": "https://github.com/onelogin/php-saml/issues", "issues": "https://github.com/onelogin/SAML-Toolkits/issues",
"source": "https://github.com/onelogin/php-saml/" "source": "https://github.com/onelogin/SAML-Toolkits/"
}, },
"time": "2019-11-25T17:30:07+00:00" "funding": [
{
"url": "https://github.com/SAML-Toolkits",
"type": "github"
}
],
"time": "2026-05-11T22:19:24+00:00"
}, },
{ {
"name": "packbackbooks/lti-1p3-tool", "name": "packbackbooks/lti-1p3-tool",
@@ -6465,16 +6470,16 @@
}, },
{ {
"name": "phpoffice/phpspreadsheet", "name": "phpoffice/phpspreadsheet",
"version": "1.30.2", "version": "1.30.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2" "reference": "97bcabd32a64924688487dcd64aceaf158affb5c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/09cdde5e2f078b9a3358dd217e2c8cb4dac84be2", "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/97bcabd32a64924688487dcd64aceaf158affb5c",
"reference": "09cdde5e2f078b9a3358dd217e2c8cb4dac84be2", "reference": "97bcabd32a64924688487dcd64aceaf158affb5c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6567,9 +6572,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.2" "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.5"
}, },
"time": "2026-01-11T05:58:24+00:00" "time": "2026-05-31T05:13:11+00:00"
}, },
{ {
"name": "phpoffice/phpword", "name": "phpoffice/phpword",
@@ -6685,16 +6690,16 @@
}, },
{ {
"name": "phpseclib/phpseclib", "name": "phpseclib/phpseclib",
"version": "2.0.52", "version": "2.0.55",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpseclib/phpseclib.git", "url": "https://github.com/phpseclib/phpseclib.git",
"reference": "2552c4001631d1cc844332faea6a08a49c964b28" "reference": "d73c9e019a895be83b18a2ccccfa7e2b0a648743"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2552c4001631d1cc844332faea6a08a49c964b28", "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d73c9e019a895be83b18a2ccccfa7e2b0a648743",
"reference": "2552c4001631d1cc844332faea6a08a49c964b28", "reference": "d73c9e019a895be83b18a2ccccfa7e2b0a648743",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6775,7 +6780,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/phpseclib/phpseclib/issues", "issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/2.0.52" "source": "https://github.com/phpseclib/phpseclib/tree/2.0.55"
}, },
"funding": [ "funding": [
{ {
@@ -6791,7 +6796,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-03-19T02:54:44+00:00" "time": "2026-06-14T19:53:12+00:00"
}, },
{ {
"name": "psr/cache", "name": "psr/cache",
@@ -7297,16 +7302,16 @@
}, },
{ {
"name": "rmccue/requests", "name": "rmccue/requests",
"version": "v2.0.17", "version": "v2.0.18",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/WordPress/Requests.git", "url": "https://github.com/WordPress/Requests.git",
"reference": "74d1648cc34e16a42ea25d548fc73ec107a90421" "reference": "2e5b8434e0dd54b35bcf1e9a5b52ba2ad84cf773"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/WordPress/Requests/zipball/74d1648cc34e16a42ea25d548fc73ec107a90421", "url": "https://api.github.com/repos/WordPress/Requests/zipball/2e5b8434e0dd54b35bcf1e9a5b52ba2ad84cf773",
"reference": "74d1648cc34e16a42ea25d548fc73ec107a90421", "reference": "2e5b8434e0dd54b35bcf1e9a5b52ba2ad84cf773",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7379,20 +7384,20 @@
"issues": "https://github.com/WordPress/Requests/issues", "issues": "https://github.com/WordPress/Requests/issues",
"source": "https://github.com/WordPress/Requests" "source": "https://github.com/WordPress/Requests"
}, },
"time": "2025-12-12T17:47:19+00:00" "time": "2026-04-30T00:41:23+00:00"
}, },
{ {
"name": "robrichards/xmlseclibs", "name": "robrichards/xmlseclibs",
"version": "3.0.4", "version": "3.1.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/robrichards/xmlseclibs.git", "url": "https://github.com/robrichards/xmlseclibs.git",
"reference": "0a53d3c3aa87564910cae4ed01416441d3ae0db5" "reference": "03062be78178cbb5e8f605cd255dc32a14981f92"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/0a53d3c3aa87564910cae4ed01416441d3ae0db5", "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/03062be78178cbb5e8f605cd255dc32a14981f92",
"reference": "0a53d3c3aa87564910cae4ed01416441d3ae0db5", "reference": "03062be78178cbb5e8f605cd255dc32a14981f92",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7419,9 +7424,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/robrichards/xmlseclibs/issues", "issues": "https://github.com/robrichards/xmlseclibs/issues",
"source": "https://github.com/robrichards/xmlseclibs/tree/master" "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.5"
}, },
"time": "2019-11-05T11:44:22+00:00" "time": "2026-03-13T10:31:56+00:00"
}, },
{ {
"name": "sabre/vobject", "name": "sabre/vobject",
@@ -7496,16 +7501,16 @@
}, },
{ {
"name": "setasign/fpdi", "name": "setasign/fpdi",
"version": "v2.6.6", "version": "v2.6.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Setasign/FPDI.git", "url": "https://github.com/Setasign/FPDI.git",
"reference": "de0cf35911be3e9ea63b48e0f307883b1c7c48ac" "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/de0cf35911be3e9ea63b48e0f307883b1c7c48ac", "url": "https://api.github.com/repos/Setasign/FPDI/zipball/881945be29a4996ad3d008eb18ddc01fa3df890c",
"reference": "de0cf35911be3e9ea63b48e0f307883b1c7c48ac", "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7517,7 +7522,7 @@
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.5.52", "phpunit/phpunit": "^8.5.52",
"setasign/fpdf": "~1.8.6", "setasign/fpdf": "^1.9.0",
"setasign/tfpdf": "~1.33", "setasign/tfpdf": "~1.33",
"squizlabs/php_codesniffer": "^3.5", "squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "^6.8" "tecnickcom/tcpdf": "^6.8"
@@ -7556,7 +7561,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Setasign/FPDI/issues", "issues": "https://github.com/Setasign/FPDI/issues",
"source": "https://github.com/Setasign/FPDI/tree/v2.6.6" "source": "https://github.com/Setasign/FPDI/tree/v2.6.8"
}, },
"funding": [ "funding": [
{ {
@@ -7564,7 +7569,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-03-13T08:38:20+00:00" "time": "2026-06-11T10:37:24+00:00"
}, },
{ {
"name": "sonata-project/admin-bundle", "name": "sonata-project/admin-bundle",
@@ -8387,28 +8392,28 @@
}, },
{ {
"name": "stripe/stripe-php", "name": "stripe/stripe-php",
"version": "v19.4.1", "version": "v20.2.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/stripe/stripe-php.git", "url": "https://github.com/stripe/stripe-php.git",
"reference": "095384404587d07de2ad1154c389c4051c5ed92f" "reference": "c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/095384404587d07de2ad1154c389c4051c5ed92f", "url": "https://api.github.com/repos/stripe/stripe-php/zipball/c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5",
"reference": "095384404587d07de2ad1154c389c4051c5ed92f", "reference": "c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-curl": "*", "ext-curl": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"php": ">=5.6.0" "php": ">=7.2.0"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "3.94.0", "friendsofphp/php-cs-fixer": "3.94.0",
"phpstan/phpstan": "^1.2", "phpstan/phpstan": "^1.2",
"phpunit/phpunit": "^5.7 || ^9.0" "phpunit/phpunit": "^8.0 || ^9.0"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@@ -8417,6 +8422,9 @@
} }
}, },
"autoload": { "autoload": {
"files": [
"lib/version_check.php"
],
"psr-4": { "psr-4": {
"Stripe\\": "lib/" "Stripe\\": "lib/"
} }
@@ -8440,22 +8448,22 @@
], ],
"support": { "support": {
"issues": "https://github.com/stripe/stripe-php/issues", "issues": "https://github.com/stripe/stripe-php/issues",
"source": "https://github.com/stripe/stripe-php/tree/v19.4.1" "source": "https://github.com/stripe/stripe-php/tree/v20.2.1"
}, },
"time": "2026-03-06T22:53:13+00:00" "time": "2026-06-12T22:41:56+00:00"
}, },
{ {
"name": "studio-42/elfinder", "name": "studio-42/elfinder",
"version": "2.1.66", "version": "2.1.69",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Studio-42/elFinder.git", "url": "https://github.com/Studio-42/elFinder.git",
"reference": "78488951e44d69e8b9e4e849f8268df408632a6c" "reference": "8f2c3ffafcdd52cf4515f1eec172f4eee44552ad"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Studio-42/elFinder/zipball/78488951e44d69e8b9e4e849f8268df408632a6c", "url": "https://api.github.com/repos/Studio-42/elFinder/zipball/8f2c3ffafcdd52cf4515f1eec172f4eee44552ad",
"reference": "78488951e44d69e8b9e4e849f8268df408632a6c", "reference": "8f2c3ffafcdd52cf4515f1eec172f4eee44552ad",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8502,7 +8510,7 @@
"homepage": "http://elfinder.org", "homepage": "http://elfinder.org",
"support": { "support": {
"issues": "https://github.com/Studio-42/elFinder/issues", "issues": "https://github.com/Studio-42/elFinder/issues",
"source": "https://github.com/Studio-42/elFinder/tree/2.1.66" "source": "https://github.com/Studio-42/elFinder/tree/2.1.69"
}, },
"funding": [ "funding": [
{ {
@@ -8510,7 +8518,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-08-28T11:51:22+00:00" "time": "2026-05-07T12:53:30+00:00"
}, },
{ {
"name": "sunra/php-simple-html-dom-parser", "name": "sunra/php-simple-html-dom-parser",
@@ -9299,6 +9307,71 @@
], ],
"time": "2022-07-20T09:59:04+00:00" "time": "2022-07-20T09:59:04+00:00"
}, },
{
"name": "symfony/css-selector",
"version": "v3.4.47",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v3.4.47"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-24T10:57:07+00:00"
},
{ {
"name": "symfony/debug", "name": "symfony/debug",
"version": "v4.4.44", "version": "v4.4.44",
@@ -10439,7 +10512,7 @@
}, },
{ {
"name": "symfony/polyfill-apcu", "name": "symfony/polyfill-apcu",
"version": "v1.33.0", "version": "v1.37.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-apcu.git", "url": "https://github.com/symfony/polyfill-apcu.git",
@@ -10493,7 +10566,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-apcu/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-apcu/tree/v1.37.0"
}, },
"funding": [ "funding": [
{ {
@@ -10517,16 +10590,16 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.33.0", "version": "v1.37.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10576,7 +10649,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
}, },
"funding": [ "funding": [
{ {
@@ -10596,20 +10669,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-09T11:45:10+00:00" "time": "2026-04-10T16:19:22+00:00"
}, },
{ {
"name": "symfony/polyfill-iconv", "name": "symfony/polyfill-iconv",
"version": "v1.33.0", "version": "v1.37.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git", "url": "https://github.com/symfony/polyfill-iconv.git",
"reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa" "reference": "2c5729fd241b4b22f6e4b436bc3354a4f262df57"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/5f3b930437ae03ae5dff61269024d8ea1b3774aa", "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/2c5729fd241b4b22f6e4b436bc3354a4f262df57",
"reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa", "reference": "2c5729fd241b4b22f6e4b436bc3354a4f262df57",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10660,7 +10733,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-iconv/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-iconv/tree/v1.37.0"
}, },
"funding": [ "funding": [
{ {
@@ -10680,20 +10753,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-17T14:58:18+00:00" "time": "2026-04-10T16:19:22+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-icu", "name": "symfony/polyfill-intl-icu",
"version": "v1.33.0", "version": "v1.38.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-icu.git", "url": "https://github.com/symfony/polyfill-intl-icu.git",
"reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c" "reference": "445c90e341fccda10311019cf82ff73bb7343945"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/445c90e341fccda10311019cf82ff73bb7343945",
"reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", "reference": "445c90e341fccda10311019cf82ff73bb7343945",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10748,7 +10821,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.38.0"
}, },
"funding": [ "funding": [
{ {
@@ -10768,20 +10841,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-20T22:24:30+00:00" "time": "2026-05-25T11:52:53+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-idn", "name": "symfony/polyfill-intl-idn",
"version": "v1.33.0", "version": "v1.38.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git", "url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" "reference": "dc21118016c039a66235cf93d96b435ffb282412"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412",
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "reference": "dc21118016c039a66235cf93d96b435ffb282412",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10835,7 +10908,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1"
}, },
"funding": [ "funding": [
{ {
@@ -10855,20 +10928,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-10T14:38:51+00:00" "time": "2026-05-25T15:22:23+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.33.0", "version": "v1.38.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "3833d7255cc303546435cb650316bff708a1c75c" "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b",
"reference": "3833d7255cc303546435cb650316bff708a1c75c", "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10920,7 +10993,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0"
}, },
"funding": [ "funding": [
{ {
@@ -10940,20 +11013,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-09T11:45:10+00:00" "time": "2026-05-25T13:48:31+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.33.0", "version": "v1.38.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -11005,7 +11078,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
}, },
"funding": [ "funding": [
{ {
@@ -11025,7 +11098,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-12-23T08:48:59+00:00" "time": "2026-05-27T06:59:30+00:00"
}, },
{ {
"name": "symfony/polyfill-php56", "name": "symfony/polyfill-php56",
@@ -11165,16 +11238,16 @@
}, },
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.33.0", "version": "v1.37.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php80.git", "url": "https://github.com/symfony/polyfill-php80.git",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -11225,7 +11298,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
}, },
"funding": [ "funding": [
{ {
@@ -11245,7 +11318,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-01-02T08:10:11+00:00" "time": "2026-04-10T16:19:22+00:00"
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
@@ -15734,71 +15807,6 @@
], ],
"time": "2022-07-25T12:56:14+00:00" "time": "2022-07-25T12:56:14+00:00"
}, },
{
"name": "symfony/css-selector",
"version": "v3.4.47",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v3.4.47"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-24T10:57:07+00:00"
},
{ {
"name": "theseer/tokenizer", "name": "theseer/tokenizer",
"version": "1.3.1", "version": "1.3.1",
@@ -15869,11 +15877,11 @@
"ddeboer/data-import": 0, "ddeboer/data-import": 0,
"doctrine/data-fixtures": 20, "doctrine/data-fixtures": 20,
"doctrine/migrations": 20, "doctrine/migrations": 20,
"packbackbooks/lti-1p3-tool": 20,
"php-xapi/client": 20, "php-xapi/client": 20,
"php-xapi/repository-api": 20, "php-xapi/repository-api": 20,
"php-xapi/repository-doctrine": 20, "php-xapi/repository-doctrine": 20,
"yuloh/bccomp-polyfill": 20, "yuloh/bccomp-polyfill": 20
"packbackbooks/lti-1p3-tool": 20
}, },
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
@@ -15891,6 +15899,6 @@
"ext-zip": "*", "ext-zip": "*",
"ext-zlib": "*" "ext-zlib": "*"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"
} }
+57
View File
@@ -110,6 +110,63 @@
</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"> <div class="version" aria-label="1.11.38">
<a id="1.11.38"></a> <a id="1.11.38"></a>
<h1>Chamilo 1.11.38 - Pontorson, 23/03/2026</h1> <h1>Chamilo 1.11.38 - Pontorson, 23/03/2026</h1>
+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
} }
+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
+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'));
} }
+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);
+1 -1
View File
@@ -328,7 +328,7 @@ function save_data(array $users, bool $sendMail = false, ?string $targetFolder =
} }
$email = $user['Email'] ?? $userInfo['email']; $email = $user['Email'] ?? $userInfo['email'];
$status = $user['Status'] ?? $userInfo['status']; $status = api_status_key($user['Status']) ?? $userInfo['status'];
$officialCode = $user['OfficialCode'] ?? $userInfo['official_code']; $officialCode = $user['OfficialCode'] ?? $userInfo['official_code'];
$phone = $user['PhoneNumber'] ?? $userInfo['phone']; $phone = $user['PhoneNumber'] ?? $userInfo['phone'];
$pictureUrl = $user['PictureUri'] ?? $userInfo['picture_uri']; $pictureUrl = $user['PictureUri'] ?? $userInfo['picture_uri'];
+7 -2
View File
@@ -469,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
+2 -2
View File
@@ -235,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;
+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 -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);
+118 -5
View File
@@ -41,17 +41,51 @@ 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'];
@@ -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');
+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 -2
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 = '';
+100 -85
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>';
} }
@@ -2152,12 +2173,8 @@ if (empty($_GET['details'])) {
$from = '&from=myspace'; $from = '&from=myspace';
} }
$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);
@@ -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,24 +216,23 @@ switch ($action) {
); );
} }
} }
}
foreach ($exportData as $key => $row) {
$list = GradebookUtils::get_list_gradebook_certificates_by_user_id(
$row[0],
$categoryId
);
foreach ($exportData as $key => $row) { foreach ($list as $valueCertificate) {
$list = GradebookUtils::get_list_gradebook_certificates_by_user_id( $row[] = $valueCertificate['score_certificate'];
$row[0], $row[] = api_convert_and_format_date($valueCertificate['created_at']);
$categoryId
);
foreach ($list as $valueCertificate) {
$row[] = $valueCertificate['score_certificate'];
$row[] = api_convert_and_format_date($valueCertificate['created_at']);
}
foreach ($usersProfileInfo as $extraInfo) {
$row[] = $extraInfo[$row[0]][0];
}
$csvContent[] = $row;
} }
foreach ($usersProfileInfo as $extraInfo) {
$row[] = $extraInfo[$row[0]][0];
}
$csvContent[] = $row;
} }
array_unshift($csvContent, $csvHeaders); array_unshift($csvContent, $csvHeaders);
+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']);
+8 -6
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 ';
+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);
@@ -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()
+10 -5
View File
@@ -80,14 +80,19 @@ switch ($action) {
$userList = implode(',', $userList); $userList = implode(',', $userList);
shell_exec(sprintf( $command = sprintf(
"php %s %s %s %s %s > /dev/null &", "php %s %s %s %s",
escapeshellarg($commandScript), escapeshellarg($commandScript),
escapeshellarg($courseCode), escapeshellarg($courseCode),
escapeshellarg((string) $sessionId), escapeshellarg((string) $sessionId),
escapeshellarg((string) $categoryId), escapeshellarg((string) $categoryId)
escapeshellarg($userList) );
));
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);
+13 -3
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,
[ [
@@ -565,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,
@@ -646,7 +656,7 @@ switch ($action) {
} }
$search_start_date = isset($_REQUEST['start_date']) && !empty($_REQUEST['start_date']) ? $_REQUEST['start_date'] : null; $search_start_date = isset($_REQUEST['start_date']) && !empty($_REQUEST['start_date']) ? $_REQUEST['start_date'] : null;
$search_end_date = isset($_REQUEST['end_date']) && !empty($_REQUEST['end_date']) ? $_REQUEST['end_date'] : null; $search_end_date = isset($_REQUEST['end_date']) && !empty($_REQUEST['end_date']) ? $_REQUEST['end_date'] : null;
$courseId = $_REQUEST['course_id'] ?? 0; $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;
+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)) {
+4 -1
View File
@@ -136,6 +136,9 @@ switch ($action) {
} }
break; break;
case 'session_info': case 'session_info':
SessionManager::protectSession(null, false);
api_protect_limit_for_session_admin();
$sessionId = isset($_GET['session_id']) ? $_GET['session_id'] : ''; $sessionId = isset($_GET['session_id']) ? $_GET['session_id'] : '';
$sessionInfo = api_get_session_info($sessionId); $sessionInfo = api_get_session_info($sessionId);
@@ -166,7 +169,7 @@ switch ($action) {
} }
break; break;
case 'get_description': case 'get_description':
if (isset($_GET['session'])) { if (CoursesAndSessionsCatalog::userCanView() && isset($_GET['session'])) {
$sessionInfo = api_get_session_info($_GET['session']); $sessionInfo = api_get_session_info($_GET['session']);
echo '<h2>'.$sessionInfo['name'].'</h2>'; echo '<h2>'.$sessionInfo['name'].'</h2>';
echo '<div class="home-course-intro"><div class="page-course"><div class="page-course-intro">'; echo '<div class="home-course-intro"><div class="page-course"><div class="page-course-intro">';
+2 -2
View File
@@ -9,7 +9,7 @@ $kernel = new AppKernel('', '');
if (isset($_GET['action'])) { if (isset($_GET['action'])) {
$action = $_GET['action']; $action = $_GET['action'];
if ($action == 'time') { if ($action === 'time') {
// Load the Chamilo configuration // Load the Chamilo configuration
$alreadyInstalled = false; $alreadyInstalled = false;
if (file_exists($kernel->getConfigurationFile())) { if (file_exists($kernel->getConfigurationFile())) {
@@ -56,7 +56,7 @@ if (isset($_GET['action'])) {
http_response_code(500); http_response_code(500);
echo json_encode(['error' => 'Error retrieving data from the current session']); echo json_encode(['error' => 'Error retrieving data from the current session']);
} }
} elseif ($action == 'logout') { } elseif ($action === 'logout') {
require_once __DIR__.'/../../../main/inc/global-min.inc.php'; require_once __DIR__.'/../../../main/inc/global-min.inc.php';
$userId = api_get_user_id(); $userId = api_get_user_id();
+7 -5
View File
@@ -464,6 +464,8 @@ switch ($action) {
echo json_encode(['items' => $items]); echo json_encode(['items' => $items]);
break; break;
case 'update_users': case 'update_users':
api_protect_admin_script(true);
$usersData = json_decode($_POST['users'], true); $usersData = json_decode($_POST['users'], true);
$updatedCount = 0; $updatedCount = 0;
@@ -480,11 +482,11 @@ switch ($action) {
} }
$updatedData = [ $updatedData = [
'firstname' => $userData['firstname'] ?? $currentUserData['firstname'], 'firstname' => attr_on_filter(html_filter($userData['firstname'] ?? $currentUserData['firstname'])),
'lastname' => $userData['lastname'] ?? $currentUserData['lastname'], 'lastname' => attr_on_filter(html_filter($userData['lastname'] ?? $currentUserData['lastname'])),
'email' => $userData['email'] ?? $currentUserData['email'], 'email' => attr_on_filter(html_filter($userData['email'] ?? $currentUserData['email'])),
'phone' => $userData['phone'] ?? $currentUserData['phone'], 'phone' => attr_on_filter(html_filter($userData['phone'] ?? $currentUserData['phone'])),
'official_code' => $userData['official_code'] ?? $currentUserData['official_code'], 'official_code' => attr_on_filter(html_filter($userData['official_code'] ?? $currentUserData['official_code'])),
'status' => isset($userData['status']) ? (int) $userData['status'] : $currentUserData['status'], 'status' => isset($userData['status']) ? (int) $userData['status'] : $currentUserData['status'],
'active' => isset($userData['active']) ? (int) $userData['active'] : $currentUserData['active'], 'active' => isset($userData['active']) ? (int) $userData['active'] : $currentUserData['active'],
]; ];
+17 -6
View File
@@ -388,6 +388,12 @@ if (isset($this_script) && $this_script == 'sub_language') {
//getting sub language info //getting sub language info
$sub_language = SubLanguageManager::get_all_information_of_language($_REQUEST['sub_language_id']); $sub_language = SubLanguageManager::get_all_information_of_language($_REQUEST['sub_language_id']);
// Validate folder names before using them in file paths
$parentFolderValid = !empty($parent_language['dokeos_folder']) &&
SubLanguageManager::isValidLanguageFolderName($parent_language['dokeos_folder']);
$subFolderValid = !empty($sub_language['dokeos_folder']) &&
SubLanguageManager::isValidLanguageFolderName($sub_language['dokeos_folder']);
$english_language_array = $parent_language_array = $sub_language_array = []; $english_language_array = $parent_language_array = $sub_language_array = [];
foreach ($language_files_to_load as $language_file_item) { foreach ($language_files_to_load as $language_file_item) {
@@ -409,10 +415,13 @@ if (isset($this_script) && $this_script == 'sub_language') {
foreach ($lang_list_result as $item) { foreach ($lang_list_result as $item) {
unset(${$item}); unset(${$item});
} }
$parent_file = $langpath.$parent_language['dokeos_folder'].'/'.$language_file_item.'.inc.php';
if (file_exists($parent_file) && is_file($parent_file)) { if ($parentFolderValid) {
include_once $parent_file; $parent_file = $langpath.$parent_language['dokeos_folder'].'/'.$language_file_item.'.inc.php';
if (file_exists($parent_file) && is_file($parent_file)) {
include_once $parent_file;
}
} }
// parent language array // parent language array
$parent_language_array[$language_file_item] = compact($lang_list_result); $parent_language_array[$language_file_item] = compact($lang_list_result);
@@ -422,9 +431,11 @@ if (isset($this_script) && $this_script == 'sub_language') {
unset(${$item}); unset(${$item});
} }
$sub_file = $langpath.$sub_language['dokeos_folder'].'/'.$language_file_item.'.inc.php'; if ($subFolderValid) {
if (file_exists($sub_file) && is_file($sub_file)) { $sub_file = $langpath.$sub_language['dokeos_folder'].'/'.$language_file_item.'.inc.php';
include $sub_file; if (file_exists($sub_file) && is_file($sub_file)) {
include $sub_file;
}
} }
// sub language array // sub language array
+120 -1
View File
@@ -1,13 +1,43 @@
<?php <?php
/* For licensing terms, see /license.txt */ /* For licensing terms, see /license.txt */
use Chamilo\UserBundle\Entity\User;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Twig\Environment;
use Twig\Extension\SandboxExtension;
use Twig\Loader\ArrayLoader;
use Twig\Sandbox\SecurityPolicy;
use Twig\TwigFilter;
use Twig\TwigFunction;
/** /**
* Class MailTemplateManager. * Class MailTemplateManager.
*/ */
class MailTemplateManager extends Model class MailTemplateManager extends Model
{ {
/**
* Twig tags allowed inside an admin-stored mail template body.
*/
public const ALLOWED_TAGS = ['if', 'for', 'set', 'apply', 'spaceless', 'autoescape', 'with'];
/**
* Twig filters allowed inside an admin-stored mail template body. The
* callable-accepting gadget filters (filter/map/reduce/sort) are
* deliberately excluded to prevent SSTI RCE.
*/
public const ALLOWED_FILTERS = [
'abs', 'capitalize', 'date', 'date_modify', 'default', 'escape', 'e',
'first', 'format', 'join', 'json_encode', 'keys', 'last', 'length',
'lower', 'merge', 'nl2br', 'number_format', 'raw', 'replace', 'reverse',
'round', 'slice', 'split', 'striptags', 'title', 'trim', 'upper',
'url_encode', 'get_lang',
];
/**
* Twig functions allowed inside an admin-stored mail template body.
*/
public const ALLOWED_FUNCTIONS = ['max', 'min', 'range', 'get_lang'];
public $columns = [ public $columns = [
'id', 'id',
'name', 'name',
@@ -113,7 +143,13 @@ class MailTemplateManager extends Model
);*/ );*/
$form->addTextarea( $form->addTextarea(
'email_template', 'email_template',
get_lang('Template') get_lang('Template'),
['rows' => 20]
);
$form->addLabel(
get_lang('Allowed template syntax'),
$this->getAllowedSyntaxHelp()
); );
$finder = new Finder(); $finder = new Finder();
@@ -234,4 +270,87 @@ class MailTemplateManager extends Model
return $result['template']; return $result['template'];
} }
/**
* Builds the help block describing the Twig syntax allowed in a mail
* template, derived from the same allowlists enforced by
* renderSandboxedTemplate() so the form and the sandbox never drift apart.
*
* @return string
*/
public function getAllowedSyntaxHelp(): string
{
$tags = api_htmlentities(implode(', ', self::ALLOWED_TAGS));
$filters = api_htmlentities(implode(', ', self::ALLOWED_FILTERS));
$functions = api_htmlentities(implode(', ', self::ALLOWED_FUNCTIONS));
$html = '<div class="space-y-2 mt-2 rounded-lg border border-gray-25 bg-gray-15 p-4 text-sm text-gray-70">';
$html .= '<p><strong>'.api_htmlentities(get_lang('Tags')).':</strong> <code>'.$tags.'</code></p>';
$html .= '<p><strong>'.api_htmlentities(get_lang('Filters')).':</strong> <code>'.$filters.'</code></p>';
$html .= '<p><strong>'.api_htmlentities(get_lang('Functions')).':</strong> <code>'.$functions.'</code></p>';
$html .= '<p>'.api_htmlentities(get_lang('Available variables depend on the template type, for example {{ user.getUsername() }} or {{ user.getEmail() }}.')).'</p>';
$html .= '<p>'.api_htmlentities(get_lang('For security reasons, any other Twig function, filter (such as filter, map, reduce or sort) or PHP call is blocked.')).'</p>';
$html .= '</div>';
return $html;
}
/**
* Renders an admin-stored mail template body through a sandboxed Twig
* environment.
*
* Stored mail templates are untrusted content (any platform admin can edit
* them) and must never be compiled with the full application Twig: the
* non-sandboxed environment exposes the callable-accepting filters
* (filter/map/reduce/sort) that turn a template body into a Server-Side
* Template Injection Remote Code Execution gadget.
*
* The sandbox here uses an explicit allow-list of tags, filters, functions
* and entity getters; everything else including the RCE gadget filters
* is rejected. When rendering is refused or fails, an empty string is
* returned so the caller falls back to the default file-based template.
*
* @param string $templateText The admin-stored Twig template body
* @param array $params The render context (template variables)
*
* @return string The rendered body, or '' when rendering is rejected
*/
public static function renderSandboxedTemplate(string $templateText, array $params): string
{
if ('' === trim($templateText)) {
return '';
}
$allowedMethods = [
User::class => [
'getId', 'getUsername', 'getFirstname', 'getLastname',
'getEmail', 'getStatus', 'getOfficialCode', 'getPhone',
],
];
$allowedProperties = [];
$policy = new SecurityPolicy(
self::ALLOWED_TAGS,
self::ALLOWED_FILTERS,
$allowedMethods,
$allowedProperties,
self::ALLOWED_FUNCTIONS
);
$twig = new Environment(
new ArrayLoader(['mail_template' => $templateText]),
['autoescape' => 'html', 'cache' => false, 'strict_variables' => false]
);
$twig->addExtension(new SandboxExtension($policy, true));
$twig->addFilter(new TwigFilter('get_lang', 'get_lang'));
$twig->addFunction(new TwigFunction('get_lang', 'get_lang'));
try {
return $twig->render('mail_template', $params);
} catch (Throwable $e) {
error_log('Refused to render stored mail template in sandbox: '.$e->getMessage());
return '';
}
}
} }
+46 -22
View File
@@ -10,6 +10,30 @@
*/ */
class MoodleImport class MoodleImport
{ {
/**
* Loads XML into a DOMDocument hardened against XXE attacks.
*
* External entity loading is blocked (libxml_disable_entity_loader) so a
* malicious Moodle backup cannot trigger SSRF via network entities nor
* disclose local files via file:// entities. LIBXML_NONET is passed as
* defense-in-depth. The previous loader state is restored afterwards.
*
* @param \DOMDocument $doc document populated in place
* @param string $xml XML to parse
*
* @return bool result of DOMDocument::loadXML()
*/
private static function loadXml(\DOMDocument $doc, $xml)
{
$previous = @libxml_disable_entity_loader(true);
try {
return @$doc->loadXML($xml, LIBXML_NONET);
} finally {
@libxml_disable_entity_loader($previous);
}
}
/** /**
* Import moodle file. * Import moodle file.
* *
@@ -134,7 +158,7 @@ class MoodleImport
$xml = @file_get_contents($destinationDir.'/moodle_backup.xml'); $xml = @file_get_contents($destinationDir.'/moodle_backup.xml');
$doc = new DOMDocument(); $doc = new DOMDocument();
$res = @$doc->loadXML($xml); $res = self::loadXml($doc, $xml);
if (empty($res)) { if (empty($res)) {
removeDir($destinationDir); removeDir($destinationDir);
@@ -617,7 +641,7 @@ class MoodleImport
$xml = file_get_contents($moodleBackupXmlPath); $xml = file_get_contents($moodleBackupXmlPath);
$doc = new DOMDocument(); $doc = new DOMDocument();
$doc->loadXML($xml); self::loadXml($doc, $xml);
$backupRelease = $doc->getElementsByTagName('backup_release'); $backupRelease = $doc->getElementsByTagName('backup_release');
$version = null; $version = null;
foreach ($backupRelease as $release) { foreach ($backupRelease as $release) {
@@ -788,7 +812,7 @@ class MoodleImport
public function readSections($xml, $destinationDir) public function readSections($xml, $destinationDir)
{ {
$doc = new DOMDocument(); $doc = new DOMDocument();
$res = @$doc->loadXML($xml); $res = self::loadXml($doc, $xml);
if (empty($res)) { if (empty($res)) {
return false; return false;
} }
@@ -823,7 +847,7 @@ class MoodleImport
public function readSectionModule($sectionInfoXml) public function readSectionModule($sectionInfoXml)
{ {
$doc = new DOMDocument(); $doc = new DOMDocument();
$res = @$doc->loadXML($sectionInfoXml); $res = self::loadXml($doc, $sectionInfoXml);
if (empty($res)) { if (empty($res)) {
return false; return false;
} }
@@ -852,7 +876,7 @@ class MoodleImport
public function readLessonModule($moduleXml) public function readLessonModule($moduleXml)
{ {
$doc = new DOMDocument(); $doc = new DOMDocument();
$res = @$doc->loadXML($moduleXml); $res = self::loadXml($doc, $moduleXml);
if (empty($res)) { if (empty($res)) {
return false; return false;
} }
@@ -906,7 +930,7 @@ class MoodleImport
public function readAssignModule($moduleXml) public function readAssignModule($moduleXml)
{ {
$doc = new DOMDocument(); $doc = new DOMDocument();
$res = @$doc->loadXML($moduleXml); $res = self::loadXml($doc, $moduleXml);
if (empty($res)) { if (empty($res)) {
return false; return false;
} }
@@ -991,7 +1015,7 @@ class MoodleImport
public function readScormModule($moduleXml) public function readScormModule($moduleXml)
{ {
$doc = new DOMDocument(); $doc = new DOMDocument();
$res = @$doc->loadXML($moduleXml); $res = self::loadXml($doc, $moduleXml);
if (empty($res)) { if (empty($res)) {
return false; return false;
} }
@@ -1025,7 +1049,7 @@ class MoodleImport
public function readGlossaryModule($moduleXml, $moduleId) public function readGlossaryModule($moduleXml, $moduleId)
{ {
$doc = new DOMDocument(); $doc = new DOMDocument();
$res = @$doc->loadXML($moduleXml); $res = self::loadXml($doc, $moduleXml);
if (empty($res)) { if (empty($res)) {
return false; return false;
} }
@@ -1059,7 +1083,7 @@ class MoodleImport
public function readHtmlModule($moduleXml, $moduleName) public function readHtmlModule($moduleXml, $moduleName)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($moduleXml); $moduleRes = self::loadXml($moduleDoc, $moduleXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return false; return false;
} }
@@ -1660,7 +1684,7 @@ class MoodleImport
public function readForumModule($moduleXml) public function readForumModule($moduleXml)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($moduleXml); $moduleRes = self::loadXml($moduleDoc, $moduleXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return false; return false;
} }
@@ -1715,7 +1739,7 @@ class MoodleImport
public function readFolderModule($moduleXml) public function readFolderModule($moduleXml)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($moduleXml); $moduleRes = self::loadXml($moduleDoc, $moduleXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return false; return false;
} }
@@ -1748,7 +1772,7 @@ class MoodleImport
public function readResourceModule($moduleXml) public function readResourceModule($moduleXml)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($moduleXml); $moduleRes = self::loadXml($moduleDoc, $moduleXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return false; return false;
} }
@@ -1781,7 +1805,7 @@ class MoodleImport
public function readUrlModule($moduleXml) public function readUrlModule($moduleXml)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($moduleXml); $moduleRes = self::loadXml($moduleDoc, $moduleXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return false; return false;
} }
@@ -1811,7 +1835,7 @@ class MoodleImport
public function readQuizGradeModule($gradeXml, $quizId) public function readQuizGradeModule($gradeXml, $quizId)
{ {
$doc = new DOMDocument(); $doc = new DOMDocument();
$gradeRes = @$doc->loadXML($gradeXml); $gradeRes = self::loadXml($doc, $gradeXml);
if (empty($gradeRes)) { if (empty($gradeRes)) {
return false; return false;
} }
@@ -1851,7 +1875,7 @@ class MoodleImport
public function readQuizModule($moduleXml) public function readQuizModule($moduleXml)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($moduleXml); $moduleRes = self::loadXml($moduleDoc, $moduleXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return false; return false;
} }
@@ -1895,7 +1919,7 @@ class MoodleImport
public function readQuizModuleV4($moduleXml) public function readQuizModuleV4($moduleXml)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
@$moduleDoc->loadXML($moduleXml); self::loadXml($moduleDoc, $moduleXml);
$quizData = []; $quizData = [];
$quizNodes = $moduleDoc->getElementsByTagName('quiz'); $quizNodes = $moduleDoc->getElementsByTagName('quiz');
@@ -1932,10 +1956,10 @@ class MoodleImport
public function readMainQuestionsXmlV4($questionsXml, $quizXml) public function readMainQuestionsXmlV4($questionsXml, $quizXml)
{ {
$questionsDoc = new DOMDocument(); $questionsDoc = new DOMDocument();
@$questionsDoc->loadXML($questionsXml); self::loadXml($questionsDoc, $questionsXml);
$quizDoc = new DOMDocument(); $quizDoc = new DOMDocument();
@$quizDoc->loadXML($quizXml); self::loadXml($quizDoc, $quizXml);
$isByCategory = $quizDoc->getElementsByTagName('question_set_reference')->length > 0; $isByCategory = $quizDoc->getElementsByTagName('question_set_reference')->length > 0;
$isByBankReference = $quizDoc->getElementsByTagName('question_reference')->length > 0; $isByBankReference = $quizDoc->getElementsByTagName('question_reference')->length > 0;
@@ -1960,7 +1984,7 @@ class MoodleImport
public function readFolderModuleFilesXml($filesXml, $contextId = null) public function readFolderModuleFilesXml($filesXml, $contextId = null)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($filesXml); $moduleRes = self::loadXml($moduleDoc, $filesXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return false; return false;
@@ -2005,7 +2029,7 @@ class MoodleImport
public function readMainFilesXml($filesXml, $contextId) public function readMainFilesXml($filesXml, $contextId)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($filesXml); $moduleRes = self::loadXml($moduleDoc, $filesXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return false; return false;
@@ -2048,7 +2072,7 @@ class MoodleImport
public function readMainQuestionsXml($questionsXml, $questionId) public function readMainQuestionsXml($questionsXml, $questionId)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($questionsXml); $moduleRes = self::loadXml($moduleDoc, $questionsXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return false; return false;
} }
@@ -2610,7 +2634,7 @@ class MoodleImport
public function getAllQuestionFiles($filesXml) public function getAllQuestionFiles($filesXml)
{ {
$moduleDoc = new DOMDocument(); $moduleDoc = new DOMDocument();
$moduleRes = @$moduleDoc->loadXML($filesXml); $moduleRes = self::loadXml($moduleDoc, $filesXml);
if (empty($moduleRes)) { if (empty($moduleRes)) {
return []; return [];
-6
View File
@@ -10355,12 +10355,6 @@ function api_set_noreply_and_from_address_to_mailer(PHPMailer $mailer, array $se
$senderName = !empty($sender['name']) ? $sender['name'] : $notification->getDefaultPlatformSenderName(); $senderName = !empty($sender['name']) ? $sender['name'] : $notification->getDefaultPlatformSenderName();
$senderEmail = !empty($sender['email']) ? $sender['email'] : $notification->getDefaultPlatformSenderEmail(); $senderEmail = !empty($sender['email']) ? $sender['email'] : $notification->getDefaultPlatformSenderEmail();
// Send errors to the platform admin
$adminEmail = api_get_setting('emailAdministrator');
if (PHPMailer::ValidateAddress($adminEmail)) {
$mailer->AddCustomHeader('Errors-To: '.$adminEmail);
}
// Reply to first // Reply to first
if (!$avoidReplyToAddress) { if (!$avoidReplyToAddress) {
if ( if (
+1 -1
View File
@@ -52,7 +52,7 @@ class ChamiloSession extends System\Session
*/ */
public static function session_lifetime() public static function session_lifetime()
{ {
return api_get_configuration_value('session_lifetime'); return (int) api_get_configuration_value('session_lifetime');
} }
/** /**
+4 -2
View File
@@ -2489,15 +2489,17 @@ class GroupManager
} }
} }
$hasCheckbox = api_is_allowed_to_edit(false, true) && count($group_list) > 1;
$defaultSortColumn = $hasCheckbox ? 1 : 0;
$table = new SortableTableFromArrayConfig( $table = new SortableTableFromArrayConfig(
$group_data, $group_data,
1, $defaultSortColumn,
20, 20,
'group_category_'.$category_id 'group_category_'.$category_id
); );
$table->set_additional_parameters(['category' => $category_id]); $table->set_additional_parameters(['category' => $category_id]);
$column = 0; $column = 0;
if (api_is_allowed_to_edit(false, true) && count($group_list) > 1) { if ($hasCheckbox) {
$table->set_header($column++, '', false); $table->set_header($column++, '', false);
} }
$table->set_header($column++, get_lang('Groups')); $table->set_header($column++, get_lang('Groups'));
+17
View File
@@ -2,6 +2,7 @@
/* For licensing terms, see /license.txt */ /* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Component\Http\SafeHttp;
use Chamilo\CourseBundle\Entity\CLink; use Chamilo\CourseBundle\Entity\CLink;
use GuzzleHttp\Client; use GuzzleHttp\Client;
@@ -1816,6 +1817,21 @@ class Link extends Model
public static function checkUrl(string $url): bool public static function checkUrl(string $url): bool
{ {
// SSRF guard (CWE-918): reject targets that resolve to a
// loopback/private/reserved/link-local address (incl. the cloud
// metadata endpoint) or use a non-http(s) scheme before any request.
if (null === SafeHttp::resolveSafeIp($url)) {
return false;
}
// Re-validate every redirect hop so a 30x cannot bounce the request to
// an internal host after the initial check passed.
$onRedirect = function ($request, $response, $uri) {
if (null === SafeHttp::resolveSafeIp((string) $uri)) {
throw new \RuntimeException("Blocked redirect to a non-public host.");
}
};
$defaults = [ $defaults = [
'allow_redirects' => [ 'allow_redirects' => [
'max' => 5, // max number of redirects allowed 'max' => 5, // max number of redirects allowed
@@ -1823,6 +1839,7 @@ class Link extends Model
'referer' => true, // whether to add the Referer header when redirecting 'referer' => true, // whether to add the Referer header when redirecting
'protocols' => ['http', 'https'], // protocols allowed to be redirected to 'protocols' => ['http', 'https'], // protocols allowed to be redirected to
'track_redirects' => true, // whether to keep track of the number of redirects 'track_redirects' => true, // whether to keep track of the number of redirects
'on_redirect' => $onRedirect,
], ],
'connect_timeout' => 4, 'connect_timeout' => 4,
'timeout' => 4, 'timeout' => 4,
+5 -5
View File
@@ -2906,11 +2906,11 @@ class MessageManager
if (api_get_configuration_value('mail_template_system') == true) { if (api_get_configuration_value('mail_template_system') == true) {
$mailTemplateManager = new MailTemplateManager(); $mailTemplateManager = new MailTemplateManager();
$templateText = $mailTemplateManager->getTemplateByType('new_user_mail_to_admin_approval.tpl'); $templateText = $mailTemplateManager->getTemplateByType('new_user_mail_to_admin_approval.tpl');
if (empty($templateText)) { if (!empty($templateText)) {
} else { // Stored mail templates are admin-edited and therefore untrusted: render
// custom procedure to load a template as a string (doesn't use cache so may slow down) // them through a sandboxed Twig environment instead of compiling the raw
$template = $tplMailBody->twig->createTemplate($templateText); // string with the full application Twig (which would allow SSTI → RCE).
$emailbody = $template->render($tplMailBody->params); $emailbody = MailTemplateManager::renderSandboxedTemplate($templateText, $tplMailBody->params);
} }
} }
if (empty($emailbody)) { if (empty($emailbody)) {
File diff suppressed because it is too large Load Diff
+48 -26
View File
@@ -44,46 +44,63 @@ class ResourceExport extends ActivityExport
*/ */
public function getData(int $resourceId, int $sectionId, ?int $moduleId = null): array public function getData(int $resourceId, int $sectionId, ?int $moduleId = null): array
{ {
if (empty($this->course->resources[RESOURCE_DOCUMENT][$resourceId])) { $docBucket = $this->course->resources[\defined('RESOURCE_DOCUMENT') ? RESOURCE_DOCUMENT : 'document']
return []; ?? $this->course->resources['document']
?? [];
$resource = $docBucket[$resourceId] ?? null;
if (null === $resource) {
return [
'id' => $resourceId,
'moduleid' => $resourceId,
'modulename' => 'resource',
'contextid' => 0,
'name' => 'Resource '.$resourceId,
'intro' => '',
'display' => 0,
'sectionid' => $sectionId,
'sectionnumber' => 1,
'timemodified' => time(),
'users' => [],
'files' => [],
];
} }
$resource = $this->course->resources[RESOURCE_DOCUMENT][$resourceId]; $documentPath = (string) ($resource->path ?? '');
$effectiveModuleId = (int) ($moduleId ?? ($resource->source_id ?? 0));
$name = (string) ($resource->title ?? '');
if ($sectionId > 0) {
$name = $this->lpItemTitle($sectionId, RESOURCE_DOCUMENT, $resourceId, $name);
}
$name = $this->sanitizeMoodleActivityName($name, 255);
$effectiveModuleId = (int) ($moduleId ?? $resource->source_id);
if ($effectiveModuleId <= 0) { if ($effectiveModuleId <= 0) {
$effectiveModuleId = (int) $resource->source_id; $effectiveModuleId = (int) ($resource->source_id ?? 0);
} }
$name = (string) ($resource->title ?? ('Resource '.$resourceId));
if ($sectionId > 0) {
$name = $this->lpItemTitle(
$sectionId,
\defined('RESOURCE_DOCUMENT') ? (string) RESOURCE_DOCUMENT : 'document',
$resourceId,
$name
);
}
$name = $this->sanitizeMoodleActivityName($name, 255);
$resourceFile = $this->buildResourceFileEntry($resource, $effectiveModuleId); $resourceFile = $this->buildResourceFileEntry($resource, $effectiveModuleId);
$introResult = $this->extractEmbeddedFilesAndNormalizeContent(
(string) ($resource->comment ?? ''),
$effectiveModuleId,
'mod_resource',
'intro',
0,
fn (int $sequence): int => $this->buildResourceIntroFileId($effectiveModuleId, $sequence)
);
return [ return [
'id' => $resourceId, 'id' => (int) ($resource->source_id ?? $resourceId),
'moduleid' => $effectiveModuleId, 'moduleid' => $effectiveModuleId,
'modulename' => 'resource', 'modulename' => 'resource',
'contextid' => $effectiveModuleId, 'contextid' => $effectiveModuleId,
'name' => $name, 'name' => $name,
'intro' => $introResult['content'], 'intro' => (string) ($resource->comment ?? ''),
'display' => $this->resolveResourceDisplay($documentPath),
'sectionid' => $sectionId, 'sectionid' => $sectionId,
'sectionnumber' => 1, 'sectionnumber' => 1,
'timemodified' => time(), 'timemodified' => time(),
'users' => [], 'users' => [],
'files' => array_merge([$resourceFile], $introResult['files']), 'files' => [$resourceFile],
]; ];
} }
@@ -222,19 +239,24 @@ class ResourceExport extends ActivityExport
$xmlContent .= '<activity id="'.$resourceData['id'].'" moduleid="'.$resourceData['moduleid'].'" modulename="resource" contextid="'.$resourceData['contextid'].'">'.PHP_EOL; $xmlContent .= '<activity id="'.$resourceData['id'].'" moduleid="'.$resourceData['moduleid'].'" modulename="resource" contextid="'.$resourceData['contextid'].'">'.PHP_EOL;
$xmlContent .= ' <resource id="'.$resourceData['id'].'">'.PHP_EOL; $xmlContent .= ' <resource id="'.$resourceData['id'].'">'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $resourceData['name']).'</name>'.PHP_EOL; $xmlContent .= ' <name>'.htmlspecialchars((string) $resourceData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro><![CDATA['.(string) $resourceData['intro'].']]></intro>'.PHP_EOL; $xmlContent .= ' <intro>'.htmlspecialchars((string) $resourceData['intro']).'</intro>'.PHP_EOL;
$xmlContent .= ' <introformat>1</introformat>'.PHP_EOL; $xmlContent .= ' <introformat>1</introformat>'.PHP_EOL;
$xmlContent .= ' <tobemigrated>0</tobemigrated>'.PHP_EOL; $xmlContent .= ' <tobemigrated>0</tobemigrated>'.PHP_EOL;
$xmlContent .= ' <legacyfiles>0</legacyfiles>'.PHP_EOL; $xmlContent .= ' <legacyfiles>0</legacyfiles>'.PHP_EOL;
$xmlContent .= ' <legacyfileslast>$@NULL@$</legacyfileslast>'.PHP_EOL; $xmlContent .= ' <legacyfileslast>$@NULL@$</legacyfileslast>'.PHP_EOL;
$xmlContent .= ' <display>0</display>'.PHP_EOL; $xmlContent .= ' <display>'.(int) ($resourceData['display'] ?? 0).'</display>'.PHP_EOL;
$xmlContent .= ' <displayoptions>a:1:{s:10:"printintro";i:1;}</displayoptions>'.PHP_EOL; $xmlContent .= ' <displayoptions>a:1:{s:10:"printintro";i:1;}</displayoptions>'.PHP_EOL;
$xmlContent .= ' <filterfiles>0</filterfiles>'.PHP_EOL; $xmlContent .= ' <filterfiles>0</filterfiles>'.PHP_EOL;
$xmlContent .= ' <revision>1</revision>'.PHP_EOL; $xmlContent .= ' <revision>1</revision>'.PHP_EOL;
$xmlContent .= ' <timemodified>'.$resourceData['timemodified'].'</timemodified>'.PHP_EOL; $xmlContent .= ' <timemodified>'.(int) $resourceData['timemodified'].'</timemodified>'.PHP_EOL;
$xmlContent .= ' </resource>'.PHP_EOL; $xmlContent .= ' </resource>'.PHP_EOL;
$xmlContent .= '</activity>'.PHP_EOL; $xmlContent .= '</activity>'.PHP_EOL;
$this->createXmlFile('resource', $xmlContent, $resourceDir); $this->createXmlFile('resource', $xmlContent, $resourceDir);
} }
private function resolveResourceDisplay(string $documentPath): int
{
return 'application/pdf' === $this->guessMimeType($documentPath) ? 1 : 0;
}
} }
+53 -45
View File
@@ -47,48 +47,48 @@ class UrlExport extends ActivityExport
/** /**
* Get URL data for the course. * Get URL data for the course.
*/ */
public function getData(int $activityId, int $sectionId, ?int $moduleId = null): ?array public function getData(int $activityId, int $sectionId): ?array
{ {
if (empty($this->course->resources['link'][$activityId])) { $url = $this->course->resources['link'][$activityId] ?? null;
return null;
if (null === $url) {
return [
'id' => $activityId,
'moduleid' => $activityId,
'modulename' => 'url',
'contextid' => (int) ($this->course->info['real_id'] ?? 0),
'name' => 'URL '.$activityId,
'description' => '',
'externalurl' => '',
'display' => 6,
'displayoptions' => $this->buildUrlDisplayOptions(),
'timecreated' => time(),
'timemodified' => time(),
'sectionid' => $sectionId,
'sectionnumber' => 0,
'users' => [],
'files' => [],
];
} }
$url = $this->course->resources['link'][$activityId]; $src = isset($url->obj) ? $url->obj : $url;
$effectiveModuleId = (int) ($moduleId ?? $activityId);
if ($effectiveModuleId <= 0) {
$effectiveModuleId = $activityId;
}
$name = (string) ($url->title ?? '');
if ($sectionId > 0) {
$name = $this->lpItemTitle($sectionId, RESOURCE_LINK, $activityId, $name);
}
$name = $this->sanitizeMoodleActivityName($name, 255);
$descriptionResult = $this->extractEmbeddedFilesAndNormalizeContent(
(string) ($url->description ?? ''),
$effectiveModuleId,
'mod_url',
'intro',
0,
fn (int $sequence): int => $this->buildUrlEmbeddedFileId($effectiveModuleId, $sequence)
);
return [ return [
'id' => $activityId, 'id' => (int) $activityId,
'moduleid' => $effectiveModuleId, 'moduleid' => (int) $activityId,
'modulename' => 'url', 'modulename' => 'url',
'contextid' => $effectiveModuleId, 'contextid' => (int) $this->course->info['real_id'],
'name' => $name, 'name' => (string) ($src->title ?: $src->url),
'description' => $descriptionResult['content'], 'description' => (string) ($src->description ?? ''),
'externalurl' => (string) ($url->url ?? ''), 'externalurl' => (string) $src->url,
'display' => 6,
'displayoptions' => $this->buildUrlDisplayOptions(),
'timecreated' => time(), 'timecreated' => time(),
'timemodified' => time(), 'timemodified' => time(),
'sectionid' => $sectionId, 'sectionid' => $sectionId,
'sectionnumber' => 0, 'sectionnumber' => 0,
'users' => [], 'users' => [],
'files' => $descriptionResult['files'], 'files' => [],
]; ];
} }
@@ -97,21 +97,21 @@ class UrlExport extends ActivityExport
*/ */
private function createUrlXml(array $urlData, string $urlDir): void private function createUrlXml(array $urlData, string $urlDir): void
{ {
$xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL; $xml = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
$xmlContent .= '<activity id="'.$urlData['id'].'" moduleid="'.$urlData['moduleid'].'" modulename="'.$urlData['modulename'].'" contextid="'.$urlData['contextid'].'">'.PHP_EOL; $xml .= '<activity id="'.$urlData['id'].'" moduleid="'.$urlData['moduleid'].'" modulename="url" contextid="'.$urlData['contextid'].'">'.PHP_EOL;
$xmlContent .= ' <url id="'.$urlData['id'].'">'.PHP_EOL; $xml .= ' <url id="'.$urlData['id'].'">'.PHP_EOL;
$xmlContent .= ' <name>'.htmlspecialchars((string) $urlData['name']).'</name>'.PHP_EOL; $xml .= ' <name>'.htmlspecialchars((string) $urlData['name']).'</name>'.PHP_EOL;
$xmlContent .= ' <intro><![CDATA['.(string) $urlData['description'].']]></intro>'.PHP_EOL; $xml .= ' <intro></intro>'.PHP_EOL;
$xmlContent .= ' <introformat>1</introformat>'.PHP_EOL; $xml .= ' <introformat>1</introformat>'.PHP_EOL;
$xmlContent .= ' <externalurl>'.htmlspecialchars((string) $urlData['externalurl']).'</externalurl>'.PHP_EOL; $xml .= ' <externalurl>'.htmlspecialchars((string) $urlData['externalurl']).'</externalurl>'.PHP_EOL;
$xmlContent .= ' <display>0</display>'.PHP_EOL; $xml .= ' <display>'.(int) ($urlData['display'] ?? 6).'</display>'.PHP_EOL;
$xmlContent .= ' <displayoptions>a:1:{s:10:"printintro";i:1;}</displayoptions>'.PHP_EOL; $xml .= ' <displayoptions>'.htmlspecialchars((string) ($urlData['displayoptions'] ?? 'a:0:{}')).'</displayoptions>'.PHP_EOL;
$xmlContent .= ' <parameters>a:0:{}</parameters>'.PHP_EOL; $xml .= ' <parameters>a:0:{}</parameters>'.PHP_EOL;
$xmlContent .= ' <timemodified>'.$urlData['timemodified'].'</timemodified>'.PHP_EOL; $xml .= ' <timemodified>'.(int) $urlData['timemodified'].'</timemodified>'.PHP_EOL;
$xmlContent .= ' </url>'.PHP_EOL; $xml .= ' </url>'.PHP_EOL;
$xmlContent .= '</activity>'; $xml .= '</activity>'.PHP_EOL;
$this->createXmlFile('url', $xmlContent, $urlDir); $this->createXmlFile('url', $xml, $urlDir);
} }
/** /**
@@ -121,4 +121,12 @@ class UrlExport extends ActivityExport
{ {
return 1200000000 + max(0, $moduleId) + max(1, $sequence); return 1200000000 + max(0, $moduleId) + max(1, $sequence);
} }
private function buildUrlDisplayOptions(): string
{
return serialize([
'popupwidth' => 1024,
'popupheight' => 768,
]);
}
} }
+6 -1
View File
@@ -1,6 +1,7 @@
<?php <?php
/* See license terms in /license.txt */ /* See license terms in /license.txt */
use Chamilo\CoreBundle\Component\Mpdf\SafeMpdfHttpClient;
use Chamilo\CoreBundle\Component\Utils\ChamiloApi; use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
use Mpdf\Mpdf; use Mpdf\Mpdf;
use Mpdf\MpdfException; use Mpdf\MpdfException;
@@ -78,7 +79,11 @@ class PDF
'margin_footer' => 8, 'margin_footer' => 8,
'orientation' => $orientation, 'orientation' => $orientation,
'tempDir' => api_get_path(SYS_ARCHIVE_PATH).'mpdf/', 'tempDir' => api_get_path(SYS_ARCHIVE_PATH).'mpdf/',
] ],
// Inject an SSRF-safe HTTP client so mPDF cannot fetch remote
// `<img src>`/CSS `url()` pointing to internal/private hosts or the
// cloud metadata endpoint when rendering user-supplied HTML.
SafeMpdfHttpClient::container()
); );
$this->pdf->margin_footer = $params['margin_footer']; $this->pdf->margin_footer = $params['margin_footer'];
@@ -256,11 +256,25 @@ class HTML_QuickForm_Renderer_ArraySmarty extends HTML_QuickForm_Renderer_Array
unset($elAry['keys']); unset($elAry['keys']);
// where should we put this element... // where should we put this element...
if (is_array($this->_currentGroup) && ('group' != $elAry['type'])) { if (is_array($this->_currentGroup) && ('group' != $elAry['type'])) {
$toEval = '$this->_currentGroup' . $sKeys . ' = $elAry;'; $ref = &$this->_currentGroup;
} else { } else {
$toEval = '$this->_ary' . $sKeys . ' = $elAry;'; $ref = &$this->_ary;
} }
eval($toEval); preg_match_all('/\[(?:\'((?:[^\'\\\\]|\\\\.)*)\')?\]/', $sKeys, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
if ('[]' === $match[0]) {
$ref[] = [];
end($ref);
$ref = &$ref[key($ref)];
} else {
$key = str_replace(['\\\\', "\\'"], ['\\', "'"], $match[1]);
if (!isset($ref[$key]) || !is_array($ref[$key])) {
$ref[$key] = [];
}
$ref = &$ref[$key];
}
}
$ref = $elAry;
} }
return; return;
} }
+36 -22
View File
@@ -413,13 +413,16 @@ class HTML_QuickForm_element extends HTML_Common
return false; return false;
} }
} }
$replacedName = str_replace( $parts = explode('[', $elementName);
array('\\', '\'', ']', '['), $ref = $values;
array('\\\\', '\\\'', '', "']['"), foreach ($parts as $i => $part) {
$elementName $key = 0 === $i ? $part : rtrim($part, ']');
); if (!isset($ref[$key])) {
$myVar = "['$replacedName']"; return null;
return eval("return (isset(\$values$myVar)) ? \$values$myVar : null;"); }
$ref = $ref[$key];
}
return $ref;
} else { } else {
return null; return null;
} }
@@ -531,22 +534,33 @@ class HTML_QuickForm_element extends HTML_Common
{ {
if (null === $value) { if (null === $value) {
return null; return null;
} elseif (!$assoc) {
return $value;
} else {
$name = $this->getName();
if (!strpos($name, '[')) {
return array($name => $value);
} else {
$valueAry = array();
$myIndex = "['" . str_replace(
array('\\', '\'', ']', '['), array('\\\\', '\\\'', '', "']['"),
$name
) . "']";
eval("\$valueAry$myIndex = \$value;");
return $valueAry;
}
} }
if (!$assoc) {
return $value;
}
$name = $this->getName();
if (!strpos($name, '[')) {
return array($name => $value);
}
$valueAry = [];
$parts = explode('[', $name);
$ref = &$valueAry;
foreach ($parts as $i => $part) {
$key = 0 === $i ? $part : rtrim($part, ']');
if (!isset($ref[$key]) || !is_array($ref[$key])) {
$ref[$key] = [];
}
$ref = &$ref[$key];
}
$ref = $value;
return $valueAry;
} }
/** /**
+40 -20
View File
@@ -223,27 +223,47 @@ class HTML_QuickForm_file extends HTML_QuickForm_input
$elementName = $this->getName(); $elementName = $this->getName();
if (isset($_FILES[$elementName])) { if (isset($_FILES[$elementName])) {
return $_FILES[$elementName]; return $_FILES[$elementName];
} elseif (false !== ($pos = strpos($elementName, '['))) {
$base = str_replace(
array('\\', '\''), array('\\\\', '\\\''),
substr($elementName, 0, $pos)
);
$idx = "['" . str_replace(
array('\\', '\'', ']', '['), array('\\\\', '\\\'', '', "']['"),
substr($elementName, $pos + 1, -1)
) . "']";
$props = array('name', 'type', 'size', 'tmp_name', 'error');
$code = "if (!isset(\$_FILES['{$base}']['name']{$idx})) {\n" .
" return null;\n" .
"} else {\n" .
" \$value = array();\n";
foreach ($props as $prop) {
$code .= " \$value['{$prop}'] = \$_FILES['{$base}']['{$prop}']{$idx};\n";
}
return eval($code . " return \$value;\n}\n");
} else {
return null;
} }
if (false !== ($pos = strpos($elementName, '['))) {
$base = substr($elementName, 0, $pos);
if (!isset($_FILES[$base])) {
return null;
}
$innerStr = substr($elementName, $pos + 1, -1);
$idxKeys = '' !== $innerStr ? explode('][', $innerStr) : [];
$nameEntry = $_FILES[$base]['name'] ?? null;
foreach ($idxKeys as $key) {
if (!is_array($nameEntry) || !array_key_exists($key, $nameEntry)) {
return null;
}
$nameEntry = $nameEntry[$key];
}
$props = array('name', 'type', 'size', 'tmp_name', 'error');
$value = [];
foreach ($props as $prop) {
$data = $_FILES[$base][$prop] ?? null;
foreach ($idxKeys as $key) {
if (!is_array($data) || !array_key_exists($key, $data)) {
$data = null;
break;
}
$data = $data[$key];
}
$value[$prop] = $data;
}
return $value;
}
return null;
} }
/** /**
+3
View File
@@ -436,6 +436,9 @@ class SortableTable extends HTML_Table
name="form_'.$this->table_name.'" name="form_'.$this->table_name.'"
class="form-search" class="form-search"
method="post" action="'.api_get_self().'?'.$params.'" >'; method="post" action="'.api_get_self().'?'.$params.'" >';
// CSRF token for the bulk-action POST. Kept in the request body only
// (not the action URL) to avoid leaking it through Referer/logs.
$html .= '<input type="hidden" name="sec_token" value="'.Security::get_existing_token().'" />';
} }
$html .= '<div class="table-responsive">'.$content.'</div>'; $html .= '<div class="table-responsive">'.$content.'</div>';
+102
View File
@@ -15,6 +15,69 @@ class SubLanguageManager
{ {
} }
/**
* Validate that a variable name is a safe PHP identifier.
*
* @param string $variableName The variable name to validate
*
* @return bool True if the variable name matches /^[a-zA-Z_][a-zA-Z0-9_]*$/
*/
public static function isValidLanguageVariable($variableName)
{
return !empty($variableName) && preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $variableName);
}
/**
* Validate that a language folder name is safe for filesystem use.
* Rejects path traversal sequences, slashes, null bytes, and non-alphanumeric
* characters other than underscores and hyphens.
*
* @param string $folderName The folder name to validate
*
* @return bool True if the folder name is safe
*/
public static function isValidLanguageFolderName($folderName)
{
if (empty($folderName)) {
return false;
}
if (strpos($folderName, "\0") !== false) {
return false;
}
if (strpos($folderName, '/') !== false ||
strpos($folderName, '\\') !== false ||
strpos($folderName, '..') !== false
) {
return false;
}
return (bool) preg_match('/^[a-zA-Z0-9_\-]+$/', $folderName);
}
/**
* Validate that a file path is within the language directory.
*
* @param string $path The path to validate
*
* @return bool True if the path is safely within SYS_LANG_PATH
*/
public static function isPathInsideLangDir($path)
{
$langDir = realpath(api_get_path(SYS_LANG_PATH));
if ($langDir === false) {
return false;
}
$realPath = realpath(dirname($path));
if ($realPath === false) {
return false;
}
return strpos($realPath, $langDir) === 0;
}
/** /**
* Get all the languages. * Get all the languages.
* *
@@ -124,6 +187,13 @@ class SubLanguageManager
*/ */
public static function get_all_language_variable_in_file(string $system_path_file): array public static function get_all_language_variable_in_file(string $system_path_file): array
{ {
// Validate the file is inside the lang directory before including it
$langDir = realpath(api_get_path(SYS_LANG_PATH));
$realFile = realpath($system_path_file);
if ($langDir === false || $realFile === false || strpos($realFile, $langDir) !== 0) {
return [];
}
ob_start(); ob_start();
include $system_path_file; include $system_path_file;
@@ -135,6 +205,8 @@ class SubLanguageManager
unset($variables['system_path_file']); unset($variables['system_path_file']);
unset($variables['get_as_string_index']); unset($variables['get_as_string_index']);
unset($variables['php_errormsg']); unset($variables['php_errormsg']);
unset($variables['langDir']);
unset($variables['realFile']);
return $variables; return $variables;
} }
@@ -148,6 +220,14 @@ class SubLanguageManager
*/ */
public static function add_file_in_language_directory($system_path_file) public static function add_file_in_language_directory($system_path_file)
{ {
// Validate the target path is inside the lang directory
// Use dirname check since the file itself may not exist yet
$langDir = realpath(api_get_path(SYS_LANG_PATH));
$parentDir = realpath(dirname($system_path_file));
if ($langDir === false || $parentDir === false || strpos($parentDir, $langDir) !== 0) {
return false;
}
$return_value = @file_put_contents($system_path_file, '<?php'.PHP_EOL); $return_value = @file_put_contents($system_path_file, '<?php'.PHP_EOL);
return $return_value; return $return_value;
@@ -164,10 +244,22 @@ class SubLanguageManager
*/ */
public static function write_data_in_file($path_file, $new_term, $new_variable) public static function write_data_in_file($path_file, $new_term, $new_variable)
{ {
// Validate variable name is a safe PHP identifier
if (!self::isValidLanguageVariable($new_variable)) {
return false;
}
// Validate the target path is inside the lang directory
if (!self::isPathInsideLangDir($path_file)) {
return false;
}
// Replace double quotes to avoid parse errors // Replace double quotes to avoid parse errors
$new_term = addcslashes($new_term, "\$\"\\"); $new_term = addcslashes($new_term, "\$\"\\");
// Replace new line signs to avoid parse errors - see #6773 // Replace new line signs to avoid parse errors - see #6773
$new_term = str_replace("\n", "\\n", $new_term); $new_term = str_replace("\n", "\\n", $new_term);
// Strip null bytes
$new_term = str_replace("\0", "", $new_term);
$return_value = false; $return_value = false;
$new_data = '$'.$new_variable.'="'.$new_term.'";'.PHP_EOL; $new_data = '$'.$new_variable.'="'.$new_term.'";'.PHP_EOL;
@@ -192,6 +284,11 @@ class SubLanguageManager
if (empty($sub_language_dir)) { if (empty($sub_language_dir)) {
return false; return false;
} }
if (!self::isValidLanguageFolderName($sub_language_dir)) {
return false;
}
$dir = api_get_path(SYS_LANG_PATH).$sub_language_dir; $dir = api_get_path(SYS_LANG_PATH).$sub_language_dir;
if (is_dir($dir)) { if (is_dir($dir)) {
return true; return true;
@@ -249,6 +346,11 @@ class SubLanguageManager
if (empty($sub_language_dir)) { if (empty($sub_language_dir)) {
return false; return false;
} }
if (!self::isValidLanguageFolderName($sub_language_dir)) {
return false;
}
$dir = api_get_path(SYS_LANG_PATH).$sub_language_dir; $dir = api_get_path(SYS_LANG_PATH).$sub_language_dir;
if (!is_dir($dir)) { if (!is_dir($dir)) {
return true; return true;
+11 -6
View File
@@ -7887,6 +7887,11 @@ class Tracking
$debug = false $debug = false
) { ) {
// Begin with the import process // Begin with the import process
if (empty($course_info)) {
echo Display::return_message(get_lang('CourseNotFound'), 'error');
return;
}
$origin_course_code = $course_info['code']; $origin_course_code = $course_info['code'];
$course_id = $course_info['real_id']; $course_id = $course_info['real_id'];
$user_id = (int) $user_id; $user_id = (int) $user_id;
@@ -8075,7 +8080,7 @@ class Tracking
// 4c. track_e_access_complete // 4c. track_e_access_complete
$sql = "SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'track_e_access_complete'"; $sql = "SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'track_e_access_complete'";
$res = Database::query($sql); $res = Database::query($sql);
$row = Database::fetch_row($result); $row = Database::fetch_row($res);
if ($row[0] > 0) { if ($row[0] > 0) {
$sql = "SELECT id FROM $TBL_TRACK_E_ACCESS_COMPLETE $sql = "SELECT id FROM $TBL_TRACK_E_ACCESS_COMPLETE
WHERE WHERE
@@ -8186,8 +8191,8 @@ class Tracking
$origin_session_id $origin_session_id
); );
$result_message['LP_VIEW'][$data['lp_id']] = [ $result_message['LP_VIEW'][$data['lp_id']] = [
'score' => $score, 'score' => is_null($score) ? 0 : $score,
'progress' => $progress, 'progress' => ($progress === false || is_null($progress)) ? 0 : $progress,
]; ];
} }
} }
@@ -8227,8 +8232,8 @@ class Tracking
$new_session_id $new_session_id
); );
$result_message_compare['LP_VIEW'][$data['lp_id']] = [ $result_message_compare['LP_VIEW'][$data['lp_id']] = [
'score' => $score, 'score' => is_null($score) ? 0 : $score,
'progress' => $progress, 'progress' => ($progress === false || is_null($progress)) ? 0 : $progress,
]; ];
} }
} }
@@ -8659,7 +8664,7 @@ class Tracking
WHERE login_id = $iIdLastConnection"; WHERE login_id = $iIdLastConnection";
$qLogoutDate = Database::query($sql); $qLogoutDate = Database::query($sql);
$resLogoutDate = convert_sql_date(Database::result($qLogoutDate, 0, 'logout_date')); $resLogoutDate = convert_sql_date(Database::result($qLogoutDate, 0, 'logout_date'));
$lifeTime = api_get_configuration_value('session_lifetime'); $lifeTime = (int) api_get_configuration_value('session_lifetime');
if ($resLogoutDate < time() - $lifeTime) { if ($resLogoutDate < time() - $lifeTime) {
// it isn't, we should create a fresh entry // it isn't, we should create a fresh entry
+15
View File
@@ -305,6 +305,21 @@ class UserManager
$emailTemplate = [], $emailTemplate = [],
$redirectToURLAfterLogin = '' $redirectToURLAfterLogin = ''
) { ) {
// Defense in depth against privilege mass-assignment (CWE-915):
// when the effective requester is anonymous AND no creator was supplied
// (the self-registration path), never allow elevated platform roles to
// be assigned. Only STUDENT and COURSEMANAGER are acceptable for
// self-service flows. Trusted server-to-server callers that pass an
// explicit $creatorId (e.g. the secret-key SOAP WSCreateUsers service,
// which runs without a session) are intentionally exempt.
if (empty($creatorId)
&& api_is_anonymous()
&& !in_array((int) $status, [STUDENT, COURSEMANAGER], true)
) {
$status = STUDENT;
$isAdmin = false;
}
$creatorId = empty($creatorId) ? api_get_user_id() : 0; $creatorId = empty($creatorId) ? api_get_user_id() : 0;
$creatorInfo = api_get_user_info($creatorId); $creatorInfo = api_get_user_info($creatorId);
$creatorEmail = isset($creatorInfo['email']) ? $creatorInfo['email'] : ''; $creatorEmail = isset($creatorInfo['email']) ? $creatorInfo['email'] : '';
+200
View File
@@ -112,13 +112,17 @@ class Rest extends WebService
public const GET_COURSES = 'get_courses'; public const GET_COURSES = 'get_courses';
public const GET_COURSES_FROM_EXTRA_FIELD = 'get_courses_from_extra_field'; public const GET_COURSES_FROM_EXTRA_FIELD = 'get_courses_from_extra_field';
public const SAVE_COURSE = 'save_course'; public const SAVE_COURSE = 'save_course';
public const UPDATE_COURSE = 'update_course';
public const DELETE_COURSE = 'delete_course'; public const DELETE_COURSE = 'delete_course';
public const GET_SESSION_FROM_EXTRA_FIELD = 'get_session_from_extra_field'; public const GET_SESSION_FROM_EXTRA_FIELD = 'get_session_from_extra_field';
public const GET_SESSION_INFO = 'session_info';
public const GET_SESSION_INFO_FROM_EXTRA_FIELD = 'get_session_info_from_extra_field'; public const GET_SESSION_INFO_FROM_EXTRA_FIELD = 'get_session_info_from_extra_field';
public const SAVE_SESSION = 'save_session'; public const SAVE_SESSION = 'save_session';
public const CREATE_SESSION_FROM_MODEL = 'create_session_from_model'; public const CREATE_SESSION_FROM_MODEL = 'create_session_from_model';
public const UPDATE_SESSION = 'update_session'; public const UPDATE_SESSION = 'update_session';
public const DELETE_SESSION = 'delete_session';
public const GET_SESSIONS = 'get_sessions'; public const GET_SESSIONS = 'get_sessions';
public const GET_COURSE_SESSIONS = 'get_course_sessions';
public const SUBSCRIBE_USER_TO_COURSE = 'subscribe_user_to_course'; public const SUBSCRIBE_USER_TO_COURSE = 'subscribe_user_to_course';
public const SUBSCRIBE_USER_TO_COURSE_PASSWORD = 'subscribe_user_to_course_password'; public const SUBSCRIBE_USER_TO_COURSE_PASSWORD = 'subscribe_user_to_course_password';
@@ -1760,6 +1764,40 @@ class Rest extends WebService
return $shortList; return $shortList;
} }
public function getSessionsByCourse(): array
{
self::protectAdminEndpoint();
$sessions = SessionManager::get_session_by_course($this->course->getId());
$extraFieldValues = new ExtraFieldValue('session');
$filter = ['variable', 'value', 'display_text'];
$list = [];
foreach ($sessions as $sessionInfo) {
$session = api_get_session_info($sessionInfo['id']);
if (empty($session)) {
continue;
}
$extraFields = $extraFieldValues->getAllValuesByItem($session['id']);
$list[] = [
'id' => $session['id'],
'name' => $session['name'],
'access_start_date' => $session['access_start_date'],
'access_end_date' => $session['access_end_date'],
'extra_fields' => array_map(
function ($item) use ($filter) {
return array_intersect_key($item, array_flip($filter));
},
$extraFields
),
];
}
return $list;
}
/** /**
* Returns an array of groups with id, group_type, name, description, visibility. * Returns an array of groups with id, group_type, name, description, visibility.
* *
@@ -1913,6 +1951,118 @@ class Rest extends WebService
return $results; return $results;
} }
/**
* @throws Exception
*/
public function updateCourse(ParameterBag $request): array
{
self::protectAdminEndpoint();
$courseId = $request->getInt('course_id');
$courseCode = $request->get('course_code');
if (!empty($courseCode)) {
$courseInfo = api_get_course_info($courseCode);
} elseif (!empty($courseId)) {
$courseInfo = api_get_course_info_by_id($courseId);
} else {
throw new Exception(get_lang('NoData'));
}
if (empty($courseInfo)) {
throw new Exception(get_lang('NoCourse'));
}
$realId = $courseInfo['real_id'];
$table = Database::get_main_table(TABLE_MAIN_COURSE);
$params = [];
$title = $request->get('title');
if (!is_null($title)) {
$params['title'] = $title;
}
$language = $request->get('language');
if (!is_null($language)) {
$params['course_language'] = $language;
}
$visibility = $request->get('visibility');
if (!is_null($visibility)) {
if (!isset(Course::getStatusList()[(int) $visibility])) {
throw new Exception(get_lang('VisibilityCannotBeChanged'));
}
$params['visibility'] = (int) $visibility;
}
$diskQuota = $request->get('disk_quota');
if (!is_null($diskQuota)) {
$params['disk_quota'] = (int) $diskQuota;
}
$categoryCode = $request->get('category_code');
if (!is_null($categoryCode)) {
$params['category_code'] = $categoryCode;
}
$departmentName = $request->get('department_name');
if (!is_null($departmentName)) {
$params['department_name'] = $departmentName;
}
$departmentUrl = $request->get('department_url');
if (!is_null($departmentUrl)) {
$params['department_url'] = $departmentUrl;
}
$subscribe = $request->get('subscribe');
if (!is_null($subscribe)) {
$params['subscribe'] = (int) $subscribe;
}
$unsubscribe = $request->get('unsubscribe');
if (!is_null($unsubscribe)) {
$params['unsubscribe'] = (int) $unsubscribe;
}
if (!empty($params)) {
Database::update($table, $params, ['id = ?' => $realId]);
}
$extraFields = array_filter(
$request->all(),
function ($key) {
return substr($key, 0, 6) === 'extra_';
},
ARRAY_FILTER_USE_KEY
);
if (!empty($extraFields)) {
$extraFields['item_id'] = $realId;
$courseFieldValue = new ExtraFieldValue('course');
$courseFieldValue->saveFieldValues($extraFields);
}
$updatedCourse = api_get_course_info_by_id($realId);
return [
'message' => get_lang('Updated'),
'id' => $realId,
'course_code' => $updatedCourse['code'],
'course_title' => $updatedCourse['title'],
];
}
/** /**
* @param $userParam * @param $userParam
* *
@@ -2599,6 +2749,33 @@ class Rest extends WebService
return (int) $sessionIdList[0]['item_id']; return (int) $sessionIdList[0]['item_id'];
} }
public function getSessionInfo(): array
{
self::protectAdminEndpoint();
$bundle = [
'id' => $this->session->getId(),
'name' => $this->session->getName(),
'access_start_date' => $this->session->getAccessStartDate()->format('Y-m-d H:i:s'),
'access_end_date' => $this->session->getAccessEndDate()
? $this->session->getAccessEndDate()->format('Y-m-d H:i:s')
: null,
];
$extraFieldValues = new ExtraFieldValue('session');
$extraFields = $extraFieldValues->getAllValuesByItem($this->session->getId());
// Only return these properties for each extra_field (the rest is not relevant to a webservice)
$filter = ['variable', 'value', 'display_text'];
$bundle['extra_fields'] = array_map(
function ($item) use ($filter) {
return array_intersect_key($item, array_flip($filter));
},
$extraFields
);
// return session details, including extra fields that have filter=1
return $bundle;
}
/** /**
* Finds the session which has a specific value in a specific extra field and return its details. * Finds the session which has a specific value in a specific extra field and return its details.
* *
@@ -3140,6 +3317,29 @@ class Rest extends WebService
]; ];
} }
/**
* @throws Exception
*/
public function deleteSession(int $sessionId): array
{
if (!SessionManager::cantEditSession($sessionId)) {
self::throwNotAllowedException();
}
$sessionInfo = api_get_session_info($sessionId);
if (empty($sessionInfo)) {
throw new Exception(get_lang('NoData'));
}
$result = SessionManager::delete($sessionId);
return [
'status' => $result,
'message' => $result ? get_lang('Deleted').': '.$sessionInfo['name'] : get_lang('Error'),
];
}
public function checkConditionalLogin(): bool public function checkConditionalLogin(): bool
{ {
$file = api_get_path(SYS_CODE_PATH).'auth/conditional_login/conditional_login.php'; $file = api_get_path(SYS_CODE_PATH).'auth/conditional_login/conditional_login.php';
+1 -1
View File
@@ -13,7 +13,7 @@
/** /**
* Variables used from the main/install/index.php. * Variables used from the main/install/index.php.
*/ */
$new_version = '1.11.38'; $new_version = '1.11.40';
$new_version_status = 'stable'; $new_version_status = 'stable';
$new_version_last_id = 0; $new_version_last_id = 0;
$new_version_stable = true; $new_version_stable = true;
File diff suppressed because one or more lines are too long
+42 -33
View File
@@ -2,6 +2,7 @@
/* For licensing terms, see /license.txt */ /* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Component\Http\SafeHttp;
use Chamilo\CoreBundle\Entity\Repository\CourseRepository; use Chamilo\CoreBundle\Entity\Repository\CourseRepository;
use Chamilo\CoreBundle\Entity\Repository\ItemPropertyRepository; use Chamilo\CoreBundle\Entity\Repository\ItemPropertyRepository;
use Chamilo\CourseBundle\Component\CourseCopy\CourseArchiver; use Chamilo\CourseBundle\Component\CourseCopy\CourseArchiver;
@@ -13443,43 +13444,51 @@ EOD;
if ($protocolFixApplied == false) { if ($protocolFixApplied == false) {
if (strpos(api_get_path(WEB_PATH), $host) === false) { if (strpos(api_get_path(WEB_PATH), $host) === false) {
// Check X-Frame-Options // Check X-Frame-Options through an SSRF guard (CWE-918): only
$ch = curl_init(); // probe public http(s) hosts. A target resolving to a
$options = [ // loopback/private/reserved/link-local address (incl. the cloud
CURLOPT_URL => $src, // metadata endpoint) is left untouched, exactly as the legacy
CURLOPT_RETURNTRANSFER => true, // path behaved when the request failed.
CURLOPT_HEADER => true, $safeIp = SafeHttp::resolveSafeIp($src);
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => "",
CURLOPT_AUTOREFERER => true,
CURLOPT_CONNECTTIMEOUT => 120,
CURLOPT_TIMEOUT => 120,
CURLOPT_MAXREDIRS => 10,
];
$proxySettings = api_get_configuration_value('proxy_settings'); if (null !== $safeIp) {
if (!empty($proxySettings) && $ch = curl_init();
isset($proxySettings['curl_setopt_array']) $options = [
) { CURLOPT_URL => $src,
$options[CURLOPT_PROXY] = $proxySettings['curl_setopt_array']['CURLOPT_PROXY']; CURLOPT_RETURNTRANSFER => true,
$options[CURLOPT_PROXYPORT] = $proxySettings['curl_setopt_array']['CURLOPT_PROXYPORT']; CURLOPT_HEADER => true,
} CURLOPT_ENCODING => "",
CURLOPT_CONNECTTIMEOUT => 120,
CURLOPT_TIMEOUT => 120,
];
// Forbid redirects/non-HTTP schemes and pin the validated IP
// to defeat DNS rebinding; restore TLS verification.
$options += SafeHttp::secureCurlOptions($src, $safeIp);
curl_setopt_array($ch, $options); $proxySettings = api_get_configuration_value('proxy_settings');
$response = curl_exec($ch); if (!empty($proxySettings) &&
$httpCode = curl_getinfo($ch); isset($proxySettings['curl_setopt_array'])
$headers = substr($response, 0, $httpCode['header_size']); ) {
$options[CURLOPT_PROXY] = $proxySettings['curl_setopt_array']['CURLOPT_PROXY'];
$options[CURLOPT_PROXYPORT] = $proxySettings['curl_setopt_array']['CURLOPT_PROXYPORT'];
}
$error = false; curl_setopt_array($ch, $options);
if (stripos($headers, 'X-Frame-Options: DENY') > -1 $response = curl_exec($ch);
//|| stripos($headers, 'X-Frame-Options: SAMEORIGIN') > -1 $httpCode = curl_getinfo($ch);
) { $headers = substr($response, 0, $httpCode['header_size']);
$error = true;
}
if ($error) { $error = false;
Session::write('x_frame_source', $src); if (stripos($headers, 'X-Frame-Options: DENY') > -1
$src = 'blank.php?error=x_frames_options'; //|| stripos($headers, 'X-Frame-Options: SAMEORIGIN') > -1
) {
$error = true;
}
if ($error) {
Session::write('x_frame_source', $src);
$src = 'blank.php?error=x_frames_options';
}
} }
} }
} }
+1 -1
View File
@@ -19,7 +19,7 @@ require_once __DIR__.'/lp_initialize_item.inc.php';
echo initialize_item( echo initialize_item(
$_POST['lid'], $_POST['lid'],
$_POST['uid'], api_get_user_id(),
$_POST['vid'], $_POST['vid'],
$_POST['iid'] $_POST['iid']
); );
+3 -1
View File
@@ -13,6 +13,8 @@
$use_anonymous = true; $use_anonymous = true;
require_once __DIR__.'/../inc/global.inc.php'; require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script();
/** /**
* Writes an item's new values into the database and returns the operation result. * Writes an item's new values into the database and returns the operation result.
* *
@@ -55,7 +57,7 @@ if (isset($_REQUEST['objectives'])) {
echo save_objectives( echo save_objectives(
$_REQUEST['lid'], $_REQUEST['lid'],
$_REQUEST['uid'], api_get_user_id(),
$_REQUEST['vid'], $_REQUEST['vid'],
$_REQUEST['iid'], $_REQUEST['iid'],
$objectives $objectives
+3 -1
View File
@@ -16,6 +16,8 @@ use ChamiloSession as Session;
$use_anonymous = true; $use_anonymous = true;
require_once __DIR__.'/../inc/global.inc.php'; require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script();
/** /**
* Get one item's details. * Get one item's details.
* *
@@ -286,7 +288,7 @@ function switch_item_details($lp_id, $user_id, $view_id, $current_item, $next_it
echo switch_item_details( echo switch_item_details(
$_REQUEST['lid'], $_REQUEST['lid'],
$_REQUEST['uid'], api_get_user_id(),
$_REQUEST['vid'], $_REQUEST['vid'],
$_REQUEST['iid'], $_REQUEST['iid'],
$_REQUEST['next'] $_REQUEST['next']
+1 -1
View File
@@ -56,7 +56,7 @@ class scormItem extends learnpathItem
case 'title': case 'title':
$tmp_children = $child->childNodes; $tmp_children = $child->childNodes;
if (1 == $tmp_children->length && '' != $child->firstChild->nodeValue) { if (1 == $tmp_children->length && '' != $child->firstChild->nodeValue) {
$this->title = $child->firstChild->nodeValue; $this->title = Security::remove_XSS($child->firstChild->nodeValue);
} }
break; break;
case 'max_score': case 'max_score':
+29 -33
View File
@@ -48,51 +48,47 @@ $allowToQualify = api_is_allowed_to_edit(null, true) ||
api_is_drh() || api_is_drh() ||
api_is_student_boss(); api_is_student_boss();
$allowedToTrackUser = // Access to a student's tracking is granted on two levels:
// 1) Wide scope: platform/session admins, HR managers (DRH) and student bosses
// may view any student's tracking data.
// 2) Teachers, coaches and course tutors may only view a student they have a
// real relationship with: a shared course (as teacher), a shared session (as
// coach), or a course they tutor where the student is enrolled. This closes
// the hole where any teacher could read any student's data regardless of
// enrollment.
$hasWideTrackingScope =
api_is_platform_admin(true, true) || api_is_platform_admin(true, true) ||
api_is_allowed_to_edit(null, true) ||
api_is_session_admin() || api_is_session_admin() ||
api_is_drh() || api_is_drh() ||
api_is_student_boss() || api_is_student_boss();
api_is_course_admin() ||
api_is_teacher()
;
$em = Database::getManager(); $em = Database::getManager();
$itemRepo = $em->getRepository(CItemProperty::class); $itemRepo = $em->getRepository(CItemProperty::class);
if (false === $allowedToTrackUser && !empty($courseInfo)) { $tracksThisStudent = false;
if (empty($sessionId)) { if (!$hasWideTrackingScope && !empty($student_id)) {
$isTeacher = CourseManager::is_course_teacher( $tracksThisStudent =
UserManager::isTeacherOfStudent(api_get_user_id(), $student_id) ||
Tracking::is_allowed_to_coach_student(api_get_user_id(), $student_id);
// A course tutor keeps access, but only to students enrolled in the course
// they tutor (not to any student, as the previous check allowed).
if (!$tracksThisStudent && !empty($courseInfo)) {
$isCourseTutor = 1 === (int) CourseManager::get_tutor_in_course_status(
api_get_user_id(), api_get_user_id(),
$courseInfo['code'] $courseInfo['real_id']
); );
$studentInCourse = CourseManager::is_user_subscribed_in_course(
if ($isTeacher) { $student_id,
$allowedToTrackUser = true; $courseInfo['code'],
} else { !empty($sessionId),
// Check if the user is tutor of the course $sessionId
$userCourseStatus = CourseManager::get_tutor_in_course_status( );
api_get_user_id(), $tracksThisStudent = $isCourseTutor && $studentInCourse;
$courseInfo['real_id']
);
if ($userCourseStatus == 1) {
$allowedToTrackUser = true;
}
}
} else {
$coach = api_is_coach($sessionId, $courseInfo['real_id']);
if ($coach) {
$allowedToTrackUser = true;
}
} }
} }
if (!$allowedToTrackUser) { if (empty($student_id) || (!$hasWideTrackingScope && !$tracksThisStudent)) {
api_not_allowed(true);
}
if (empty($student_id)) {
api_not_allowed(true); api_not_allowed(true);
} }
+1 -1
View File
@@ -28,7 +28,7 @@ $httpRequest = HttpRequest::createFromGlobals();
$action = $httpRequest->query->get('action', 'list'); $action = $httpRequest->query->get('action', 'list');
// It validates the management of categories will be only for admins // It validates the management of categories will be only for admins
if (in_array($action, ['list_categories', 'add_category', 'edit_category']) && !api_is_platform_admin()) { if (in_array($action, ['list_categories', 'add_category', 'edit_category', 'translate_category']) && !api_is_platform_admin()) {
api_not_allowed(true); api_not_allowed(true);
} }
+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\Course; use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\ExtraField; use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\Repository\SequenceResourceRepository; use Chamilo\CoreBundle\Entity\Repository\SequenceResourceRepository;
@@ -238,7 +239,11 @@ $redirectToSession = 1 == $coursesCount && $redirectToSession
$template->assign('redirect_to_session', $redirectToSession); $template->assign('redirect_to_session', $redirectToSession);
$template->assign('courses', $courses); $template->assign('courses', $courses);
$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( $template->assign(
'session_extra_fields', 'session_extra_fields',
+25
View File
@@ -802,6 +802,11 @@ try {
Event::addEvent(LOG_WS.$action, 'course_id', $data['id']); Event::addEvent(LOG_WS.$action, 'course_id', $data['id']);
$restResponse->setData($data); $restResponse->setData($data);
break; break;
case Rest::UPDATE_COURSE:
$data = $restApi->updateCourse($httpRequest->request);
Event::addEvent(LOG_WS.$action, 'course_id', $data['id']);
$restResponse->setData($data);
break;
case Rest::DELETE_COURSE: case Rest::DELETE_COURSE:
if (!api_is_platform_admin()) { if (!api_is_platform_admin()) {
throw new Exception(get_lang('NotAllowed')); throw new Exception(get_lang('NotAllowed'));
@@ -827,6 +832,13 @@ try {
Event::addEvent(LOG_WS.$action, 'course_id', $courseId); Event::addEvent(LOG_WS.$action, 'course_id', $courseId);
$restResponse->setData(['status' => $result]); $restResponse->setData(['status' => $result]);
break; break;
case Rest::GET_SESSION_INFO:
$sessionInfo = $restApi->getSessionInfo();
Event::addEvent(LOG_WS.$action, 'course_id', $sessionInfo['id']);
$restResponse->setData($sessionInfo);
break;
case Rest::GET_SESSION_FROM_EXTRA_FIELD: case Rest::GET_SESSION_FROM_EXTRA_FIELD:
$fieldName = trim($httpRequest->request->get('field_name')); $fieldName = trim($httpRequest->request->get('field_name'));
$fieldValue = trim($httpRequest->request->get('field_value')); $fieldValue = trim($httpRequest->request->get('field_value'));
@@ -870,6 +882,12 @@ try {
Event::addEvent(LOG_WS.$action, 'session_id', $data['id_session']); Event::addEvent(LOG_WS.$action, 'session_id', $data['id_session']);
$restResponse->setData($data); $restResponse->setData($data);
break; break;
case Rest::DELETE_SESSION:
$sessionId = (int) ($_REQUEST['session_id'] ?? 0);
$data = $restApi->deleteSession($sessionId);
Event::addEvent(LOG_WS.$action, 'session_id', $sessionId);
$restResponse->setData($data);
break;
case Rest::SUBSCRIBE_USER_TO_COURSE: case Rest::SUBSCRIBE_USER_TO_COURSE:
$data = $restApi->subscribeUserToCourse($_POST); $data = $restApi->subscribeUserToCourse($_POST);
Event::addEvent(LOG_WS.$action, 'course_id-user_id', (int) $_POST['course_id'].':'.(int) $_POST['user_id']); Event::addEvent(LOG_WS.$action, 'course_id-user_id', (int) $_POST['course_id'].':'.(int) $_POST['user_id']);
@@ -906,6 +924,13 @@ try {
$data = $restApi->getSessionsCampus($campusId, $getExtraFields); $data = $restApi->getSessionsCampus($campusId, $getExtraFields);
$restResponse->setData($data); $restResponse->setData($data);
break; break;
case Rest::GET_COURSE_SESSIONS:
Event::addEvent(LOG_WS.$action, 'course', $course);
$restResponse->setData(
$restApi->getSessionsByCourse()
);
break;
case Rest::ADD_COURSES_SESSION: case Rest::ADD_COURSES_SESSION:
$data = $restApi->addCoursesSession($_POST); $data = $restApi->addCoursesSession($_POST);
Event::addEvent( Event::addEvent(
+4 -1
View File
@@ -239,7 +239,10 @@ if (!empty($courses)) {
$url .= '&status='.(int) $status; $url .= '&status='.(int) $status;
} }
if (!empty($values['work_parent_ids'])) { if (!empty($values['work_parent_ids'])) {
$url .= '&work_parent_ids='.Security::remove_XSS(implode(',', $values['work_parent_ids'])); $values['work_parent_ids'] = array_filter(
array_map('intval', (array) $values['work_parent_ids'])
);
$url .= '&work_parent_ids='.implode(',', $values['work_parent_ids']);
} }
if ($exportXls) { if ($exportXls) {
exportPendingWorksToExcel($values); exportPendingWorksToExcel($values);
+7 -2
View File
@@ -6562,11 +6562,16 @@ function exportPendingWorksToExcel($values)
]; ];
$tableXls[] = $headers; $tableXls[] = $headers;
$courseId = $values['course'] ?? 0; $courseId = (int) ($values['course'] ?? 0);
$status = $values['status'] ?? 0; $status = $values['status'] ?? 0;
$whereCondition = ''; $whereCondition = '';
if (!empty($values['work_parent_ids'])) { if (!empty($values['work_parent_ids'])) {
$whereCondition = ' parent_id IN('.implode(',', $values['work_parent_ids']).')'; $workParentIds = array_filter(
array_map('intval', (array) $values['work_parent_ids'])
);
if (!empty($workParentIds)) {
$whereCondition = ' parent_id IN('.implode(',', $workParentIds).')';
}
} }
$allWork = getAllWork( $allWork = getAllWork(
null, null,
+11 -1
View File
@@ -9,6 +9,9 @@
* *
* @package chamilo.buycourses_plugin * @package chamilo.buycourses_plugin
*/ */
use Chamilo\CoreBundle\Component\Essence\SafeEssenceHttpClient;
$cidReset = true; $cidReset = true;
require_once '../../../main/inc/global.inc.php'; require_once '../../../main/inc/global.inc.php';
@@ -34,7 +37,14 @@ if (!$service['id']) {
$template = new Template(false); $template = new Template(false);
$template->assign('pageUrl', api_get_path(WEB_PATH)."service/{$serviceId}/information/"); $template->assign('pageUrl', api_get_path(WEB_PATH)."service/{$serviceId}/information/");
$template->assign('service', $service); $template->assign('service', $service);
$template->assign('essence', Essence\Essence::instance()); // SSRF protection: route Essence's server-side OEmbed/OpenGraph fetches
// through an IP-filtered HTTP client.
$template->assign(
'essence',
Essence\Essence::instance(
['Http' => new SafeEssenceHttpClient()]
)
);
$content = $template->fetch('buycourses/view/service_information.tpl'); $content = $template->fetch('buycourses/view/service_information.tpl');
+67 -6
View File
@@ -12,33 +12,94 @@ api_protect_admin_script();
$plugin = CleanDeletedFilesPlugin::create(); $plugin = CleanDeletedFilesPlugin::create();
$action = isset($_REQUEST['a']) ? $_REQUEST['a'] : null; $action = isset($_REQUEST['a']) ? $_REQUEST['a'] : null;
/**
* Resolves a client-supplied path to a safe, canonical file path.
*
* Only regular files whose name contains "DELETED" and that live inside the
* directories the plugin actually scans (app/courses, app/upload) are allowed.
* Returns the canonical path on success, or null when the path is not allowed.
*
* @param string|null $path
*
* @return string|null
*/
function cleanDeletedFilesResolvePath($path)
{
if (!is_string($path) || "" === $path) {
return null;
}
// The front-end may send the path prefixed with the file:// scheme.
if (0 === strpos($path, "file://")) {
$path = substr($path, 7);
}
$realPath = realpath($path);
if (false === $realPath || !is_file($realPath)) {
return null;
}
// Only files flagged as DELETED by Chamilo may be removed here.
if (false === strpos(basename($realPath), "DELETED")) {
return null;
}
$allowedDirs = [
realpath(api_get_path(SYS_PATH)."app/courses"),
realpath(api_get_path(SYS_PATH)."app/upload"),
];
foreach ($allowedDirs as $allowedDir) {
if (false !== $allowedDir
&& 0 === strpos($realPath, $allowedDir.DIRECTORY_SEPARATOR)
) {
return $realPath;
}
}
return null;
}
switch ($action) { switch ($action) {
case 'delete-file': case 'delete-file':
$path = isset($_REQUEST['path']) ? $_REQUEST['path'] : null; $path = $_REQUEST['path'] ?? null;
if (empty($path)) { if (empty($path)) {
echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorEmptyPath')]); echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorEmptyPath')]);
exit; exit;
} }
if (unlink($path)) { $realPath = cleanDeletedFilesResolvePath($path);
if (null === $realPath) {
echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorDeleteFile')]);
exit;
}
if (unlink($realPath)) {
Display::addFlash($plugin->get_lang("DeletedSuccess"), 'success'); Display::addFlash($plugin->get_lang("DeletedSuccess"), 'success');
echo json_encode(["status" => "true"]); echo json_encode(["status" => "true"]);
} else { } else {
echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorDeleteFile')]); echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorDeleteFile')]);
} }
break; break;
case 'delete-files-list': case 'delete-files-list':
$list = isset($_REQUEST['list']) ? $_REQUEST['list'] : []; $list = $_REQUEST['list'] ?? [];
if (empty($list)) { if (empty($list)) {
echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorEmptyPath')]); echo json_encode(["status" => "false", "message" => $plugin->get_lang('ErrorEmptyPath')]);
exit; exit;
} }
foreach ($list as $value) { foreach ($list as $value) {
if (empty($value)) { $realPath = cleanDeletedFilesResolvePath($value);
continue;
if (null !== $realPath) {
unlink($realPath);
} }
unlink($value);
} }
Display::addFlash($plugin->get_lang("DeletedSuccess"), 'success'); Display::addFlash($plugin->get_lang("DeletedSuccess"), 'success');
+13 -2
View File
@@ -2,6 +2,7 @@
/* For license terms, see /license.txt */ /* For license terms, see /license.txt */
use Chamilo\CoreBundle\Component\Http\SafeHttp;
use Chamilo\CoreBundle\Entity\Course; use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CTool; use Chamilo\CourseBundle\Entity\CTool;
@@ -466,15 +467,25 @@ class ImsLtiPlugin extends Plugin
*/ */
public function getLaunchUrlFromCartridge($configUrl) public function getLaunchUrlFromCartridge($configUrl)
{ {
// SSRF guard (CWE-918): only fetch public http(s) targets. Reject
// loopback/private/reserved/link-local hosts and the cloud metadata
// endpoint before issuing any request.
$safeIp = SafeHttp::resolveSafeIp($configUrl);
if (null === $safeIp) {
throw new Exception($this->get_lang('NoAccessToUrl'));
}
$options = [ $options = [
CURLOPT_CUSTOMREQUEST => 'GET', CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_POST => false, CURLOPT_POST => false,
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false, CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => '', CURLOPT_ENCODING => '',
CURLOPT_SSL_VERIFYPEER => false,
]; ];
// Forbid redirects/non-HTTP schemes and pin the validated IP to defeat
// DNS rebinding; restore TLS verification.
$options += SafeHttp::secureCurlOptions($configUrl, $safeIp);
$ch = curl_init($configUrl); $ch = curl_init($configUrl);
curl_setopt_array($ch, $options); curl_setopt_array($ch, $options);
@@ -19,7 +19,7 @@ class TrackCourseAccessLoader implements LoaderInterface
{ {
list($userId, $cId, $loginCourseDate, $ip, $sessionId) = array_values($incomingData); list($userId, $cId, $loginCourseDate, $ip, $sessionId) = array_values($incomingData);
$sessionLifetime = api_get_configuration_value('session_lifetime'); $sessionLifetime = (int) api_get_configuration_value('session_lifetime');
/** @var \DateTime $time */ /** @var \DateTime $time */
$time = clone $loginCourseDate; $time = clone $loginCourseDate;
+24 -1
View File
@@ -26,6 +26,8 @@
* @licence http://www.gnu.org/licenses/gpl.txt * @licence http://www.gnu.org/licenses/gpl.txt
*/ */
use Chamilo\CoreBundle\Component\Http\SafeHttp;
require_once __DIR__ . '/pens_controller.php'; require_once __DIR__ . '/pens_controller.php';
require_once __DIR__ . '/pens_package_handler.php'; require_once __DIR__ . '/pens_package_handler.php';
require_once __DIR__ . '/pens_exception.php'; require_once __DIR__ . '/pens_exception.php';
@@ -175,11 +177,22 @@ class PENSServer extends PENSController {
$tmp = "/tmp"; $tmp = "/tmp";
} }
$path_to_file = $tmp."/".$request->getFilename(); $path_to_file = $tmp."/".$request->getFilename();
// SSRF guard (CWE-918): block downloads from targets resolving to a
// loopback/private/reserved/link-local address (incl. the cloud metadata
// endpoint) or using a non-http(s) scheme, before opening any socket.
$packageUrl = $request->getPackageUrl();
$safeIp = SafeHttp::resolveSafeIp($packageUrl);
if (null === $safeIp) {
throw new PENSException(1310);
}
$fp = fopen($path_to_file, 'w'); $fp = fopen($path_to_file, 'w');
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $request->getPackageUrl()); curl_setopt($ch, CURLOPT_URL, $packageUrl);
curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt($ch, CURLOPT_FILE, $fp);
// Forbid redirects/non-HTTP schemes and pin the validated IP to defeat
// DNS rebinding; keep TLS verification on.
curl_setopt_array($ch, SafeHttp::secureCurlOptions($packageUrl, $safeIp));
if(!is_null($request->getPackageUrlUserId())) { if(!is_null($request->getPackageUrlUserId())) {
curl_setopt($ch, CURLOPT_USERPWD, $request->getPackageUrlUserId().":".$request->getPackageUrlPassword()); curl_setopt($ch, CURLOPT_USERPWD, $request->getPackageUrlUserId().":".$request->getPackageUrlPassword());
} }
@@ -263,10 +276,20 @@ class PENSServer extends PENSController {
} else { } else {
$params = array_merge($request->getSendReceiptArray(), $response->getArray()); $params = array_merge($request->getSendReceiptArray(), $response->getArray());
} }
// SSRF guard (CWE-918): only POST the alert/receipt callback
// to a public http(s) host; block loopback/private/reserved/
// link-local targets (incl. the cloud metadata endpoint).
$safeIp = SafeHttp::resolveSafeIp($url);
if (null === $safeIp) {
return null;
}
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params); curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Forbid redirects/non-HTTP schemes and pin the validated IP
// to defeat DNS rebinding; keep TLS verification on.
curl_setopt_array($ch, SafeHttp::secureCurlOptions($url, $safeIp));
$data = curl_exec($ch); $data = curl_exec($ch);
curl_close($ch); curl_close($ch);
if($data === false) { if($data === false) {
@@ -0,0 +1,62 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Component\Essence;
use Chamilo\CoreBundle\Component\Http\SafeHttp;
use Essence\Http\Client;
use Essence\Http\Exception as EssenceHttpException;
/**
* SSRF-safe HTTP client for the Essence library.
*
* Essence fetches OEmbed/OpenGraph URLs server-side (Essence::_extract ->
* Client::get) while rendering the course/session "about" pages from a
* teacher-set video URL. Its bundled cURL/native clients have no private-IP
* filtering and follow redirects, so the fetch could reach loopback/internal
* hosts or the cloud metadata endpoint (CWE-918, SSRF).
*
* Injecting this client via
* `Essence\Essence::instance(["Http" => new SafeEssenceHttpClient()])` routes
* every Essence fetch through SafeHttp: it refuses loopback/private/reserved/
* link-local targets, speaks only http(s), forbids redirects and pins the
* validated IP (CURLOPT_RESOLVE) to defeat DNS rebinding.
*/
class SafeEssenceHttpClient extends Client
{
/**
* {@inheritDoc}
*/
public function get($url)
{
$safeIp = SafeHttp::resolveSafeIp($url);
if (null === $safeIp) {
// Blocked or unresolvable target: surface Essence's own exception so
// the provider skips the embed instead of reaching internal content.
throw new EssenceHttpException($url, 403);
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_setopt($ch, CURLOPT_USERAGENT, $this->_userAgent);
curl_setopt_array($ch, SafeHttp::secureCurlOptions($url, $safeIp));
$contents = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$failed = false === $contents || curl_errno($ch);
if (PHP_VERSION_ID < 80000) {
curl_close($ch);
}
if ($failed) {
throw new EssenceHttpException($url, $httpCode);
}
return $contents;
}
}
@@ -17,6 +17,8 @@ class RemoveOnAttributes extends HTMLPurifier_Filter
public static function filter($html) public static function filter($html)
{ {
// Strip null bytes before regex matching to prevent bypass via on\x00load patterns
$html = str_replace("\0", '', $html);
$pattern = '/\s+on\w+\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i'; $pattern = '/\s+on\w+\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i';
return preg_replace($pattern, '', $html); return preg_replace($pattern, '', $html);
@@ -0,0 +1,152 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Component\Http;
/**
* Shared SSRF guard for every server-side fetch of a user-supplied URL.
*
* Chamilo issues outbound HTTP requests on behalf of users in several places
* (mPDF remote assets, the LTI cartridge auto-detect, ...). Without filtering,
* an attacker can point those fetches at loopback/private/reserved hosts or the
* cloud metadata endpoint (169.254.169.254) to scan the internal network or
* steal instance IAM credentials (CWE-918, SSRF).
*
* This class centralises the validation logic: resolve the host, reject any
* address inside a private/reserved/loopback/link-local range, and expose cURL
* options that pin the connection to the validated IP (defeating DNS rebinding)
* while forbidding non-HTTP schemes and redirects.
*
* The 1.11.x branch ships Symfony 3.4, which has no NoPrivateNetworkHttpClient,
* so the validation is implemented here directly.
*/
final class SafeHttp
{
/**
* Validates the URL and returns a single safe IP to connect to, or null if
* the target must be blocked.
*
* A target is blocked when: the scheme is not http(s), the host is missing,
* the host does not resolve, or ANY resolved address falls inside a
* private/reserved/loopback/link-local range (e.g. 127.0.0.0/8,
* 169.254.0.0/16, 10/8, ::1, fc00::/7).
*
* @param string $uri
*
* @return string|null
*/
public static function resolveSafeIp($uri)
{
$parts = parse_url($uri);
if (false === $parts || empty($parts["host"])) {
return null;
}
$scheme = strtolower(isset($parts["scheme"]) ? $parts["scheme"] : "");
if (!in_array($scheme, ["http", "https"], true)) {
return null;
}
// parse_url keeps IPv6 literals wrapped in brackets.
$host = trim($parts["host"], "[]");
$addresses = self::resolveHost($host);
if (empty($addresses)) {
return null;
}
$safeIp = null;
foreach ($addresses as $address) {
if (!self::isPublicIp($address)) {
// A single private/reserved hit blocks the whole request.
return null;
}
if (null === $safeIp) {
$safeIp = $address;
}
}
return $safeIp;
}
/**
* Returns the cURL options that harden an outbound request against SSRF:
* restrict the protocol to http(s), forbid following redirects (a 30x could
* bounce to an internal host after validation) and pin the connection to the
* already-validated IP to prevent a DNS-rebinding race.
*
* @param string $uri the validated URL
* @param string $ip the safe IP returned by resolveSafeIp()
*
* @return array
*/
public static function secureCurlOptions($uri, $ip)
{
$parts = parse_url($uri);
$host = trim($parts["host"] ?? "", "[]");
$scheme = strtolower($parts["scheme"] ?? "http");
$port = (int) ($parts["port"] ?? ("https" === $scheme ? 443 : 80));
$options = [
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_RESOLVE => ["$host:$port:$ip"],
];
if (defined("CURLOPT_PROTOCOLS") && defined("CURLPROTO_HTTP") && defined("CURLPROTO_HTTPS")) {
$options[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
$options[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
}
return $options;
}
/**
* Resolves a host (or returns it as-is if already an IP literal) into a
* list of IPv4/IPv6 addresses.
*
* @param string $host
*
* @return string[]
*/
public static function resolveHost($host)
{
if (filter_var($host, FILTER_VALIDATE_IP)) {
return [$host];
}
$addresses = [];
$ipv4 = @gethostbynamel($host);
if (is_array($ipv4)) {
$addresses = $ipv4;
}
$records = @dns_get_record($host, DNS_AAAA);
if (is_array($records)) {
foreach ($records as $record) {
if (!empty($record["ipv6"])) {
$addresses[] = $record["ipv6"];
}
}
}
return array_values(array_unique($addresses));
}
/**
* @param string $ip
*
* @return bool
*/
public static function isPublicIp($ip)
{
return false !== filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
);
}
}
@@ -0,0 +1,103 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Component\Mpdf;
use Chamilo\CoreBundle\Component\Http\SafeHttp;
use Mpdf\Container\SimpleContainer;
use Mpdf\Http\ClientInterface;
use Mpdf\PsrHttpMessageShim\Response;
use Mpdf\PsrHttpMessageShim\Stream;
use Psr\Http\Message\RequestInterface;
/**
* SSRF-safe HTTP client for mPDF.
*
* mPDF fetches every remote `<img src>` and CSS `url()` server-side while
* rendering. By default it uses its internal cURL/socket client with no IP
* filtering, which lets attacker-controlled wiki/document HTML reach the cloud
* metadata endpoint (169.254.169.254) or internal services (SSRF).
*
* This client is injected into mPDF through its experimental container (2nd
* constructor argument) and refuses any URL that resolves to a loopback,
* private, reserved or link-local address. The connection is pinned to the
* already-validated IP (CURLOPT_RESOLVE) and redirects are disabled so a remote
* 30x cannot bounce the request to an internal host.
*
* The 1.11.x branch ships Symfony 3.4, which has no NoPrivateNetworkHttpClient,
* so the validation is implemented here directly (the master branch relies on
* Symfony\Component\HttpClient\NoPrivateNetworkHttpClient instead).
*/
final class SafeMpdfHttpClient implements ClientInterface
{
/**
* Builds the mPDF container exposing this client as "httpClient".
*
* Usage: new \Mpdf\Mpdf($config, SafeMpdfHttpClient::container());
*/
public static function container(): SimpleContainer
{
return new SimpleContainer(["httpClient" => new self()]);
}
/**
* @return Response
*/
public function sendRequest(RequestInterface $request)
{
$response = new Response();
$uri = (string) $request->getUri();
$ip = SafeHttp::resolveSafeIp($uri);
if (null === $ip) {
// Blocked or unresolvable target: return an empty 403 so mPDF
// silently skips the asset instead of embedding internal content.
return $response->withStatus(403);
}
$parts = parse_url($uri);
$host = $parts["host"];
$scheme = strtolower($parts["scheme"] ?? "http");
$port = (int) ($parts["port"] ?? ("https" === $scheme ? 443 : 80));
$ch = curl_init($uri);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
// Never follow redirects: a 30x could point to an internal host that
// would bypass the validation performed above.
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
// Pin the connection to the IP we already validated to prevent a
// DNS-rebinding race between resolution and the actual request.
curl_setopt($ch, CURLOPT_RESOLVE, ["$host:$port:$ip"]);
$data = curl_exec($ch);
if (false === $data || curl_errno($ch)) {
self::closeCurl($ch);
return $response;
}
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
self::closeCurl($ch);
if ($httpCode < 200 || $httpCode >= 300) {
return $response->withStatus($httpCode ?: 502);
}
return $response
->withStatus($httpCode)
->withBody(Stream::create($data))
;
}
private static function closeCurl($ch): void
{
if (PHP_VERSION_ID < 80000) {
curl_close($ch);
}
}
}
@@ -288,6 +288,36 @@ class CourseRestorer
CourseManager::update_attributes($origin_course_info['real_id'], $params); CourseManager::update_attributes($origin_course_info['real_id'], $params);
} }
/**
* Validates a relative path coming from an (untrusted) course backup
* archive before it is used in file operations.
*
* Rejects non-strings, path traversal (`..`) and null bytes, and optionally
* enforces that the path stays within an expected sub-directory. This
* prevents a tampered backup from escaping the course directory and writing
* an arbitrary file (ZIP-slip -> arbitrary file write -> RCE).
*
* @param mixed $path
* @param string $requiredPrefix Sub-tree the path must start with (optional)
*
* @return bool True when the path is safe to use
*/
private function isSafeBackupPath($path, $requiredPrefix = '')
{
if (!is_string($path)
|| false !== strpos($path, '..')
|| false !== strpos($path, "\0")
) {
return false;
}
if ('' !== $requiredPrefix && 0 !== strpos($path, $requiredPrefix)) {
return false;
}
return true;
}
/** /**
* Restore documents. * Restore documents.
* *
@@ -312,6 +342,14 @@ class CourseRestorer
$path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/'; $path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/';
$originalFolderNameList = []; $originalFolderNameList = [];
foreach ($resources[RESOURCE_DOCUMENT] as $id => $document) { foreach ($resources[RESOURCE_DOCUMENT] as $id => $document) {
// Security: $document->path comes from the (possibly tampered)
// backup archive. Confine it to the "document" sub-tree and
// neutralize executable filenames to prevent arbitrary file write.
if (!$this->isSafeBackupPath($document->path, 'document')) {
continue;
}
$document->path = disable_dangerous_file($document->path);
$my_session_id = empty($document->item_properties[0]['session_id']) ? 0 : $session_id; $my_session_id = empty($document->item_properties[0]['session_id']) ? 0 : $session_id;
if (false === $respect_base_content && $session_id) { if (false === $respect_base_content && $session_id) {
@@ -1053,6 +1091,12 @@ class CourseRestorer
if ($this->course->has_resources(RESOURCE_SCORM)) { if ($this->course->has_resources(RESOURCE_SCORM)) {
$resources = $this->course->resources; $resources = $this->course->resources;
foreach ($resources[RESOURCE_SCORM] as $document) { foreach ($resources[RESOURCE_SCORM] as $document) {
// Security: confine the backup-supplied SCORM path to prevent a
// tampered backup from escaping the course directory (-> RCE).
if (!$this->isSafeBackupPath($document->path)) {
continue;
}
$path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/'; $path = api_get_path(SYS_COURSE_PATH).$this->course->destination_path.'/';
@mkdir(dirname($path.$document->path), $perm, true); @mkdir(dirname($path.$document->path), $perm, true);
if (file_exists($path.$document->path)) { if (file_exists($path.$document->path)) {
+12 -12
View File
@@ -57,7 +57,7 @@ if (!isset($_SERVER['REQUEST_URI']) && isset($_SERVER['SCRIPT_NAME']) && isset($
/** /**
* phpCAS version. accessible for the user by phpCAS::getVersion(). * phpCAS version. accessible for the user by phpCAS::getVersion().
*/ */
define('PHPCAS_VERSION', '1.6.1'); define('PHPCAS_VERSION', '1.6.2');
/** /**
* @addtogroup public * @addtogroup public
@@ -303,7 +303,7 @@ class phpCAS
/** /**
* This variable is used to enable verbose mode * This variable is used to enable verbose mode
* This pevents debug info to be show to the user. Since it's a security * This prevents debug info to be show to the user. Since it's a security
* feature the default is false * feature the default is false
* *
* @hideinitializer * @hideinitializer
@@ -338,7 +338,7 @@ class phpCAS
* @param bool $changeSessionID Allow phpCAS to change the session_id * @param bool $changeSessionID Allow phpCAS to change the session_id
* (Single Sign Out/handleLogoutRequests * (Single Sign Out/handleLogoutRequests
* is based on that change) * is based on that change)
* @param \SessionHandlerInterface $sessionHandler the session handler * @param \SessionHandlerInterface|null $sessionHandler the session handler
* *
* @return void a newly created CAS_Client object * @return void a newly created CAS_Client object
* @note Only one of the phpCAS::client() and phpCAS::proxy functions should be * @note Only one of the phpCAS::client() and phpCAS::proxy functions should be
@@ -347,7 +347,7 @@ class phpCAS
*/ */
public static function client($server_version, $server_hostname, public static function client($server_version, $server_hostname,
$server_port, $server_uri, $service_base_url, $server_port, $server_uri, $service_base_url,
$changeSessionID = true, \SessionHandlerInterface $sessionHandler = null $changeSessionID = true, ?\SessionHandlerInterface $sessionHandler = null
) { ) {
phpCAS :: traceBegin(); phpCAS :: traceBegin();
if (is_object(self::$_PHPCAS_CLIENT)) { if (is_object(self::$_PHPCAS_CLIENT)) {
@@ -393,7 +393,7 @@ class phpCAS
* @param bool $changeSessionID Allow phpCAS to change the session_id * @param bool $changeSessionID Allow phpCAS to change the session_id
* (Single Sign Out/handleLogoutRequests * (Single Sign Out/handleLogoutRequests
* is based on that change) * is based on that change)
* @param \SessionHandlerInterface $sessionHandler the session handler * @param \SessionHandlerInterface|null $sessionHandler the session handler
* *
* @return void a newly created CAS_Client object * @return void a newly created CAS_Client object
* @note Only one of the phpCAS::client() and phpCAS::proxy functions should be * @note Only one of the phpCAS::client() and phpCAS::proxy functions should be
@@ -402,14 +402,14 @@ class phpCAS
*/ */
public static function proxy($server_version, $server_hostname, public static function proxy($server_version, $server_hostname,
$server_port, $server_uri, $service_base_url, $server_port, $server_uri, $service_base_url,
$changeSessionID = true, \SessionHandlerInterface $sessionHandler = null $changeSessionID = true, ?\SessionHandlerInterface $sessionHandler = null
) { ) {
phpCAS :: traceBegin(); phpCAS :: traceBegin();
if (is_object(self::$_PHPCAS_CLIENT)) { if (is_object(self::$_PHPCAS_CLIENT)) {
phpCAS :: error(self::$_PHPCAS_INIT_CALL['method'] . '() has already been called (at ' . self::$_PHPCAS_INIT_CALL['file'] . ':' . self::$_PHPCAS_INIT_CALL['line'] . ')'); phpCAS :: error(self::$_PHPCAS_INIT_CALL['method'] . '() has already been called (at ' . self::$_PHPCAS_INIT_CALL['file'] . ':' . self::$_PHPCAS_INIT_CALL['line'] . ')');
} }
// store where the initialzer is called from // store where the initializer is called from
$dbg = debug_backtrace(); $dbg = debug_backtrace();
self::$_PHPCAS_INIT_CALL = array ( self::$_PHPCAS_INIT_CALL = array (
'done' => true, 'done' => true,
@@ -560,7 +560,7 @@ class phpCAS
$indent_str .= '| '; $indent_str .= '| ';
} }
// allow for multiline output with proper identing. Usefull for // allow for multiline output with proper identing. Useful for
// dumping cas answers etc. // dumping cas answers etc.
$str2 = str_replace("\n", "\n" . self::$_PHPCAS_DEBUG['unique_id'] . ' ' . $indent_str, $str); $str2 = str_replace("\n", "\n" . self::$_PHPCAS_DEBUG['unique_id'] . ' ' . $indent_str, $str);
$str3 = self::$_PHPCAS_DEBUG['unique_id'] . ' ' . $indent_str . $str2; $str3 = self::$_PHPCAS_DEBUG['unique_id'] . ' ' . $indent_str . $str2;
@@ -568,7 +568,7 @@ class phpCAS
self::$_PHPCAS_DEBUG['logger']->info($str3); self::$_PHPCAS_DEBUG['logger']->info($str3);
} }
if (!empty(self::$_PHPCAS_DEBUG['filename'])) { if (!empty(self::$_PHPCAS_DEBUG['filename'])) {
// Check if file exists and modifiy file permissions to be only // Check if file exists and modify file permissions to be only
// readable by the webserver // readable by the webserver
if (!file_exists(self::$_PHPCAS_DEBUG['filename'])) { if (!file_exists(self::$_PHPCAS_DEBUG['filename'])) {
touch(self::$_PHPCAS_DEBUG['filename']); touch(self::$_PHPCAS_DEBUG['filename']);
@@ -1769,7 +1769,7 @@ class phpCAS
/** /**
* If you want your service to be proxied you have to enable it (default * If you want your service to be proxied you have to enable it (default
* disabled) and define an accepable list of proxies that are allowed to * disabled) and define an acceptable list of proxies that are allowed to
* proxy your service. * proxy your service.
* *
* Add each allowed proxy definition object. For the normal CAS_ProxyChain * Add each allowed proxy definition object. For the normal CAS_ProxyChain
@@ -1790,7 +1790,7 @@ class phpCAS
* 'http://client.example.com/' * 'http://client.example.com/'
* ))); * )));
* *
* For quick testing or in certain production screnarios you might want to * For quick testing or in certain production scenarios you might want to
* allow allow any other valid service to proxy your service. To do so, add * allow allow any other valid service to proxy your service. To do so, add
* the "Any" chain: * the "Any" chain:
* phpCAS::allowProxyChain(new CAS_ProxyChain_Any); * phpCAS::allowProxyChain(new CAS_ProxyChain_Any);
@@ -1897,7 +1897,7 @@ class phpCAS
} }
/** /**
* Checks of a proxy client aready exists * Checks of a proxy client already exists
* *
* @throws CAS_OutOfSequenceBeforeProxyException * @throws CAS_OutOfSequenceBeforeProxyException
* *
@@ -55,7 +55,7 @@ implements CAS_Exception
* authenticated. * authenticated.
* *
* @param CAS_Client $client phpcas client * @param CAS_Client $client phpcas client
* @param string $failure the failure that occured * @param string $failure the failure that occurred
* @param string $cas_url the URL the CAS server was asked for * @param string $cas_url the URL the CAS server was asked for
* @param bool $no_response the response from the CAS server (other * @param bool $no_response the response from the CAS server (other
* parameters are ignored if TRUE) * parameters are ignored if TRUE)
+5 -5
View File
@@ -788,7 +788,7 @@ class CAS_Client
'file' => $dbg[1]['file'], 'file' => $dbg[1]['file'],
'line' => $dbg[1]['line'], 'line' => $dbg[1]['line'],
'method' => $dbg[1]['class'] . '::' . $dbg[1]['function'], 'method' => $dbg[1]['class'] . '::' . $dbg[1]['function'],
'result' => (boolean)$auth 'result' => (bool)$auth
); );
} }
private $_authentication_caller; private $_authentication_caller;
@@ -926,7 +926,7 @@ class CAS_Client
* CAS_ServiceBaseUrl_Interface for custom * CAS_ServiceBaseUrl_Interface for custom
* behavior. Added in 1.6.0. Similar to * behavior. Added in 1.6.0. Similar to
* serverName config in other CAS clients. * serverName config in other CAS clients.
* @param \SessionHandlerInterface $sessionHandler the session handler * @param \SessionHandlerInterface|null $sessionHandler the session handler
* *
* @return self a newly created CAS_Client object * @return self a newly created CAS_Client object
*/ */
@@ -938,7 +938,7 @@ class CAS_Client
$server_uri, $server_uri,
$service_base_url, $service_base_url,
$changeSessionID = true, $changeSessionID = true,
\SessionHandlerInterface $sessionHandler = null ?\SessionHandlerInterface $sessionHandler = null
) { ) {
// Argument validation // Argument validation
if (gettype($server_version) != 'string') if (gettype($server_version) != 'string')
@@ -3166,7 +3166,7 @@ class CAS_Client
$proxiedService->setCasClient($this); $proxiedService->setCasClient($this);
} }
return $proxiedService; return $proxiedService;
case PHPCAS_PROXIED_SERVICE_IMAP; case PHPCAS_PROXIED_SERVICE_IMAP:
$proxiedService = new CAS_ProxiedService_Imap($this->_getUser()); $proxiedService = new CAS_ProxiedService_Imap($this->_getUser());
if ($proxiedService instanceof CAS_ProxiedService_Testable) { if ($proxiedService instanceof CAS_ProxiedService_Testable) {
$proxiedService->setCasClient($this); $proxiedService->setCasClient($this);
@@ -4163,7 +4163,7 @@ class CAS_Client
* This method is used to print the HTML output when the user was not * This method is used to print the HTML output when the user was not
* authenticated. * authenticated.
* *
* @param string $failure the failure that occured * @param string $failure the failure that occurred
* @param string $cas_url the URL the CAS server was asked for * @param string $cas_url the URL the CAS server was asked for
* @param bool $no_response the response from the CAS server (other * @param bool $no_response the response from the CAS server (other
* parameters are ignored if true) * parameters are ignored if true)
+1 -1
View File
@@ -316,7 +316,7 @@ class CAS_PGTStorage_Db extends CAS_PGTStorage_AbstractStorage
try { try {
$pdo->beginTransaction(); $pdo->beginTransaction();
$query = $pdo->query($this->createTableSQL()); $query = $pdo->query($this->createTableSql());
$query->closeCursor(); $query->closeCursor();
$pdo->commit(); $pdo->commit();
+6
View File
@@ -128,6 +128,12 @@ class CAS_PGTStorage_File extends CAS_PGTStorage_AbstractStorage
phpCAS::error('an absolute path is needed for PGT storage to file'); phpCAS::error('an absolute path is needed for PGT storage to file');
} }
// ensure that the directory separator on Windows is '/' for consistency with the rest of the phpcas code
$path = str_replace(DIRECTORY_SEPARATOR , '/', $path);
// store the path (with a trailing '/')
$path = preg_replace('|([^/])$|', '$1/', $path);
} else { } else {
if ( $path[0] != '/' ) { if ( $path[0] != '/' ) {
@@ -139,7 +139,12 @@ implements CAS_Request_MultiRequestInterface
$buf = curl_multi_getcontent($handles[$i]); $buf = curl_multi_getcontent($handles[$i]);
$request->_storeResponseBody($buf); $request->_storeResponseBody($buf);
curl_multi_remove_handle($multiHandle, $handles[$i]); curl_multi_remove_handle($multiHandle, $handles[$i]);
curl_close($handles[$i]); if (PHP_VERSION_ID < 80000) {
curl_close($handles[$i]);
} else {
// unreference it => it will be closed
unset($handles[$i]);
}
} }
curl_multi_close($multiHandle); curl_multi_close($multiHandle);

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