Possibly wrong use of static caching (drupal_static) for dynamic variables

Created on 11 November 2021, about 3 years ago
Updated 8 October 2024, 2 months ago

Problem/Motivation

I'm not 100% sure but I think I found a bug in the current caching implementation of the _etracker_XXX_should_be_tracked implementations.

While the "parameter" like path, role or user given to the method is implicit (determined in the function), the caching will always return the same value from drupal_static() even if the user or path is not the same.

I guess this is just theoretical and minor therefore, as I think drupal_static() is typically "cleared" for each new request(?) but there may be caching modules or mechanisms which persist the values over requests and in that case, the results may definitely become wrong!

See this code:

/**
 * Tracking visibility check for pages.
 *
 * Based on visibility setting this function returns TRUE if JS code should
 * be added to the current page and otherwise FALSE.
 *
 * @param \Drupal\Core\Config\ImmutableConfig $config
 *
 * @return array|bool
 */
function _etracker_path_should_be_tracked(ImmutableConfig $config) {
  $path_should_be_tracked = &drupal_static(__FUNCTION__);

  // Cache tracking result if function is called more than once.
  if (!isset($path_should_be_tracked)) {

    $visible_path_mode = $config->get('etracker_track_path_mode');
    $visible_path_pages = $config->get('etracker_track_paths');

    // Match path if necessary.
    if (!empty($visible_path_pages)) {
      // Convert path to lowercase. This allows comparison of the same path
      // with different case. Ex: /Page, /page, /PAGE.
      $pages = mb_strtolower($visible_path_pages);

      $path = Drupal::service('path.current')->getPath();
      $path_alias = \Drupal::service('path_alias.manager')->getAliasByPath($path);
      if (empty($path_alias)) {
        $path_alias = mb_strtolower($path);
      }
      else {
        $path_alias = mb_strtolower($path_alias);
      }
      $page_match = \Drupal::service('path.matcher')->matchPath($path_alias, $pages) || (($path != $path_alias) && \Drupal::service('path.matcher')->matchPath($path, $pages));
      // When $visible_path_mode has a value of 'all_pages', the tracking
      // code is displayed on all pages except those listed in $pages. When set
      // to 'all_listed', it is displayed only on those pages listed in $pages.
      $track_all_paths = ($visible_path_mode == Constants::ETRACKER_TRACK_PATHS_MODE_ALL);
      $path_should_be_tracked = ($track_all_paths xor $page_match);
    }
    else {
      $path_should_be_tracked = TRUE;
    }
  }
  return $path_should_be_tracked;
}

with more explicit dependencies (as function parameters) the $path would be a parameter passed to the function. Currently it's fetched in the function but that doesn't mean it can't change, as it's still an external value.

But due to
&drupal_static(__FUNCTION__);
drupal_static will always return the first result, even if path is no more the same at second call. Typically the functions result SHOULD BE different for a different path and only be cached for that path, see https://drupal.psu.edu/blog/post/implementing-drupalstatic-function-dyna....

So correct this line should be:
&drupal_static(__FUNCTION__ . $path);
with the $path given from the calling function.

See https://api.drupal.org/api/drupal/core%21includes%21bootstrap.inc/functi...

Sidenote:
Keep in mind that it might still make sense to nest the conditions, for example only fetch the user or path, if the key is set and the other function didn't return false. For performance reasons.

Steps to reproduce

Proposed resolution

Remaining tasks

User interface changes

API changes

Data model changes

🐛 Bug report
Status

Active

Version

3.0

Component

Code

Created by

🇩🇪Germany Anybody Porta Westfalica

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • First commit to issue fork.
  • 🇩🇪Germany sunlix Wesel

    Hmm this is my very first contact with drupal_static.
    I have adjusted the code to store the boolean in the $path wise manner.

    I hope anyone can review and help here out. I am unsure if I got it right. :)

  • 🇩🇪Germany Anybody Porta Westfalica

    Thanks @sunlix! Yes as written above, this is very theoretical, so this way or another, both implementations won't affect anyone. But your correction makes this logically correct.

    If the functionality still works as before, code-wise this looks correct. I just wasn't totally sure about the new array structure and key, but I think drupal_static can handle this.

    BTW drupal_static is super helpful for functions that are called many times per request with equal parameters and results.

    So RTBC+1 from code review. But we should keep calm as this is minor... Feel free to RTBC if it simply works! :)

  • 🇩🇪Germany sunlix Wesel

    The only uncertainty I have is on the isset check.
    Should I change this to:

    before:
    if (!isset($path_should_be_tracked))

    after:
    if (!isset($path_should_be_tracked[$path]))

    I have researched on other contrib modules like paragraphs permissions.
    There is a direct check on if (!isset($path_should_be_tracked[$path])) instead the general one.

  • 🇩🇪Germany Anybody Porta Westfalica

    Yup makes sense!

  • 🇩🇪Germany sunlix Wesel

    Changed.

Production build 0.71.5 2024