Skip to content
Commits on Source (463)
################
# Drupal GitLabCI template
#
# Based off GitlabCI templates project: https://git.drupalcode.org/project/gitlab_templates
# Guide: https://www.drupal.org/docs/develop/git/using-gitlab-to-contribute-to-drupal/gitlab-ci
#
# With thanks to:
# - The GitLab Acceleration Initiative participants
# - DrupalSpoons
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the
# changes affect all pipelines using that include.
#
# Includes can be overriden by re-declaring anything provided in an include,
# here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
################
# Variables
#
# Overriding variables
# - To override one or more of these variables, simply declare your own
# variables keyword.
# - Keywords declared directly in .gitlab-ci.yml take precedence over include
# files.
# - Documentation: https://docs.gitlab.com/ee/ci/variables/
# - Predefined variables: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
#
################
variables:
_TARGET_PHP: "8.1"
CONCURRENCY: 15
GIT_DEPTH: "3"
################
# Stages
#
# Each job is assigned to a stage, defining the order in which the jobs are executed.
# Jobs in the same stage run in parallel.
#
# If all jobs in a stage succeed, the pipeline will proceed to the next stage.
# If any job in the stage fails, the pipeline will exit early.
################
stages:
################
# Code quality checks
#
# This stage includes any codebase validation that we want to perform
# before running functional tests.
################
- 🪄 Lint
################
# Test
#
# The test phase actually executes the tests, as well as gathering results
# and artifacts.
################
- 🗜️ Test
#############
# Templates #
#############
.run-on-mr: &run-on-mr
if: $CI_PIPELINE_SOURCE == "merge_request_event"
.run-on-mr-manual: &run-on-mr-manual
if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
allow_failure: true
.run-on-commit: &run-on-commit
if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ROOT_NAMESPACE == "project"
.run-daily: &run-daily
if: $CI_PIPELINE_SOURCE == "schedule" && $CI_PROJECT_ROOT_NAMESPACE == "project"
.default-stage: &default-stage
stage: 🗜️ Test
trigger:
# Rely on the status of the child pipeline.
strategy: depend
include:
- local: .gitlab-ci/pipeline.yml
rules:
- <<: *run-on-commit
- <<: *run-on-mr-manual
################
# Jobs
#
# Jobs define what scripts are actually executed in each stage.
################
'🧹 PHP Compatibility checks (PHPCS)':
stage: 🪄 Lint
variables:
PHPCS_PHP_VERSION: "5.6"
KUBERNETES_CPU_REQUEST: "16"
interruptible: true
allow_failure: true
retry:
max: 2
when:
- unknown_failure
- api_failure
- stuck_or_timeout_failure
- runner_system_failure
- scheduler_failure
image:
name: $_CONFIG_DOCKERHUB_ROOT/php-$_TARGET_PHP-apache:production
artifacts:
expire_in: 6 mos
paths:
- phpcs-quality-report.json
reports:
codequality: phpcs-quality-report.json
rules:
- <<: *run-on-mr
before_script:
- echo "{}" > composer.json
- composer config allow-plugins true -n
- composer require --dev drupal/coder:^8.2@stable micheh/phpcs-gitlab phpcompatibility/php-compatibility dealerdirect/phpcodesniffer-composer-installer
- export TARGET_BRANCH=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}${CI_COMMIT_BRANCH}
script:
- git fetch -vn --depth=$GIT_DEPTH origin "+refs/heads/$TARGET_BRANCH:refs/heads/$TARGET_BRANCH"
- export MODIFIED=`git diff --name-only refs/heads/$TARGET_BRANCH|while read r;do echo "$CI_PROJECT_DIR/$r";done|tr "\n" " "`
- echo -e "$MODIFIED" | tr " " "\n"
- echo "If this list contains more files than what you changed, then you need to rebase your branch."
- vendor/bin/phpcs --basepath=$CI_PROJECT_DIR --report-\\Micheh\\PhpCodeSniffer\\Report\\Gitlab=phpcs-quality-report.json --report-full --report-summary --standard=PHPCompatibility --runtime-set testVersion $PHPCS_PHP_VERSION --extensions=php,module,inc,install,test,profile,theme $MODIFIED
# Default job.
'PHP 8.1 MySQL 5.7':
<<: *default-stage
variables:
_TARGET_PHP: "8.1"
_TARGET_DB: "mysql-5.7"
rules:
- <<: *run-on-commit
- <<: *run-on-mr
'PHP 5.6 MySQL 5.5':
<<: *default-stage
variables:
_TARGET_PHP: "5.6"
_TARGET_DB: "mysql-5.5"
'PHP 7.2 MySQL 5.7':
<<: *default-stage
variables:
_TARGET_PHP: "7.2"
_TARGET_DB: "mysql-5.7"
'PHP 7.4 MySQL 5.7':
<<: *default-stage
variables:
_TARGET_PHP: "7.4"
_TARGET_DB: "mysql-5.7"
'PHP 8.0 MySQL 5.7':
<<: *default-stage
variables:
_TARGET_PHP: "8.0"
_TARGET_DB: "mysql-5.7"
'PHP 8.2 MySQL 8':
<<: *default-stage
variables:
_TARGET_PHP: "8.2"
_TARGET_DB: "mysql-8"
'PHP 7.4 PostgreSQL 9.5':
<<: *default-stage
variables:
_TARGET_PHP: "7.4"
_TARGET_DB: "pgsql-9.5"
'PHP 8.1 PostgreSQL 14.1':
<<: *default-stage
variables:
_TARGET_PHP: "8.1"
_TARGET_DB: "pgsql-14.1"
'PHP 7.4 SQLite 3.27.0':
<<: *default-stage
variables:
_TARGET_PHP: "7.4"
_TARGET_DB: "sqlite-3"
'PHP 8.1 MariaDB 10.3.22':
<<: *default-stage
variables:
_TARGET_PHP: "8.1"
_TARGET_DB: "mariadb-10.3.22"
# Redirect everything via the subdirectory.
RewriteEngine on
RewriteRule (.*) subdirectory/$1 [L]
stages:
################
# Test
#
# The test phase actually executes the tests, as well as gathering results
# and artifacts.
################
- 🗜️ Test
#############
# Templates #
#############
.default-job-settings: &default-job-settings
interruptible: true
allow_failure: false
retry:
max: 2
when:
- unknown_failure
- api_failure
- stuck_or_timeout_failure
- runner_system_failure
- scheduler_failure
image:
name: $_CONFIG_DOCKERHUB_ROOT/php-$_TARGET_PHP-apache:production
rules:
- if: $CI_PIPELINE_SOURCE == "parent_pipeline"
.test-variables: &test-variables
FF_NETWORK_PER_BUILD: 1
SIMPLETEST_BASE_URL: http://localhost/subdirectory
DB_DRIVER: mysql
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: drupal
MYSQL_USER: drupaltestbot
MYSQL_PASSWORD: drupaltestbotpw
POSTGRES_DB: drupaltestbot
POSTGRES_USER: drupaltestbot
POSTGRES_PASSWORD: drupaltestbotpw
CI_PARALLEL_NODE_INDEX: $CI_NODE_INDEX
CI_PARALLEL_NODE_TOTAL: $CI_NODE_TOTAL
.with-database: &with-database
name: $_CONFIG_DOCKERHUB_ROOT/$_TARGET_DB:production
alias: database
.with-chrome: &with-chrome
name: $_CONFIG_DOCKERHUB_ROOT/chromedriver:production
alias: chrome
entrypoint:
- chromedriver
- "--no-sandbox"
- "--log-path=/tmp/chromedriver.log"
- "--verbose"
- "--whitelisted-ips="
.phpunit-artifacts: &phpunit-artifacts
artifacts:
when: always
expire_in: 6 mos
reports:
junit: ./sites/default/files/simpletest/*.xml
paths:
- ./sites/default/files/simpletest
.setup-webroot: &setup-webserver
before_script:
- ln -s $CI_PROJECT_DIR /var/www/html/subdirectory
- cp $CI_PROJECT_DIR/.gitlab-ci/.htaccess-parent /var/www/html/.htaccess
- sudo service apache2 start
.get-simpletest-db: &get-simpletest-db
- |
# Assume SQLite unless we have another known target.
export SIMPLETEST_DB=sqlite://localhost/$CI_PROJECT_DIR/sites/default/files/db.sqlite
[[ $_TARGET_DB == mysql* ]] && export SIMPLETEST_DB=mysql://$MYSQL_USER:$MYSQL_PASSWORD@database/$MYSQL_DATABASE
[[ $_TARGET_DB == mariadb* ]] && export SIMPLETEST_DB=mysql://$MYSQL_USER:$MYSQL_PASSWORD@database/$MYSQL_DATABASE
[[ $_TARGET_DB == pgsql* ]] && export SIMPLETEST_DB=pgsql://$POSTGRES_USER:$POSTGRES_PASSWORD@database/$POSTGRES_DB
- echo "SIMPLETEST_DB = $SIMPLETEST_DB"
.prepare-dirs: &prepare-dirs
- mkdir -p ./sites/default/files ./sites/default/files/simpletest ./build/logs/junit
- chown -R www-data:www-data ./sites ./build/logs/junit /var/www/
- sudo -u www-data git config --global --add safe.directory $CI_PROJECT_DIR
.install-drupal: &install-drupal
- sudo -u www-data /usr/local/bin/drush si -y --db-url=$SIMPLETEST_DB --clean-url=0 --account-name=admin --account-pass=drupal --account-mail=admin@example.com
- sudo -u www-data /usr/local/bin/drush vset simpletest_clear_results '0'
- sudo -u www-data /usr/local/bin/drush vset simpletest_verbose '1'
- sudo -u www-data /usr/local/bin/drush en -y simpletest
.run-tests: &run-tests
script:
- *get-simpletest-db
- *prepare-dirs
- *install-drupal
# We need to pass this along directly even though it's set in the environment parameters.
- sudo -u www-data php ./scripts/run-tests.sh --color --concurrency "$CONCURRENCY" --url "$SIMPLETEST_BASE_URL" --verbose --fail-only --all --xml "$CI_PROJECT_DIR/sites/default/files/simpletest" --ci-parallel-node-index $CI_PARALLEL_NODE_INDEX --ci-parallel-node-total $CI_PARALLEL_NODE_TOTAL
.run-test-only-tests: &run-test-only-tests
script:
- *get-simpletest-db
- *prepare-dirs
- *install-drupal
- export TARGET_BRANCH=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}${CI_COMMIT_BRANCH}
- git fetch -vn --depth=50 origin "+refs/heads/$TARGET_BRANCH:refs/heads/$TARGET_BRANCH"
- |
echo "ℹ️ Changes from ${TARGET_BRANCH}"
git diff ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --name-only
echo "1️⃣ Reverting non test changes"
if [[ $(git diff ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --diff-filter=DM --name-only|grep -Ev '.test$'|grep -v .gitlab-ci|grep -v scripts/run-tests.sh) ]]; then
git diff ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --diff-filter=DM --name-only|grep -Ev '.test$'|grep -v .gitlab-ci|grep -v scripts/run-tests.sh|while read file;do
echo "↩️ Reverting $file"
git checkout refs/heads/${TARGET_BRANCH} -- $file;
done
fi
echo "2️⃣ Deleting new files"
if [[ $(git diff ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --diff-filter=A --name-only|grep -Ev '.test$'|grep -v .gitlab-ci|grep -v scripts/run-tests.sh) ]]; then
git diff ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --diff-filter=A --name-only|grep -Ev '.test$'|grep -v .gitlab-ci|grep -v scripts/run-tests.sh|while read file;do
echo "🗑️️ Deleting $file"
git rm $file
done
fi
echo "3️⃣ Running test changes for this branch"
if [[ $(git diff ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --name-only|grep -E '.test$') ]]; then
git diff ${CI_MERGE_REQUEST_DIFF_BASE_SHA} --name-only|grep -E ".test$"|while read file;do
sudo -u www-data php ./scripts/run-tests.sh --color --concurrency "$CONCURRENCY" --url "$SIMPLETEST_BASE_URL" --verbose --fail-only --xml "$CI_PROJECT_DIR/sites/default/files/simpletest/test-only" --file "$file"
done
fi
################
# Jobs
#
# Jobs define what scripts are actually executed in each stage.
################
'⚡️ PHPUnit Unit':
<<: [ *phpunit-artifacts, *setup-webserver, *run-tests, *default-job-settings ]
stage: 🗜️ Test
parallel: 3
services:
- <<: *with-database
- <<: *with-chrome
variables:
<<: *test-variables
CONCURRENCY: "$CONCURRENCY"
KUBERNETES_CPU_REQUEST: "16"
'🩹 Test-only changes':
<<: [ *phpunit-artifacts, *setup-webserver, *run-test-only-tests, *default-job-settings ]
stage: 🗜️ Test
when: manual
interruptible: true
allow_failure: true
variables:
<<: *test-variables
services:
- <<: *with-database
- <<: *with-chrome
......@@ -3,7 +3,7 @@
#
# Protect files and directories from prying eyes.
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock))$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig\.save)$">
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
......@@ -134,9 +134,9 @@ DirectoryIndex index.php index.html index.htm
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.*)\.js $1\.js\.gz [QSA]
# Serve correct content types, and prevent mod_deflate double gzip.
RewriteRule \.css\.gz$ - [T=text/css,E=no-gzip:1]
RewriteRule \.js\.gz$ - [T=text/javascript,E=no-gzip:1]
# Serve correct content types, and prevent double compression.
RewriteRule \.css\.gz$ - [T=text/css,E=no-gzip:1,E=no-brotli:1]
RewriteRule \.js\.gz$ - [T=text/javascript,E=no-gzip:1,E=no-brotli:1]
<FilesMatch "(\.js\.gz|\.css\.gz)$">
# Serve correct encoding type.
......@@ -147,8 +147,10 @@ DirectoryIndex index.php index.html index.htm
</IfModule>
</IfModule>
# Add headers to all responses.
# Various header fixes.
<IfModule mod_headers.c>
# Disable content sniffing, since it's an attack vector.
Header always set X-Content-Type-Options nosniff
# Disable Proxy header, since it's an attack vector.
RequestHeader unset Proxy
</IfModule>
Drupal 7.xxx, xxxx-xx-xx (development version)
------------------------
Drupal 7.100, 2024-03-06
------------------------
- Security improvements
- Announcements module added
Drupal 7.99, 2023-12-06
-----------------------
- Various security improvements
- Various bug fixes, optimizations and improvements
Drupal 7.98, 2023-06-07
-----------------------
- Various security improvements
- Various bug fixes, optimizations and improvements
Drupal 7.97, 2023-04-21
-----------------------
- Fix PHP 5.x regression caused by SA-CORE-2023-005
Drupal 7.96, 2023-04-19
-----------------------
- Fixed security issues:
- SA-CORE-2023-005
Drupal 7.95, 2023-03-15
-----------------------
- Fixed security issues:
- SA-CORE-2023-004
Drupal 7.94, 2022-12-14
-----------------------
- Hotfix for book.module and Select query properties
Drupal 7.93, 2022-12-07
-----------------------
- Improved support for PHP 8.2
- Minimum PHP version changed to PHP 5.3
- Various security hardenings
- Various bug fixes, optimizations and improvements
Drupal 7.92, 2022-09-07
-----------------------
- Improved support for PHP 8.1
- Various security hardenings
- Various bug fixes, optimizations and improvements
Drupal 7.91, 2022-07-20
-----------------------
- Fixed security issues:
- SA-CORE-2022-012
Drupal 7.90, 2022-06-01
-----------------------
- Improved support for PHP 8.1
- Improved support for PostgreSQL
- Various bug fixes, optimizations and improvements
Drupal 7.89, 2022-03-02
-----------------------
- Bug fixes for PHP 8.1
- Fix tests for PostgreSQL
Drupal 7.88, 2022-02-15
-----------------------
- Fixed security issues:
- SA-CORE-2022-003
Drupal 7.87, 2022-01-19
-----------------------
- Fix regression caused by jQuery UI position() backport
Drupal 7.86, 2022-01-18
-----------------------
- Fixed security issues:
- SA-CORE-2022-001
- SA-CORE-2022-002
Drupal 7.85, 2022-01-12
-----------------------
- Fix session cookies for sites with different base_urls but a shared domain
Drupal 7.84, 2021-12-13
-----------------------
- Hotfix for session cookie domain on www subdomains
Drupal 7.83, 2021-12-01
-----------------------
- Initial support for PHP 8.1
- The has_js cookie has been removed (but can be re-enabled)
- The leading www. is no longer stripped from cookie domain by default
- The user entity now has a "changed" property
- Introduced a skip_permissions_hardening setting
- Changes to the password reset process to avoid email and username enumeration
- Various bug fixes, optimizations and improvements
Drupal 7.82, 2021-07-21
-----------------------
- Fixed security issues:
- SA-CORE-2021-004
Drupal 7.81, 2021-06-02
-----------------------
- Block Google FLoC by default
- Testing and accessibility enhancements
- Various bug fixes, optimizations and improvements
Drupal 7.80, 2021-04-20
-----------------------
- Fixed security issues:
- SA-CORE-2021-002
Drupal 7.79, 2021-04-07
-----------------------
- Initial support for PHP 8
- Support for SameSite cookie attribute
- Avoid write for unchanged fields (opt-in)
Drupal 7.78, 2021-01-19
-----------------------
- Fixed security issues:
- SA-CORE-2021-001
Drupal 7.77, 2020-12-03
-----------------------
- Hotfix for schema.prefixed tables
Drupal 7.76, 2020-12-02
-----------------------
- Support for MySQL 8
- Core tests pass in SQLite
- Better user flood control logging
Drupal 7.75, 2020-11-26
-----------------------
- Fixed security issues:
- SA-CORE-2020-013
Drupal 7.74, 2020-11-17
-----------------------
- Fixed security issues:
- SA-CORE-2020-012
Drupal 7.73, 2020-09-16
-----------------------
- Fixed security issues:
- SA-CORE-2020-007
Drupal 7.72, 2020-06-17
-----------------------
- Fixed security issues:
- SA-CORE-2020-004
Drupal 7.71, 2020-06-03
-----------------------
- Fix for jQuery Form bug in Chromium-based browsers
- Full support for PHP 7.4
Drupal 7.70, 2020-05-19
-----------------------
- Fixed security issues:
- SA-CORE-2020-002
- SA-CORE-2020-003
Drupal 7.69, 2019-12-18
-----------------------
- Fixed security issues:
- SA-CORE-2019-012
Drupal 7.68, 2019-12-04
-----------------------
- Fixed: Hide toolbar when printing
- Fixed: Settings returned via ajax are not run through hook_js_alter()
- Fixed: Use drupal_http_build_query() in drupal_http_request()
- Fixed: DrupalRequestSanitizer not found fatal error when bootstrap phase order is changed
- Fixed: Block web.config in .htaccess (and vice-versa)
- Fixed: Create "scripts" element to align rendering workflow to how "styles" are handled
- PHP 7.3: Fixed 'Cannot change session id when session is active'
- PHP 7.1: Fixed 'A non-numeric value encountered in theme_pager()'
- PHP 7.x: Fixed file.inc generated .htaccess does not cover PHP 7
- PHP 5.3: Fixed check_plain() 'Invalid multibyte sequence in argument' test failures
- Fixed: Allow passing data as array to drupal_http_request()
- Fixed: Skip module_invoke/module_hook in calling hook_watchdog (excessive function_exist)
- Fixed: HTTP status 200 returned for 'Additional uncaught exception thrown while handling exception'
- Fixed: theme_table() should take an optional footer variable and produce <tfoot>
- Fixed: 'uasort() expects parameter 1 to be array, null given in node_view_multiple()'
- [regression] Fix default.settings.php permission
Drupal 7.67, 2019-05-08
-----------------------
- Fixed security issues:
- SA-CORE-2019-007
Drupal 7.66, 2019-04-17
-----------------------
- Fixed security issues:
- SA-CORE-2019-006
Drupal 7.65, 2019-03-20
-----------------------
- Fixed security issues:
- SA-CORE-2019-004
Drupal 7.64, 2019-02-06
-----------------------
- [regression] Unset the 'host' header in drupal_http_request() during redirect
- Fixed: 7.x does not have Phar protection and Phar tests are failing on Drupal 7
- Fixed: Notice: Undefined index: display_field in file_field_widget_value() (line 582 of /module/file/file.field.inc)
- Performance improvement: Registry rebuild should not parse the same file twice in the same request
- Fixed _registry_update() to clear caches after transaction is committed
Drupal 7.63, 2019-01-16
-----------------------
- Fixed a fatal error for some Drush users introduced by SA-CORE-2019-002.
Drupal 7.62, 2019-01-15
-----------------------
- Fixed security issues:
- SA-CORE-2019-001
- SA-CORE-2019-002
Drupal 7.61, 2018-11-07
-----------------------
- File upload validation functions and hook_file_validate() implementations are
......
......@@ -3,7 +3,7 @@ SQLITE REQUIREMENTS
-------------------
To use SQLite with your Drupal installation, the following requirements must be
met: Server has PHP 5.2 or later with PDO, and the PDO SQLite driver must be
met: Server has PHP 5.6 or later with PDO, and the PDO SQLite driver must be
enabled.
SQLITE DATABASE CREATION
......
......@@ -15,19 +15,20 @@ REQUIREMENTS AND NOTES
Drupal requires:
- A web server. Apache (version 2.0 or greater) is recommended.
- PHP 5.2.4 (or greater) (http://www.php.net/).
- PHP 5.6 (at least, PHP 8.x or greater recommended) (https://www.php.net/).
- One of the following databases:
- MySQL 5.0.15 (or greater) (http://www.mysql.com/).
- MariaDB 5.1.44 (or greater) (http://mariadb.org/). MariaDB is a fully
compatible drop-in replacement for MySQL.
- Percona Server 5.1.70 (or greater) (http://www.percona.com/). Percona
Server is a backwards-compatible replacement for MySQL.
- PostgreSQL 8.3 (or greater) (http://www.postgresql.org/).
- SQLite 3.3.7 (or greater) (http://www.sqlite.org/).
For more detailed information about Drupal requirements, including a list of
PHP extensions and configurations that are required, see "System requirements"
(http://drupal.org/requirements) in the Drupal.org online documentation.
- MySQL 5.5 (or greater) (https://www.mysql.com/) or equivalent versions of a
compatible database such as MariaDB or Percona.
- PostgreSQL 9.5 (or greater) (https://www.postgresql.org/).
- SQLite 3.27 (or greater) (https://www.sqlite.org/).
Note that version numbers above represent the minimum versions that Drupal 7 is
routinely tested with. For more detailed information about compatibility with
newer versions (that benefit from support from their maintainers), and
requirements including a list of PHP extensions and configurations that are
required, see "System requirements"
(https://www.drupal.org/docs/7/system-requirements) in the Drupal.org online
documentation.
For detailed information on how to configure a test server environment using a
variety of operating systems and web servers, see "Local server setup"
......
......@@ -11,11 +11,9 @@ The Drupal Core branch maintainers oversee the development of Drupal as a whole.
The branch maintainers for Drupal 7 are:
- Dries Buytaert 'dries' https://www.drupal.org/u/dries
- Angela Byron 'webchick' https://www.drupal.org/u/webchick
- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx
- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
- Stefan Ruijsenaars 'stefan.r' https://www.drupal.org/u/stefanr-0
- (provisional) Pol Dellaiera 'Pol' https://www.drupal.org/u/pol
- Drew Webber 'mcdruid' https://www.drupal.org/u/mcdruid
- Juraj Nemec 'poker10' https://www.drupal.org/u/poker10
Component maintainers
......
......@@ -13,12 +13,12 @@
include_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
if (!isset($_GET['cron_key']) || variable_get('cron_key', 'drupal') != $_GET['cron_key']) {
watchdog('cron', 'Cron could not run because an invalid key was used.', array(), WATCHDOG_NOTICE);
drupal_access_denied();
}
elseif (variable_get('maintenance_mode', 0)) {
if (variable_get('maintenance_mode', 0)) {
watchdog('cron', 'Cron could not run because the site is in maintenance mode.', array(), WATCHDOG_NOTICE);
drupal_site_offline();
}
elseif (!isset($_GET['cron_key']) || variable_get('cron_key', 'drupal') != $_GET['cron_key']) {
watchdog('cron', 'Cron could not run because an invalid key was used.', array(), WATCHDOG_NOTICE);
drupal_access_denied();
}
else {
......
......@@ -294,6 +294,7 @@ function ajax_render($commands = array()) {
// Now add a command to merge changes and additions to Drupal.settings.
$scripts = drupal_add_js();
drupal_alter('js', $scripts);
if (!empty($scripts['settings'])) {
$settings = $scripts['settings'];
array_unshift($commands, ajax_command_settings(drupal_array_merge_deep_array($settings['data']), TRUE));
......
......@@ -104,11 +104,6 @@ function authorize_filetransfer_form($form, &$form_state) {
// Start non-JS code.
if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default']) && $form_state['values']['connection_settings']['authorize_filetransfer_default'] == $name) {
// If the user switches from JS to non-JS, Drupal (and Batch API) will
// barf. This is a known bug: http://drupal.org/node/229825.
setcookie('has_js', '', time() - 3600, '/');
unset($_COOKIE['has_js']);
// Change the submit button to the submit_process one.
$form['submit_process']['#attributes'] = array();
unset($form['submit_connection']);
......
......@@ -72,7 +72,9 @@ function _batch_page() {
$output = NULL;
switch ($op) {
case 'start':
$output = _batch_start();
// Display the full progress page on startup and on each additional
// non-JavaScript iteration.
$output = _batch_progress_page();
break;
case 'do':
......@@ -82,7 +84,7 @@ function _batch_page() {
case 'do_nojs':
// Non-JavaScript-based progress page.
$output = _batch_progress_page_nojs();
$output = _batch_progress_page();
break;
case 'finished':
......@@ -93,69 +95,12 @@ function _batch_page() {
return $output;
}
/**
* Initializes the batch processing.
*
* JavaScript-enabled clients are identified by the 'has_js' cookie set in
* drupal.js. If no JavaScript-enabled page has been visited during the current
* user's browser session, the non-JavaScript version is returned.
*/
function _batch_start() {
if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) {
return _batch_progress_page_js();
}
else {
return _batch_progress_page_nojs();
}
}
/**
* Outputs a batch processing page with JavaScript support.
*
* This initializes the batch and error messages. Note that in JavaScript-based
* processing, the batch processing page is displayed only once and updated via
* AHAH requests, so only the first batch set gets to define the page title.
* Titles specified by subsequent batch sets are not displayed.
*
* @see batch_set()
* @see _batch_do()
*/
function _batch_progress_page_js() {
$batch = batch_get();
$current_set = _batch_current_set();
drupal_set_title($current_set['title'], PASS_THROUGH);
// Merge required query parameters for batch processing into those provided by
// batch_set() or hook_batch_alter().
$batch['url_options']['query']['id'] = $batch['id'];
$js_setting = array(
'batch' => array(
'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
'initMessage' => $current_set['init_message'],
'uri' => url($batch['url'], $batch['url_options']),
),
);
drupal_add_js($js_setting, 'setting');
drupal_add_library('system', 'drupal.batch');
return '<div id="progress"></div>';
}
/**
* Does one execution pass with JavaScript and returns progress to the browser.
*
* @see _batch_progress_page_js()
* @see _batch_process()
*/
function _batch_do() {
// HTTP POST required.
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
drupal_set_message(t('HTTP POST is required.'), 'error');
drupal_set_title(t('Error'));
return '';
}
// Perform actual processing.
list($percentage, $message) = _batch_process();
......@@ -164,11 +109,11 @@ function _batch_do() {
}
/**
* Outputs a batch processing page without JavaScript support.
* Outputs a batch processing page.
*
* @see _batch_process()
*/
function _batch_progress_page_nojs() {
function _batch_progress_page() {
$batch = &batch_get();
$current_set = _batch_current_set();
......@@ -216,6 +161,9 @@ function _batch_progress_page_nojs() {
$url = url($batch['url'], $batch['url_options']);
$element = array(
// Redirect through a 'Refresh' meta tag if JavaScript is disabled.
'#prefix' => '<noscript>',
'#suffix' => '</noscript>',
'#tag' => 'meta',
'#attributes' => array(
'http-equiv' => 'Refresh',
......@@ -224,6 +172,17 @@ function _batch_progress_page_nojs() {
);
drupal_add_html_head($element, 'batch_progress_meta_refresh');
// Adds JavaScript code and settings for clients where JavaScript is enabled.
$js_setting = array(
'batch' => array(
'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
'initMessage' => $current_set['init_message'],
'uri' => $url,
),
);
drupal_add_js($js_setting, 'setting');
drupal_add_library('system', 'drupal.batch');
return theme('progress_bar', array('percent' => $percentage, 'message' => $message));
}
......@@ -478,18 +437,17 @@ function _batch_finished() {
$queue->deleteQueue();
}
}
// Clean-up the session. Not needed for CLI updates.
if (isset($_SESSION)) {
unset($_SESSION['batches'][$batch['id']]);
if (empty($_SESSION['batches'])) {
unset($_SESSION['batches']);
}
}
}
$_batch = $batch;
$batch = NULL;
// Clean-up the session. Not needed for CLI updates.
if (isset($_SESSION)) {
unset($_SESSION['batches'][$batch['id']]);
if (empty($_SESSION['batches'])) {
unset($_SESSION['batches']);
}
}
// Redirect if needed.
if ($_batch['progressive']) {
// Revert the 'destination' that was saved in batch_process().
......
......@@ -8,7 +8,7 @@
/**
* The current system version.
*/
define('VERSION', '7.61');
define('VERSION', '7.101-dev');
/**
* Core API compatibility.
......@@ -18,7 +18,7 @@
/**
* Minimum supported version of PHP.
*/
define('DRUPAL_MINIMUM_PHP', '5.2.4');
define('DRUPAL_MINIMUM_PHP', '5.3.3');
/**
* Minimum recommended value of PHP memory_limit.
......@@ -359,6 +359,7 @@ public function __construct($cid, $bin) {
/**
* Implements ArrayAccess::offsetExists().
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset) {
return $this->offsetGet($offset) !== NULL;
}
......@@ -366,6 +367,7 @@ public function offsetExists($offset) {
/**
* Implements ArrayAccess::offsetGet().
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
if (isset($this->storage[$offset]) || array_key_exists($offset, $this->storage)) {
return $this->storage[$offset];
......@@ -378,6 +380,7 @@ public function offsetGet($offset) {
/**
* Implements ArrayAccess::offsetSet().
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value) {
$this->storage[$offset] = $value;
}
......@@ -385,6 +388,7 @@ public function offsetSet($offset, $value) {
/**
* Implements ArrayAccess::offsetUnset().
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset) {
unset($this->storage[$offset]);
}
......@@ -450,6 +454,9 @@ protected function set($data, $lock = TRUE) {
* Destructs the DrupalCacheArray object.
*/
public function __destruct() {
if ($this->bin == 'cache_form' && !variable_get('drupal_cache_array_persist_cache_form', FALSE)) {
return;
}
$data = array();
foreach ($this->keysToPersist as $offset => $persist) {
if ($persist) {
......@@ -803,14 +810,17 @@ function drupal_settings_initialize() {
// HTTP_HOST can be modified by a visitor, but we already sanitized it
// in drupal_settings_initialize().
if (!empty($_SERVER['HTTP_HOST'])) {
$cookie_domain = $_SERVER['HTTP_HOST'];
// Strip leading periods, www., and port numbers from cookie domain.
$cookie_domain = ltrim($cookie_domain, '.');
if (strpos($cookie_domain, 'www.') === 0) {
$cookie_domain = substr($cookie_domain, 4);
}
$cookie_domain = explode(':', $cookie_domain);
$cookie_domain = '.' . $cookie_domain[0];
$cookie_domain = _drupal_get_cookie_domain($_SERVER['HTTP_HOST']);
}
// Drupal 7.83 included a security improvement whereby www. is no longer
// stripped from the cookie domain. However, this can cause problems with
// existing session cookies where some users are left unable to login. In
// order to avoid that, prepend a leading dot to the session_name that was
// derived from the base_url when a www. subdomain is in use.
// @see https://www.drupal.org/project/drupal/issues/2522002
if (strpos($session_name, 'www.') === 0) {
$session_name = '.' . $session_name;
}
}
// Per RFC 2109, cookie domains must contain at least one dot other than the
......@@ -831,6 +841,24 @@ function drupal_settings_initialize() {
session_name($prefix . substr(hash('sha256', $session_name), 0, 32));
}
/**
* Derive the cookie domain to use for session cookies.
*
* @param $host
* The value of the HTTP host name.
*
* @return
* The string to use as a cookie domain.
*/
function _drupal_get_cookie_domain($host) {
$cookie_domain = $host;
// Strip leading periods and port numbers from cookie domain.
$cookie_domain = ltrim($cookie_domain, '.');
$cookie_domain = explode(':', $cookie_domain);
$cookie_domain = '.' . $cookie_domain[0];
return $cookie_domain;
}
/**
* Returns and optionally sets the filename for a system resource.
*
......@@ -1157,6 +1185,31 @@ function _drupal_trigger_error_with_delayed_logging($error_msg, $error_type = E_
$delay_logging = FALSE;
}
/**
* Invoke trigger_error() using a fatal error that will terminate the request.
*
* Normally, Drupal's error handler does not terminate script execution on
* user-level errors, even if the error is of type E_USER_ERROR. This function
* triggers an error of type E_USER_ERROR that is explicitly forced to be a
* fatal error which terminates script execution.
*
* @param string $error_msg
* The error message to trigger. As with trigger_error() itself, this is
* limited to 1024 bytes; additional characters beyond that will be removed.
*
* @see _drupal_error_handler_real()
*/
function drupal_trigger_fatal_error($error_msg) {
$fatal_error = &drupal_static(__FUNCTION__, FALSE);
$fatal_error = TRUE;
trigger_error($error_msg, E_USER_ERROR);
$fatal_error = FALSE;
// The standard Drupal error handler should have treated this as a fatal
// error and already ended the page request. But in case another error
// handler is being used, terminate execution explicitly here also.
exit;
}
/**
* Writes the file scan cache to the persistent cache.
*
......@@ -1189,19 +1242,21 @@ function variable_initialize($conf = array()) {
$variables = $cached->data;
}
else {
// Cache miss. Avoid a stampede.
// Cache miss. Avoid a stampede by acquiring a lock. If the lock fails to
// acquire, optionally just continue with uncached processing.
$name = 'variable_init';
if (!lock_acquire($name, 1)) {
// Another request is building the variable cache.
// Wait, then re-run this function.
$lock_acquired = lock_acquire($name, 1);
if (!$lock_acquired && variable_get('variable_initialize_wait_for_lock', FALSE)) {
lock_wait($name);
return variable_initialize($conf);
}
else {
// Proceed with variable rebuild.
// Load the variables from the table.
$variables = array_map('unserialize', db_query('SELECT name, value FROM {variable}')->fetchAllKeyed());
cache_set('variables', $variables, 'cache_bootstrap');
lock_release($name);
if ($lock_acquired) {
cache_set('variables', $variables, 'cache_bootstrap');
lock_release($name);
}
}
}
......@@ -1550,7 +1605,7 @@ function drupal_page_header() {
*/
function drupal_serve_page_from_cache(stdClass $cache) {
// Negotiate whether to use compression.
$page_compression = !empty($cache->data['page_compressed']);
$page_compression = !empty($cache->data['page_compressed']) && !empty($cache->data['body']);
$return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE;
// Get headers set in hook_boot(). Keys are lower-case.
......@@ -1853,7 +1908,7 @@ function format_string($string, array $args = array()) {
* @ingroup sanitization
*/
function check_plain($text) {
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8');
}
/**
......@@ -1881,7 +1936,7 @@ function check_plain($text) {
* TRUE if the text is valid UTF-8, FALSE if not.
*/
function drupal_validate_utf8($text) {
if (strlen($text) == 0) {
if (strlen((string) $text) == 0) {
return TRUE;
}
// With the PCRE_UTF8 modifier 'u', preg_match() fails silently on strings
......@@ -1987,6 +2042,8 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia
* - WATCHDOG_DEBUG: Debug-level messages.
* @param $link
* A link to associate with the message.
* SECURITY NOTE: Make sure your link is properly sanitized.
* Use the l() function to generate secure links.
*
* @see watchdog_severity_levels()
* @see hook_watchdog()
......@@ -1998,7 +2055,7 @@ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NO
// It is possible that the error handling will itself trigger an error. In that case, we could
// end up in an infinite loop. To avoid that, we implement a simple static semaphore.
if (!$in_error_state && function_exists('module_implements')) {
if (!$in_error_state && function_exists('module_invoke_all')) {
$in_error_state = TRUE;
// The user object may not exist in all conditions, so 0 is substituted if needed.
......@@ -2021,9 +2078,7 @@ function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NO
);
// Call the logging hooks to log/process the message
foreach (module_implements('watchdog') as $module) {
module_invoke($module, 'watchdog', $log_entry);
}
module_invoke_all('watchdog', $log_entry);
// It is critical that the semaphore is only cleared here, in the parent
// watchdog() call (not outside the loop), to prevent recursive execution.
......@@ -2253,19 +2308,34 @@ function drupal_base64_encode($string) {
/**
* Returns a string of highly randomized bytes (over the full 8-bit range).
*
* This function is better than simply calling mt_rand() or any other built-in
* PHP function because it can return a long string of bytes (compared to < 4
* bytes normally from mt_rand()) and uses the best available pseudo-random
* source.
* On PHP 7 and later, this function is a wrapper around the built-in PHP
* function random_bytes(). If that function does not exist or cannot find an
* appropriate source of randomness, this function is better than simply calling
* mt_rand() or any other built-in PHP function because it can return a long
* string of bytes (compared to < 4 bytes normally from mt_rand()) and uses the
* best available pseudo-random source.
*
* @param $count
* @param int $count
* The number of characters (bytes) to return in the string.
*
* @return string
* A randomly generated string.
*/
function drupal_random_bytes($count) {
if (function_exists('random_bytes')) {
try {
return random_bytes($count);
}
catch (Exception $e) {
// An appropriate source of randomness could not be found. Fall back to a
// less secure implementation.
}
}
// $random_state does not use drupal_static as it stores random bytes.
static $random_state, $bytes, $has_openssl;
$missing_bytes = $count - strlen($bytes);
$missing_bytes = $count - strlen((string) $bytes);
if ($missing_bytes > 0) {
// PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes()
......@@ -2298,7 +2368,7 @@ function drupal_random_bytes($count) {
// the microtime() - is prepended rather than appended. This is to avoid
// directly leaking $random_state via the $output stream, which could
// allow for trivial prediction of further "random" numbers.
if (strlen($bytes) < $count) {
if (strlen((string) $bytes) < $count) {
// Initialize on the first call. The contents of $_SERVER includes a mix of
// user-specific and system information that varies a little with each page.
if (!isset($random_state)) {
......@@ -2518,6 +2588,7 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
switch ($current_phase) {
case DRUPAL_BOOTSTRAP_CONFIGURATION:
require_once DRUPAL_ROOT . '/includes/request-sanitizer.inc';
_drupal_bootstrap_configuration();
break;
......@@ -2595,13 +2666,10 @@ function drupal_get_hash_salt() {
* The filename that the error was raised in.
* @param $line
* The line number the error was raised at.
* @param $context
* An array that points to the active symbol table at the point the error
* occurred.
*/
function _drupal_error_handler($error_level, $message, $filename, $line, $context) {
function _drupal_error_handler($error_level, $message, $filename, $line) {
require_once DRUPAL_ROOT . '/includes/errors.inc';
_drupal_error_handler_real($error_level, $message, $filename, $line, $context);
_drupal_error_handler_real($error_level, $message, $filename, $line);
}
/**
......@@ -2622,6 +2690,10 @@ function _drupal_exception_handler($exception) {
_drupal_log_error(_drupal_decode_exception($exception), TRUE);
}
catch (Exception $exception2) {
// Add a 500 status code in case an exception was thrown before the 500
// status could be set (e.g. while loading a maintenance theme from cache).
drupal_add_http_header('Status', '500 Internal Server Error');
// Another uncaught exception was thrown while handling the first one.
// If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
if (error_displayable()) {
......@@ -2647,7 +2719,6 @@ function _drupal_bootstrap_configuration() {
drupal_settings_initialize();
// Sanitize unsafe keys from the request.
require_once DRUPAL_ROOT . '/includes/request-sanitizer.inc';
DrupalRequestSanitizer::sanitize();
}
......@@ -3875,3 +3946,93 @@ function drupal_clear_opcode_cache($filepath) {
@apc_delete_file($filepath);
}
}
/**
* Drupal's wrapper around PHP's setcookie() function.
*
* This allows the cookie's $value and $options to be altered.
*
* @param $name
* The name of the cookie.
* @param $value
* The value of the cookie.
* @param $options
* An associative array which may have any of the keys expires, path, domain,
* secure, httponly, samesite.
*
* @see setcookie()
* @ingroup php_wrappers
*/
function drupal_setcookie($name, $value, $options) {
$options = _drupal_cookie_params($options);
if (\PHP_VERSION_ID >= 70300) {
setcookie($name, $value, $options);
}
else {
$defaults = array(
'expires' => 0,
'path' => '',
'domain' => '',
'secure' => FALSE,
'httponly' => FALSE,
);
$options += $defaults;
setcookie($name, $value, $options['expires'], $options['path'], $options['domain'], $options['secure'], $options['httponly']);
}
}
/**
* Process the params for cookies. This emulates support for the SameSite
* attribute in earlier versions of PHP, and allows the value of that attribute
* to be overridden.
*
* @param $options
* An associative array which may have any of the keys expires, path, domain,
* secure, httponly, samesite.
*
* @return
* An associative array which may have any of the keys expires, path, domain,
* secure, httponly, and samesite.
*/
function _drupal_cookie_params($options) {
$options['samesite'] = _drupal_samesite_cookie($options);
if (\PHP_VERSION_ID < 70300) {
// Emulate SameSite support in older PHP versions.
if (!empty($options['samesite'])) {
// Ensure the SameSite attribute is only added once.
if (!preg_match('/SameSite=/i', $options['path'])) {
$options['path'] .= '; SameSite=' . $options['samesite'];
}
}
}
return $options;
}
/**
* Determine the value for the samesite cookie attribute, in the following order
* of precedence:
*
* 1) A value explicitly passed to drupal_setcookie()
* 2) A value set in $conf['samesite_cookie_value']
* 3) The setting from php ini
* 4) The default of None, or FALSE (no attribute) if the cookie is not Secure
*
* @param $options
* An associative array as passed to drupal_setcookie().
* @return
* The value for the samesite cookie attribute.
*/
function _drupal_samesite_cookie($options) {
if (isset($options['samesite'])) {
return $options['samesite'];
}
$override = variable_get('samesite_cookie_value', NULL);
if ($override !== NULL) {
return $override;
}
$ini_options = session_get_cookie_params();
if (isset($ini_options['samesite'])) {
return $ini_options['samesite'];
}
return empty($options['secure']) ? FALSE : 'None';
}
This diff is collapsed.
......@@ -184,7 +184,7 @@
*
* @see http://php.net/manual/book.pdo.php
*/
abstract class DatabaseConnection extends PDO {
abstract class DatabaseConnection {
/**
* The database target this connection is for.
......@@ -261,6 +261,13 @@ abstract class DatabaseConnection extends PDO {
*/
protected $temporaryNameIndex = 0;
/**
* The actual PDO connection.
*
* @var \PDO
*/
protected $connection;
/**
* The connection information for this connection object.
*
......@@ -310,6 +317,13 @@ abstract class DatabaseConnection extends PDO {
*/
protected $escapedAliases = array();
/**
* List of un-prefixed table names, keyed by prefixed table names.
*
* @var array
*/
protected $unprefixedTablesMap = array();
function __construct($dsn, $username, $password, $driver_options = array()) {
// Initialize and prepare the connection prefix.
$this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
......@@ -318,14 +332,27 @@ function __construct($dsn, $username, $password, $driver_options = array()) {
$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
// Call PDO::__construct and PDO::setAttribute.
parent::__construct($dsn, $username, $password, $driver_options);
$this->connection = new PDO($dsn, $username, $password, $driver_options);
// Set a Statement class, unless the driver opted out.
if (!empty($this->statementClass)) {
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
$this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
}
}
/**
* Proxy possible direct calls to the \PDO methods.
*
* Since PHP8.0 the signature of the the \PDO::query() method has changed,
* and this class can't extending \PDO any more.
*
* However, for the BC, proxy any calls to the \PDO methods to the actual
* PDO connection object.
*/
public function __call($name, $arguments) {
return call_user_func_array(array($this->connection, $name), $arguments);
}
/**
* Destroys this Connection object.
*
......@@ -338,7 +365,9 @@ public function destroy() {
// Destroy all references to this connection by setting them to NULL.
// The Statement class attribute only accepts a new value that presents a
// proper callable, so we reset it to PDOStatement.
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
if (!empty($this->statementClass)) {
$this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
}
$this->schema = NULL;
}
......@@ -442,6 +471,13 @@ protected function setPrefix($prefix) {
$this->prefixReplace[] = $this->prefixes['default'];
$this->prefixSearch[] = '}';
$this->prefixReplace[] = '';
// Set up a map of prefixed => un-prefixed tables.
foreach ($this->prefixes as $table_name => $prefix) {
if ($table_name !== 'default') {
$this->unprefixedTablesMap[$prefix . $table_name] = $table_name;
}
}
}
/**
......@@ -477,6 +513,17 @@ public function tablePrefix($table = 'default') {
}
}
/**
* Gets a list of individually prefixed table names.
*
* @return array
* An array of un-prefixed table names, keyed by their fully qualified table
* names (i.e. prefix + table_name).
*/
public function getUnprefixedTablesMap() {
return $this->unprefixedTablesMap;
}
/**
* Prepares a query string and returns the prepared statement.
*
......@@ -494,7 +541,7 @@ public function prepareQuery($query) {
$query = $this->prefixTables($query);
// Call PDO::prepare.
return parent::prepare($query);
return $this->connection->prepare($query);
}
/**
......@@ -706,7 +753,7 @@ public function query($query, array $args = array(), $options = array()) {
case Database::RETURN_AFFECTED:
return $stmt->rowCount();
case Database::RETURN_INSERT_ID:
return $this->lastInsertId();
return $this->connection->lastInsertId();
case Database::RETURN_NULL:
return;
default:
......@@ -717,12 +764,12 @@ public function query($query, array $args = array(), $options = array()) {
if ($options['throw_exception']) {
// Add additional debug information.
if ($query instanceof DatabaseStatementInterface) {
$e->query_string = $stmt->getQueryString();
$e->errorInfo['query_string'] = $stmt->getQueryString();
}
else {
$e->query_string = $query;
$e->errorInfo['query_string'] = $query;
}
$e->args = $args;
$e->errorInfo['args'] = $args;
throw $e;
}
return NULL;
......@@ -1089,7 +1136,7 @@ public function rollback($savepoint_name = 'drupal_transaction') {
$rolled_back_other_active_savepoints = TRUE;
}
}
parent::rollBack();
$this->connection->rollBack();
if ($rolled_back_other_active_savepoints) {
throw new DatabaseTransactionOutOfOrderException();
}
......@@ -1117,7 +1164,7 @@ public function pushTransaction($name) {
$this->query('SAVEPOINT ' . $name);
}
else {
parent::beginTransaction();
$this->connection->beginTransaction();
}
$this->transactionLayers[$name] = $name;
}
......@@ -1168,7 +1215,7 @@ protected function popCommittableTransactions() {
// If there are no more layers left then we should commit.
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) {
if (!parent::commit()) {
if (!$this->connection->commit()) {
throw new DatabaseTransactionCommitFailedException();
}
}
......@@ -1252,7 +1299,7 @@ abstract public function driver();
* Returns the version of the database server.
*/
public function version() {
return $this->getAttribute(PDO::ATTR_SERVER_VERSION);
return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION);
}
/**
......@@ -1697,12 +1744,16 @@ final public static function renameConnection($old_key, $new_key) {
*
* @param $key
* The connection key.
* @param $close
* Whether to close the connection.
* @return
* TRUE in case of success, FALSE otherwise.
*/
final public static function removeConnection($key) {
final public static function removeConnection($key, $close = TRUE) {
if (isset(self::$databaseInfo[$key])) {
self::closeConnection(NULL, $key);
if ($close) {
self::closeConnection(NULL, $key);
}
unset(self::$databaseInfo[$key]);
return TRUE;
}
......@@ -1873,6 +1924,11 @@ class DatabaseTransactionOutOfOrderException extends Exception { }
*/
class InvalidMergeQueryException extends Exception {}
/**
* Exception thrown if an invalid query condition is specified.
*/
class InvalidQueryConditionOperatorException extends Exception {}
/**
* Exception thrown if an insert query specifies a field twice.
*
......@@ -2206,6 +2262,7 @@ protected function __construct($dbh) {
$this->setFetchMode(PDO::FETCH_OBJ);
}
#[\ReturnTypeWillChange]
public function execute($args = array(), $options = array()) {
if (isset($options['fetch'])) {
if (is_string($options['fetch'])) {
......@@ -2343,23 +2400,27 @@ public function fetchAllAssoc($key, $fetch = NULL) {
}
/* Implementations of Iterator. */
#[\ReturnTypeWillChange]
public function current() {
return NULL;
}
#[\ReturnTypeWillChange]
public function key() {
return NULL;
}
#[\ReturnTypeWillChange]
public function rewind() {
// Nothing to do: our DatabaseStatement can't be rewound.
}
#[\ReturnTypeWillChange]
public function next() {
// Do nothing, since this is an always-empty implementation.
}
#[\ReturnTypeWillChange]
public function valid() {
return FALSE;
}
......@@ -2840,7 +2901,6 @@ function db_field_exists($table, $field) {
*
* @param $table_expression
* An SQL expression, for example "simpletest%" (without the quotes).
* BEWARE: this is not prefixed, the caller should take care of that.
*
* @return
* Array, both the keys and the values are the matching tables.
......@@ -2849,6 +2909,23 @@ function db_find_tables($table_expression) {
return Database::getConnection()->schema()->findTables($table_expression);
}
/**
* Finds all tables that are like the specified base table name. This is a
* backport of the change made to db_find_tables in Drupal 8 to work with
* virtual, un-prefixed table names. The original function is retained for
* Backwards Compatibility.
* @see https://www.drupal.org/node/2552435
*
* @param $table_expression
* An SQL expression, for example "simpletest%" (without the quotes).
*
* @return
* Array, both the keys and the values are the matching tables.
*/
function db_find_tables_d8($table_expression) {
return Database::getConnection()->schema()->findTablesD8($table_expression);
}
function _db_create_keys_sql($spec) {
return Database::getConnection()->schema()->createKeysSql($spec);
}
......
......@@ -5,6 +5,11 @@
* Database interface code for MySQL database servers.
*/
/**
* The default character for quoting identifiers in MySQL.
*/
define('MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT', '`');
/**
* @addtogroup database
* @{
......@@ -19,6 +24,279 @@ class DatabaseConnection_mysql extends DatabaseConnection {
*/
protected $needsCleanup = FALSE;
/**
* The list of MySQL reserved key words.
*
* @link https://dev.mysql.com/doc/refman/8.0/en/keywords.html
*/
private $reservedKeyWords = array(
'accessible',
'add',
'admin',
'all',
'alter',
'analyze',
'and',
'as',
'asc',
'asensitive',
'before',
'between',
'bigint',
'binary',
'blob',
'both',
'by',
'call',
'cascade',
'case',
'change',
'char',
'character',
'check',
'collate',
'column',
'condition',
'constraint',
'continue',
'convert',
'create',
'cross',
'cube',
'cume_dist',
'current_date',
'current_time',
'current_timestamp',
'current_user',
'cursor',
'database',
'databases',
'day_hour',
'day_microsecond',
'day_minute',
'day_second',
'dec',
'decimal',
'declare',
'default',
'delayed',
'delete',
'dense_rank',
'desc',
'describe',
'deterministic',
'distinct',
'distinctrow',
'div',
'double',
'drop',
'dual',
'each',
'else',
'elseif',
'empty',
'enclosed',
'escaped',
'except',
'exists',
'exit',
'explain',
'false',
'fetch',
'first_value',
'float',
'float4',
'float8',
'for',
'force',
'foreign',
'from',
'fulltext',
'function',
'generated',
'get',
'grant',
'group',
'grouping',
'groups',
'having',
'high_priority',
'hour_microsecond',
'hour_minute',
'hour_second',
'if',
'ignore',
'in',
'index',
'infile',
'inner',
'inout',
'insensitive',
'insert',
'int',
'int1',
'int2',
'int3',
'int4',
'int8',
'integer',
'intersect',
'interval',
'into',
'io_after_gtids',
'io_before_gtids',
'is',
'iterate',
'join',
'json_table',
'key',
'keys',
'kill',
'lag',
'last_value',
'lateral',
'lead',
'leading',
'leave',
'left',
'like',
'limit',
'linear',
'lines',
'load',
'localtime',
'localtimestamp',
'lock',
'long',
'longblob',
'longtext',
'loop',
'low_priority',
'master_bind',
'master_ssl_verify_server_cert',
'match',
'maxvalue',
'mediumblob',
'mediumint',
'mediumtext',
'middleint',
'minute_microsecond',
'minute_second',
'mod',
'modifies',
'natural',
'not',
'no_write_to_binlog',
'nth_value',
'ntile',
'null',
'numeric',
'of',
'on',
'optimize',
'optimizer_costs',
'option',
'optionally',
'or',
'order',
'out',
'outer',
'outfile',
'over',
'partition',
'percent_rank',
'persist',
'persist_only',
'precision',
'primary',
'procedure',
'purge',
'range',
'rank',
'read',
'reads',
'read_write',
'real',
'recursive',
'references',
'regexp',
'release',
'rename',
'repeat',
'replace',
'require',
'resignal',
'restrict',
'return',
'revoke',
'right',
'rlike',
'row',
'rows',
'row_number',
'schema',
'schemas',
'second_microsecond',
'select',
'sensitive',
'separator',
'set',
'show',
'signal',
'smallint',
'spatial',
'specific',
'sql',
'sqlexception',
'sqlstate',
'sqlwarning',
'sql_big_result',
'sql_calc_found_rows',
'sql_small_result',
'ssl',
'starting',
'stored',
'straight_join',
'system',
'table',
'terminated',
'then',
'tinyblob',
'tinyint',
'tinytext',
'to',
'trailing',
'trigger',
'true',
'undo',
'union',
'unique',
'unlock',
'unsigned',
'update',
'usage',
'use',
'using',
'utc_date',
'utc_time',
'utc_timestamp',
'values',
'varbinary',
'varchar',
'varcharacter',
'varying',
'virtual',
'when',
'where',
'while',
'window',
'with',
'write',
'xor',
'year_month',
'zerofill',
);
public function __construct(array $connection_options = array()) {
// This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
......@@ -56,6 +334,11 @@ public function __construct(array $connection_options = array()) {
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
// Because MySQL's prepared statements skip the query cache, because it's dumb.
PDO::ATTR_EMULATE_PREPARES => TRUE,
// Convert numeric values to strings when fetching. In PHP 8.1,
// PDO::ATTR_EMULATE_PREPARES now behaves the same way as non emulated
// prepares and returns integers. See https://externals.io/message/113294
// for further discussion.
PDO::ATTR_STRINGIFY_FETCHES => TRUE,
);
if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
// An added connection option in PHP 5.5.21+ to optionally limit SQL to a
......@@ -69,10 +352,10 @@ public function __construct(array $connection_options = array()) {
// certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci'
// for UTF-8.
if (!empty($connection_options['collation'])) {
$this->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
$this->connection->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
}
else {
$this->exec('SET NAMES ' . $charset);
$this->connection->exec('SET NAMES ' . $charset);
}
// Set MySQL init_commands if not already defined. Default Drupal's MySQL
......@@ -86,15 +369,95 @@ public function __construct(array $connection_options = array()) {
$connection_options += array(
'init_commands' => array(),
);
$sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';
// NO_AUTO_CREATE_USER was removed in MySQL 8.0.11
// https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-deprecation-removal
if (version_compare($this->connection->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11', '<')) {
$sql_mode .= ',NO_AUTO_CREATE_USER';
}
$connection_options['init_commands'] += array(
'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
'sql_mode' => "SET sql_mode = '$sql_mode'",
);
// Execute initial commands.
foreach ($connection_options['init_commands'] as $sql) {
$this->exec($sql);
$this->connection->exec($sql);
}
}
/**
* {@inheritdoc}}
*/
protected function setPrefix($prefix) {
parent::setPrefix($prefix);
// Successive versions of MySQL have become increasingly strict about the
// use of reserved keywords as table names. Drupal 7 uses at least one such
// table (system). Therefore we surround all table names with quotes.
$quote_char = variable_get('mysql_identifier_quote_character', MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT);
foreach ($this->prefixSearch as $i => $prefixSearch) {
if (substr($prefixSearch, 0, 1) === '{') {
// If the prefix already contains one or more quotes remove them.
// This can happen when - for example - DrupalUnitTestCase sets up a
// "temporary prefixed database". Also if there's a dot in the prefix,
// wrap it in quotes to cater for schema names in prefixes.
$search = array($quote_char, '.');
$replace = array('', $quote_char . '.' . $quote_char);
$this->prefixReplace[$i] = $quote_char . str_replace($search, $replace, $this->prefixReplace[$i]);
}
if (substr($prefixSearch, -1) === '}') {
$this->prefixReplace[$i] .= $quote_char;
}
}
}
/**
* {@inheritdoc}
*/
public function escapeField($field) {
$field = parent::escapeField($field);
return $this->quoteIdentifier($field);
}
public function escapeFields(array $fields) {
foreach ($fields as &$field) {
$field = $this->escapeField($field);
}
return $fields;
}
/**
* {@inheritdoc}
*/
public function escapeAlias($field) {
$field = parent::escapeAlias($field);
return $this->quoteIdentifier($field);
}
/**
* Quotes an identifier if it matches a MySQL reserved keyword.
*
* @param string $identifier
* The field to check.
*
* @return string
* The identifier, quoted if it matches a MySQL reserved keyword.
*/
private function quoteIdentifier($identifier) {
// Quote identifiers so that MySQL reserved words like 'function' can be
// used as column names. Sometimes the 'table.column_name' format is passed
// in. For example, menu_load_links() adds a condition on "ml.menu_name".
if (strpos($identifier, '.') !== FALSE) {
list($table, $identifier) = explode('.', $identifier, 2);
}
if (in_array(strtolower($identifier), $this->reservedKeyWords, TRUE)) {
// Quote the string for MySQL reserved keywords.
$quote_char = variable_get('mysql_identifier_quote_character', MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT);
$identifier = $quote_char . $identifier . $quote_char;
}
return isset($table) ? $table . '.' . $identifier : $identifier;
}
public function __destruct() {
if ($this->needsCleanup) {
$this->nextIdDelete();
......@@ -180,7 +543,7 @@ protected function popCommittableTransactions() {
// If there are no more layers left then we should commit.
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) {
if (!PDO::commit()) {
if (!$this->doCommit()) {
throw new DatabaseTransactionCommitFailedException();
}
}
......@@ -203,7 +566,7 @@ protected function popCommittableTransactions() {
$this->transactionLayers = array();
// We also have to explain to PDO that the transaction stack has
// been cleaned-up.
PDO::commit();
$this->doCommit();
}
else {
throw $e;
......@@ -213,6 +576,53 @@ protected function popCommittableTransactions() {
}
}
/**
* Do the actual commit, including a workaround for PHP 8 behaviour changes.
*
* @return bool
* Success or otherwise of the commit.
*/
protected function doCommit() {
if ($this->connection->inTransaction()) {
return $this->connection->commit();
}
else {
// In PHP 8.0 a PDOException is thrown when a commit is attempted with no
// transaction active. In previous PHP versions this failed silently.
return TRUE;
}
}
/**
* {@inheritdoc}
*/
public function rollback($savepoint_name = 'drupal_transaction') {
// MySQL will automatically commit transactions when tables are altered or
// created (DDL transactions are not supported). Prevent triggering an
// exception to ensure that the error that has caused the rollback is
// properly reported.
if (!$this->connection->inTransaction()) {
// Before PHP 8 $this->connection->inTransaction() will return TRUE and
// $this->connection->rollback() does not throw an exception; the
// following code is unreachable.
// If \DatabaseConnection::rollback() would throw an
// exception then continue to throw an exception.
if (!$this->inTransaction()) {
throw new DatabaseTransactionNoActiveException();
}
// A previous rollback to an earlier savepoint may mean that the savepoint
// in question has already been accidentally committed.
if (!isset($this->transactionLayers[$savepoint_name])) {
throw new DatabaseTransactionNoActiveException();
}
trigger_error('Rollback attempted when there is no active transaction. This can cause data integrity issues.', E_USER_WARNING);
return;
}
return parent::rollback($savepoint_name);
}
public function utf8mb4IsConfigurable() {
return TRUE;
}
......@@ -223,7 +633,7 @@ public function utf8mb4IsActive() {
public function utf8mb4IsSupported() {
// Ensure that the MySQL driver supports utf8mb4 encoding.
$version = $this->getAttribute(PDO::ATTR_CLIENT_VERSION);
$version = $this->connection->getAttribute(PDO::ATTR_CLIENT_VERSION);
if (strpos($version, 'mysqlnd') !== FALSE) {
// The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
$version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
......
......@@ -48,6 +48,10 @@ public function __toString() {
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
if (method_exists($this->connection, 'escapeFields')) {
$insert_fields = $this->connection->escapeFields($insert_fields);
}
// If we're selecting from a SelectQuery, finish building the query and
// pass it back, as any remaining options are irrelevant.
if (!empty($this->fromQuery)) {
......@@ -89,6 +93,20 @@ public function __toString() {
class TruncateQuery_mysql extends TruncateQuery { }
class UpdateQuery_mysql extends UpdateQuery {
public function __toString() {
if (method_exists($this->connection, 'escapeField')) {
$escapedFields = array();
foreach ($this->fields as $field => $data) {
$field = $this->connection->escapeField($field);
$escapedFields[$field] = $data;
}
$this->fields = $escapedFields;
}
return parent::__toString();
}
}
/**
* @} End of "addtogroup database".
*/
......@@ -57,6 +57,11 @@ protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
$info = $this->connection->getConnectionOptions();
// Ensure the table name is not surrounded with quotes as that is not
// appropriate for schema queries.
$quote_char = variable_get('mysql_identifier_quote_character', MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT);
$table_name = str_replace($quote_char, '', $table_name);
$table_info = $this->getPrefixInfo($table_name, $add_prefix);
$condition = new DatabaseCondition('AND');
......@@ -494,11 +499,11 @@ public function getComment($table, $column = NULL) {
$condition->condition('column_name', $column);
$condition->compile($this->connection, $this);
// Don't use {} around information_schema.columns table.
return $this->connection->query("SELECT column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
return $this->connection->query("SELECT column_comment AS column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
}
$condition->compile($this->connection, $this);
// Don't use {} around information_schema.tables table.
$comment = $this->connection->query("SELECT table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
$comment = $this->connection->query("SELECT table_comment AS table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
// Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
return preg_replace('/; InnoDB free:.*$/', '', $comment);
}
......
......@@ -66,11 +66,11 @@ public function __construct(array $connection_options = array()) {
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
// Force PostgreSQL to use the UTF-8 character set by default.
$this->exec("SET NAMES 'UTF8'");
$this->connection->exec("SET NAMES 'UTF8'");
// Execute PostgreSQL init_commands.
if (isset($connection_options['init_commands'])) {
$this->exec(implode('; ', $connection_options['init_commands']));
$this->connection->exec(implode('; ', $connection_options['init_commands']));
}
}
......@@ -117,7 +117,8 @@ public function query($query, array $args = array(), $options = array()) {
case Database::RETURN_AFFECTED:
return $stmt->rowCount();
case Database::RETURN_INSERT_ID:
return $this->lastInsertId($options['sequence_name']);
$sequence_name = isset($options['sequence_name']) ? $options['sequence_name'] : NULL;
return $this->connection->lastInsertId($sequence_name);
case Database::RETURN_NULL:
return;
default:
......@@ -128,12 +129,12 @@ public function query($query, array $args = array(), $options = array()) {
if ($options['throw_exception']) {
// Add additional debug information.
if ($query instanceof DatabaseStatementInterface) {
$e->query_string = $stmt->getQueryString();
$e->errorInfo['query_string'] = $stmt->getQueryString();
}
else {
$e->query_string = $query;
$e->errorInfo['query_string'] = $query;
}
$e->args = $args;
$e->errorInfo['args'] = $args;
throw $e;
}
return NULL;
......
......@@ -30,7 +30,7 @@ public function execute() {
foreach ($this->insertFields as $idx => $field) {
if (isset($table_information->blob_fields[$field])) {
$blobs[$blob_count] = fopen('php://memory', 'a');
fwrite($blobs[$blob_count], $insert_values[$idx]);
fwrite($blobs[$blob_count], (string) $insert_values[$idx]);
rewind($blobs[$blob_count]);
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], PDO::PARAM_LOB);
......@@ -182,7 +182,7 @@ public function execute() {
if (isset($table_information->blob_fields[$field])) {
$blobs[$blob_count] = fopen('php://memory', 'a');
fwrite($blobs[$blob_count], $value);
fwrite($blobs[$blob_count], (string) $value);
rewind($blobs[$blob_count]);
$stmt->bindParam($placeholder, $blobs[$blob_count], PDO::PARAM_LOB);
++$blob_count;
......