Problem/Motivation

When installing Drupal 8 on a system that does not have mbstring support, a fatal error is thrown after the database has been created, as the cache module uses mb_check_encoding in the normalizeCid function, without checking for existence of that function.

Fatal error: Call to undefined function Drupal\Core\Cache\mb_check_encoding() in core/lib/Drupal/Core/Cache/DatabaseBackend.php on line 410

As mbstring is not a requirement (the installer system check gives a warning if mbstring is not present, not an error), the module should either check to see if it's present, or not use it at all.

Steps to reproduce:

  • make sure mbstring is disabled
  • follow install.php
  • after entering the database details, the error is thrown

Proposed resolution

Add a method in DatabaseBackend to check for the existence of mbstring, and use a regular expression match to determine if the CID is ASCII instead. This allows for the non-mbstring functionality to be tested on testing infrastructure with mbstring installed. The DatabaseBackend unit tests should be expanded to cover this case.

Remaining tasks

  • Write patch with tests in DatabaseBackendUnitTest
  • Review patch
  • Beta Summary (done)

User interface changes

None

API changes

An internal method is added to the cache DatabaseBackend.

Data model changes

None.

Beta phase evaluation

-->

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because an error is thrown during installation.
Issue priority Major because fatal error and some people don't have mbstring
Unfrozen changes Unfrozen because it only changes CSS/markup/strings/documentation/tests. (Which? Specify.)

Comments

TomCan created an issue. See original summary.

TomCan’s picture

TomCan’s picture

I've provided 2 patches, one that add the check for mb_check_encoding with fallback to preg_match, the other simply replaces the mb_check_encoding call with preg_match.

TomCan’s picture

Issue summary: View changes
TomCan’s picture

Status: Active » Needs review
oadaeh’s picture

+++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
@@ -407,7 +407,11 @@ protected function catchException(\Exception $e, $table_name = NULL) {
+    } else {

The else { needs to be on a new line.

Edit: here's the reference: https://www.drupal.org/coding-standards#controlstruct

TomCan’s picture

New patch, following the Drupal coding standards

othermachines’s picture

This error cropped up for me on a fresh install after switching to a new PHP version (mbstring disabled by default). Patch in #7 resolved it.

TomCan’s picture

Version: 8.0.0-beta14 » 8.0.x-dev

Updated version, also present in beta15 and dev.

borisson_’s picture

Issue summary: View changes
stefan.r’s picture

Priority: Normal » Major
Issue tags: +Needs tests

Having written that original line of code, I think the patch is fine - mb_check_encoding() is faster so we default to that, and fall back to a regular expression (as promised in the installer)

This should have a unit test though - we can put function_exists('mb_check_encoding') in a separate method and mock that.

stefan.r’s picture

Issue summary: View changes
TomCan’s picture

Maybe a stupid question, but should we actually be testing this? Both the normalizeCid as the newly to be defined method to check the presence of mbstring are non-public functions. Such functionality would be implicitly tested if tests existed for the public methods (but in this case don't).

In any case, it would indeed be better to move the check to it's own method, and call that once from the constructor and store the result in a variable. There's no need to call it on every call of the normalizeCid function. Also, this way you can simply set the value through reflection rather than having to mock the method. I've added a patch for this.

I had a go at adding a test. The test will check to see if the correct method was selected. Obviously, the result of that test is dependent on the config of the system that is running the test. I also wrote a few test cids to test. The expected results were 'manualy' composed, using the Crypt::hashBase64 function. The tests probably could use some work (duplicate code etc), but it's a start.

stefan.r’s picture

Issue tags: -Needs tests
  1. +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
    @@ -42,6 +42,11 @@ class DatabaseBackend implements CacheBackendInterface {
    +  protected $useMbstring;
    
    @@ -58,6 +63,9 @@ public function __construct(Connection $connection, CacheTagsChecksumInterface $
    +    $this->useMbstring = function_exists('mb_check_encoding');
    

    Let's stick to adding a method that wraps function_exists('mb_check_encoding') rather than adding another property?

  2. +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
    @@ -58,6 +63,9 @@ public function __construct(Connection $connection, CacheTagsChecksumInterface $
    +    // check if mb_check_encoding function is present
    

    We start comments with a capital and end with a full stop.

  3. +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
    @@ -421,6 +429,25 @@ protected function normalizeCid($cid) {
    +   * Checks to see if a given string is only made up of ascii characters
    

    Capital/full stop.

  4. +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
    @@ -421,6 +429,25 @@ protected function normalizeCid($cid) {
    +   * Uses mb_check_encoding if present, otherwise falls back to preg_match
    

    Missing newline before long description.

  5. +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
    @@ -421,6 +429,25 @@ protected function normalizeCid($cid) {
    +   *   the text to check
    

    Capital/full stop.

  6. +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
    @@ -421,6 +429,25 @@ protected function normalizeCid($cid) {
    +   *   true if $text only contains ascii characters, otherwise false
    

    Capital/full stop.

  7. +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
    @@ -421,6 +429,25 @@ protected function normalizeCid($cid) {
    +    if ($this->useMbstring) {
    

    if ($this->mbStringExists())?

  8. +++ b/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendTest.php
    @@ -0,0 +1,91 @@
    +class DatabaseBackendTest extends UnitTestCase
    +{
    

    { goes on the same line as the class

  9. +++ b/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendTest.php
    @@ -0,0 +1,91 @@
    +    /**
    

    indentation is by 2 spaces

  10. +++ b/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendTest.php
    @@ -0,0 +1,91 @@
    +     * Tests if the correct function is choosen
    

    chosen.

  11. +++ b/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendTest.php
    @@ -0,0 +1,91 @@
    +        // make method and variable accessible through reflection
    ...
    +        // create classes
    ...
    +            // ascii only, should return same
    ...
    +            // utf-8, should return hash
    ...
    +            // ascii, over 255 long, should return hash padded with original string to a 255 length
    ...
    +            // utf-8, over 255 long, should return hash
    ...
    +        // make method and variable accessible through reflection
    ...
    +        // create classes
    ...
    +            // run checks
    ...
    +        // now force usage of preg
    

    Capital/full stop.

  12. +++ b/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendTest.php
    @@ -0,0 +1,91 @@
    +     * Tests that the DatabaseBackend normalizeCid works both with or without mbstring.
    

    Short description should be 80 chars max.

TomCan’s picture

By looking at the TestResults of the two patches I submitted, I noticed that appearantly there is already a test class for the DatabaseBackend class, located in Drupal\system\Tests\Cache\DatabaseBackendUnitTest (core/modules/system/src/Tests/Cache/DatabaseBackendUnitTest.php).

I'll create a new patch that takes in your comments, as well as moves the tests to the appropriate file.

mradcliffe’s picture

Issue summary: View changes
Status: Needs review » Needs work

I tried to summarize from the comments the recommended approach based off of TomCan's and stefan_r's review.

I think the correct status is currently Needs work.

borisson_’s picture

Status: Needs work » Needs review
StatusFileSize
new10.87 KB
new6.23 KB

Fixed remarks in #14 and moved tests as suggested in #15.

stefan.r’s picture

Status: Needs review » Needs work

Discussed this in person with @borisson_ at the sprint in BCN. Tests will be added and we'll get rid of useMbstring.

borisson_’s picture

Status: Needs work » Needs review
StatusFileSize
new6.23 KB

Discussed this yesterday with dawehner, instead of using reflection we're now using a subclass. Test currently fails though.

Interdiff is bigger than the patch so only uploading a patch.

mradcliffe’s picture

Is that the correct patch? It still has a reflection class and has useMbstring?

borisson_’s picture

StatusFileSize
new5.38 KB

@mradcliffe, thanks! This should be the correct patch.

Status: Needs review » Needs work

The last submitted patch, 21: cache_databasebackend-2560123-21.patch, failed testing.

The last submitted patch, 21: cache_databasebackend-2560123-21.patch, failed testing.

mradcliffe’s picture

  1. +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php
    @@ -421,6 +421,36 @@ protected function normalizeCid($cid) {
    +   * @return bool
    

    This is different from the "@return boolean" above. It may have the

  2. +++ b/core/modules/system/src/Tests/Cache/DatabaseBackendUnitTest.php
    @@ -53,4 +54,99 @@ public function testSetGet() {
    +class MockedDatabaseBackend extends DatabaseBackend {
    

    The database system has the precedent of using the Drupal\Tests\Core\Database\Stub namespace. Is that something that we want to replicate instead of having the stub in the test file? Would other tests find it useful? The MockedDatabaseBackend class in the test is fairly localized in its requirements to test the specific return values required for testing the existence of mbString so it may not have value enough on its own.

dawehner’s picture

According to the test failure in https://www.drupal.org/pift-ci-job/53967 the mb_check_encoding doesn't help anway, so we should maybe reconsider to hash it always.

stefan.r’s picture

@dawehner what do you mean, doesn't help?

Hashing always would be a simpler solution, if I remember correctly the conditional hashing was to help with debugging or perhaps for performance reasons, maybe @catch would remember?

dawehner’s picture

@dawehner what do you mean, doesn't help?

Well, it seems to not hash it, see the error message, even https://3v4l.org/N9u3X indicates it should

catch’s picture

Not hashing all the time was for debugging I think, but if it's unreliable, then we can skip the debugging aid - there are other ways to debug cache items.

athakural’s picture

#7 worked for me.. Thanks!

Pete B’s picture

There's also a reference to mb_check_encoding in normalizeLockName in core/lib/Drupal/Core/Cache/CacheCollector.php

protected function normalizeLockName($cid) {
    // Nothing to do if the ID is a US ASCII string of 255 characters or less.
    $cid_is_ascii = mb_check_encoding($cid, 'ASCII');
...

So we probably need a generic function to take care of checking whether mb_check_encoding is available.

I haven't spent enough time with D8 to suggest the best place for such a function though.

berdir’s picture

StatusFileSize
new1.08 KB

So based on some simple benchmarks, preg_match() seems to be about twice as slow as mb_check_encoding(). But as @dawehner pointed out, mb_check_encoding() is actually wrong in the example string from him. So what's the point of being faster if the result is wrong? ;)

What's interesting is that on PHP 5, hashing is slower than preg_match, but on PHP7, that's actually not the case. So from a performance perspective, on PHP7, it almost isn't worth checking, the only argument to do so is making debugging easier.

$ php bench/mbstring.php 
mb_check_encoding:
Total time: 0.9808s
Per call: 0.002ms
preg_match:
Total time: 1.7637s
Per call: 0.0035ms
hashing:
Total time: 1.9511s
Per call: 0.0039ms
$ php7 bench/mbstring.php 
mb_check_encoding:
Total time: 0.6707s
Per call: 0.0013ms
preg_match:
Total time: 1.303s
Per call: 0.0026ms
hashing:
Total time: 0.8716s
Per call: 0.0017ms
catch’s picture

So I personally use cids in the database backend for debugging at least against Drupal 7 - makes it very easy to see which cache entries are being created and how many etc.

You can get that from writing out somewhere but a lot of extra work compared to running some queries. So for me I'd go for the preg_match().

Once we require PHP 7 we might want to revisit, but to get very good performance, you shouldn't be using database caching anyway.

tstoeckler’s picture

Not sure per the above whether this should be using mb_check_encoding() anyway, but if we are providing a polyfill for that function in PHP (which at least the latest patch is doing) then I would have expected that to live in \Drupal\Component\Utility\Unicode as a new checkEncoding() method

dawehner’s picture

We already pull in the polyfill component of symfony, so we could leverage https://github.com/symfony/polyfill/blob/master/src/Mbstring/Mbstring.php even I still think, its pointless to do so, when it doesn't work in the first place.

stefan.r’s picture

@dawehner/@Berdir I still dont understand why this is wrong?

mb_check_encoding is merely supposed to check if the string is an ascii string, which it does a good job of in the 3v4l example -- it's not supposed to hash anything. Only if the string is not ascii do we need to hash it, but that happens elsewhere.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

gnuget’s picture

I'm Agreed with @stefan the 3v4l example seems correct to me as well.

So having vendor/symfony/polyfill-mbstring/Mbstring.php we should change the approach to fix this problem? we would need to test if the extension is loaded and if it is not the case then include this class?

dawehner’s picture

@stefan.r
Well, we somehow have to explain the weird test failure above, don't we?

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

catch’s picture

Title: Cache DatabaseBackend depends on mbstring » mbstring_check_encoding() in cache database backend doesn't actually work
Priority: Major » Normal
Issue tags: +Bug Smash Initiative

Per #2547419: Drupal requires mbstring PHP extension during install phase we now ship Symfony's mbstring polyfill with core, so this should no-longer fatal. But if the check itself isn't working we probably still need to do something here.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

sboden’s picture

I just upgraded from Drupal 10 to Drupal 11.2.3, and I still get the error:

Deprecated function: mb_check_encoding(): Calling mb_check_encoding() without argument is deprecated in Drupal\Core\Cache\DatabaseBackend->normalizeCid() (line 501 of core/lib/Drupal/Core/Cache/DatabaseBackend.php).
Drupal\Core\Cache\DatabaseBackend->normalizeCid() (Line: 120)
Drupal\Core\Cache\DatabaseBackend->getMultiple() (Line: 110)
Drupal\Core\Cache\DatabaseBackend->get() (Line: 326)
Drupal\Core\Cache\CacheCollector->lazyLoadCache() (Line: 144)
Drupal\Core\Cache\CacheCollector->get() (Line: 112)
sboden’s picture

StatusFileSize
new986 bytes

The patch for Drupal 11.2.3, possible Drupal 11.x

And the snippet to include it in composer.json "cweagans" patching under "drupal/core" section:
"2560123: Fix mb_check_encoding deprecation in DatabaseBackend.php": "https://www.drupal.org/files/issues/2025-08-10/cache_databasebackend-2560123-53.patch",

catch’s picture

mb_check_encoding() shouldn't be called without arguments - that suggests NULL is being passed as a cache ID?

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.