Bug #23520 Bug Fixed

Issue with creating new fields using the AJAX based modal sidebar in a Windows hosted Docker container

Version: 4.1.1 Reporter: martin.tzonev

When trying to create fields (possible other entities, I’m still new to EE, so I’m not sure) using the modal sidebar, with the File caching driver on, in a Linux Docker container running on Windows the modal hangs in “Saving” state because of a warning.

This happens because the structure of the nested directories for the cache that needs to be created is incorrect.

The save method of EE_Cache_file is trying to create the following directory structure:

/system/user/cache/default_site/EllisLab/ExpressionEngine/Model/Channel/ChannelField/

like this:

@mkdir($path, DIR_WRITE_MODE, TRUE);

Seeing the recursion argument of mkdir is set to TRUE it looks like these are supposed to be nested directories.

The $path argument is formed like this in the Cache class (/system/ee/legacy/libraries/Cache/Cache.php):

...
     // Make sure the key doesn't begin or end with a namespace separator or
     // directory separator to force the last segment of the key to be the
     // file name and so we can prefix a directory reliably
     $key = trim($key, Cache::NAMESPACE_SEPARATOR.DIRECTORY_SEPARATOR);

     // Replace all namespace separators with the system's directory separator
     $key = str_replace(Cache::NAMESPACE_SEPARATOR, DIRECTORY_SEPARATOR, $key);

     // Replace \ with _ if on Windows
     if (DIRECTORY_SEPARATOR == '\\')
     {
      $key = str_replace('\\', '_', $key);
     }
...

There seems to be a problem with Cache::NAMESPACE_SEPARATOR, as it is currently set to “/” (forward slash). In the context of namespacing this is incorrect and backslash should be used instead.

Because of this the $path for the caching directory results in:

/system/user/cache/default_site/EllisLab\ExpressionEngine\Model\Channel\ChannelField/ (note the incosistent slashes in the middle)

In Linux this ends up being a single, long-named directory which holds the actual cache files (basically works by accident), while on Windows the directory creation fails entirely which triggers a warning (if error reporting is on). That warning disrupts the expected JSON structure which ends up blocking the AJAX call resolution of the modal.

Suggested solution: Set Cache::NAMESPACE_SEPARATOR (/system/ee/legacy/libraries/Cache/Cache.php) to ‘\’ so it mirrors the actual structure of namespaces and is replaced properly.

  • Small correction: the correct value is double backslash for esacping purposes.

    martin.tzonev
    13th March, 2018 at 9:52am
  • The namespace separator is correct. It is not supposed to correlate to PHP namespacing, it’s its own thing.

    In Linux this ends up being a single, long-named directory which holds the actual cache files (basically works by accident)

    This is intentional.

    while on Windows the directory creation fails entirely which triggers a warning (if error reporting is on)

    Could you share the warning you are getting?

    I imagine you’re having issues because being in Docker, PHP probably thinks you’re in Linux but your filesystem is Windows.

    Kevin Cupp
    14th March, 2018 at 2:23pm
  • The namespace separator is correct. It is not supposed to correlate to PHP namespacing, it’s its own thing.

    If it is, why is it trying to make replacement against a string that is built using the PHP namespacing?

    This is intentional.

    I figured it wasn’t as mkdir was called with the recursive argument set to true which would normally create the directories in a nested structure.

    I imagine you’re having issues because being in Docker, PHP probably thinks you’re in Linux but your filesystem is Windows.

    Absolutely. The windows filesystem doesn’t like the backslashes so mkdir is failing and is triggering the warning.

    Could you share the warning you are getting?
    <div class="err-wrap warn">
        <h1>Warning</h1>
        <h2></h2>
        ee/legacy/libraries/Cache/drivers/Cache_file.php, line 103
        <ul>
            <li><b>Severity</b>: E_USER_WARNING</li>
        </ul>
    </div>

    This is triggered by the following code:

    // Create namespace directory if it doesn't exist
      if ( ! file_exists($path) OR ! is_dir($path))
      {
       @mkdir($path, DIR_WRITE_MODE, TRUE);
    
       // Grab the error if there was one
       $error = error_get_last();
    
       // If we had trouble creating the directory, it's likely due to a
       // concurrent process already having created it, so we'll check
       // to see if that's the case and if not, something else went wrong
       // and we'll show an error
       if ( ! is_dir($path) OR ! is_really_writable($path))
       {
        trigger_error($error['message'], E_USER_WARNING);
       }
       else
       {
        // Write an index.html file to ensure no directory indexing
        write_index_html($path);
       }
      }
    martin.tzonev
    14th March, 2018 at 8:18pm
  • If it is, why is it trying to make replacement against a string that is built using the PHP namespacing?

    Because the cache namespace separator is for creating these nested folders. So if someone sets a cache item with the key of some/cache/key, it will create folders then you can store individual keys under each namespace and then it’s easy to clear individual namespaces. But if the directory separator is different, then we want to make sure we update the character we use for namespace separation so that it writes to the filesystem correctly. But it has nothing to do with PHP namespaces.

    I figured it wasn’t as mkdir was called with the recursive argument set to true which would normally create the directories in a nested structure.

    It would, but it doesn’t mean we intend to create nested folders just because a PHP class name happens to have backslashes in it, sometimes we just use a class name as a unique cache key.

    What if we just always replace \\ with _ regardless of environment? I think that would fix this.

    Kevin Cupp
    14th March, 2018 at 9:03pm
  • What if we just always replace \ with _ regardless of environment? I think that would fix this.

    If the idea is to always have one long-named directory rather than nested structure I do believe that it’s better to have consistent behavior regardless of the environment.

    Currently this is done only in the Windows workaround, but from my point of view it’s not working becuase the script that does the replacing looks like this:

    // Replace all namespace separators with the system's directory separator
    $key = str_replace(Cache::NAMESPACE_SEPARATOR, DIRECTORY_SEPARATOR, $key);
    
    // Replace \ with _ if on Windows
    if (DIRECTORY_SEPARATOR == '\\')
    {
        $key = str_replace('\\', '_', $key);
    }

    but we also have this:

    const NAMESPACE_SEPARATOR = '/';

    The first separator replacement does nothing, because our string looks like this:

    EllisLab\\ExpressionEngine\\Model\\Channel\\ChannelField

    It’s trying to replace forward slash where there are only back-slashes. In my environment the second replacing doesn’t trigger as my system’s DIRECTORY_SEPARATOR isn’t the Windows one.

    TL;DR - if the script always makes sure to replace any separator on any environment with “_” things should become a lot more consistent and reliable.

    martin.tzonev
    15th March, 2018 at 5:26am
  • Yes I understand the problem. Again, the first replacement isn’t intended to do anything on a cache key of that name, it’s not for PHP namespaces. I’ll remove that if (DIRECTORY_SEPARATOR == '\\') conditional and will suspect that this should be fixed for you. Give that a try on your end to verify and get you going again, if you like.

    Kevin Cupp
    15th March, 2018 at 9:18am
  • And actually, you’ll need to move that replacement up a little bit. Your new method should look like this:

    protected function _namespaced_key($key, $scope = Cache::LOCAL_SCOPE)
    {
        // Make sure the key doesn't begin or end with a namespace separator or
        // directory separator to force the last segment of the key to be the
        // file name and so we can prefix a directory reliably
        $key = trim($key, Cache::NAMESPACE_SEPARATOR.DIRECTORY_SEPARATOR);
    
        // Sometime class names are used as keys, replace class namespace
        // slashes with underscore to prevent filesystem issues
        $key = str_replace('\\', '_', $key);
    
        // Replace all namespace separators with the system's directory separator
        $key = str_replace(Cache::NAMESPACE_SEPARATOR, DIRECTORY_SEPARATOR, $key);
    
        // For locally-cached items, separate by site name
        if ($scope == Cache::LOCAL_SCOPE)
        {
          $key = ee()->config->item('site_short_name') . DIRECTORY_SEPARATOR . $key;
        }
    
        return $key;
    }
    Kevin Cupp
    15th March, 2018 at 9:22am
  • Works like a charm 😊

    martin.tzonev
    15th March, 2018 at 9:28am

You must be signed in to comment on a bug report.

ExpressionEngine News

#eecms, #events, #releases