Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/theme/ -> styles.php (source)

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * This file is responsible for serving the one huge CSS of each theme.
  19   *
  20   * @package   core
  21   * @copyright 2009 Petr Skoda (skodak)  {@link http://skodak.org}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  // Disable moodle specific debug messages and any errors in output,
  26  // comment out when debugging or better look into error log!
  27  define('NO_DEBUG_DISPLAY', true);
  28  
  29  define('ABORT_AFTER_CONFIG', true);
  30  require('../config.php');
  31  require_once($CFG->dirroot.'/lib/csslib.php');
  32  
  33  if ($slashargument = min_get_slash_argument()) {
  34      $slashargument = ltrim($slashargument, '/');
  35      if (substr_count($slashargument, '/') < 2) {
  36          css_send_css_not_found();
  37      }
  38  
  39      if (strpos($slashargument, '_s/') === 0) {
  40          // Can't use SVG.
  41          $slashargument = substr($slashargument, 3);
  42          $usesvg = false;
  43      } else {
  44          $usesvg = true;
  45      }
  46  
  47      list($themename, $rev, $type) = explode('/', $slashargument, 3);
  48      $themename = min_clean_param($themename, 'SAFEDIR');
  49      $rev       = min_clean_param($rev, 'RAW');
  50      $type      = min_clean_param($type, 'SAFEDIR');
  51  
  52  } else {
  53      $themename = min_optional_param('theme', 'standard', 'SAFEDIR');
  54      $rev       = min_optional_param('rev', 0, 'RAW');
  55      $type      = min_optional_param('type', 'all', 'SAFEDIR');
  56      $usesvg    = (bool)min_optional_param('svg', '1', 'INT');
  57  }
  58  
  59  // Check if we received a theme sub revision which allows us
  60  // to handle local caching on a per theme basis.
  61  $values = explode('_', $rev);
  62  $rev = min_clean_param(array_shift($values), 'INT');
  63  $themesubrev = array_shift($values);
  64  
  65  if (!is_null($themesubrev)) {
  66      $themesubrev = min_clean_param($themesubrev, 'INT');
  67  }
  68  
  69  // Check that type fits into the expected values.
  70  if (!in_array($type, ['all', 'all-rtl', 'editor'])) {
  71      css_send_css_not_found();
  72  }
  73  
  74  if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
  75      // The theme exists in standard location - ok.
  76  } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {
  77      // Alternative theme location contains this theme - ok.
  78  } else {
  79      header('HTTP/1.0 404 not found');
  80      die('Theme was not found, sorry.');
  81  }
  82  
  83  $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
  84  $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg);
  85  $etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg);
  86  
  87  if (file_exists($candidatesheet)) {
  88      if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  89          // We do not actually need to verify the etag value because our files
  90          // never change in cache because we increment the rev counter.
  91          css_send_unmodified(filemtime($candidatesheet), $etag);
  92      }
  93      css_send_cached_css($candidatesheet, $etag);
  94  }
  95  
  96  // Ok, now we need to start normal moodle script, we need to load all libs and $DB.
  97  define('ABORT_AFTER_CONFIG_CANCEL', true);
  98  
  99  define('NO_MOODLE_COOKIES', true); // Session not used here.
 100  define('NO_UPGRADE_CHECK', true);  // Ignore upgrade check.
 101  
 102  require("$CFG->dirroot/lib/setup.php");
 103  
 104  $theme = theme_config::load($themename);
 105  $theme->force_svg_use($usesvg);
 106  $theme->set_rtl_mode($type === 'all-rtl' ? true : false);
 107  
 108  $themerev = theme_get_revision();
 109  $currentthemesubrev = theme_get_sub_revision_for_theme($themename);
 110  
 111  $cache = true;
 112  // If the client is requesting a revision that doesn't match both
 113  // the global theme revision and the theme specific revision then
 114  // tell the browser not to cache this style sheet because it's
 115  // likely being regenerated.
 116  if ($themerev <= 0 or $themerev != $rev or $themesubrev != $currentthemesubrev) {
 117      $rev = $themerev;
 118      $themesubrev = $currentthemesubrev;
 119      $cache = false;
 120  
 121      $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
 122      $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg);
 123      $etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg);
 124  }
 125  
 126  make_localcache_directory('theme', false);
 127  
 128  if ($type === 'editor') {
 129      $csscontent = $theme->get_css_content_editor();
 130  
 131      if ($cache) {
 132          css_store_css($theme, $candidatesheet, $csscontent);
 133          css_send_cached_css($candidatesheet, $etag);
 134      } else {
 135          css_send_uncached_css($csscontent);
 136      }
 137  
 138  }
 139  
 140  if (($fallbacksheet = theme_styles_fallback_content($theme)) && !$theme->has_css_cached_content()) {
 141      // The theme is not yet available and a fallback is available.
 142      // Return the fallback immediately, specifying the Content-Length, then generate in the background.
 143      $css = file_get_contents($fallbacksheet);
 144      css_send_temporary_css($css);
 145  
 146      // The fallback content has now been sent.
 147      // There will be an attempt to generate the content, but it should not be served.
 148      // The Content-Length above means that the client will disregard it anyway.
 149      $sendaftergeneration = false;
 150  
 151      // There may be another client currently holding a lock and generating the stylesheet.
 152      // Use a very low lock timeout as the connection will be ended immediately afterwards.
 153      $locktimeout = 1;
 154  } else {
 155      // There is no fallback content to be issued here, therefore the generated content must be output.
 156      $sendaftergeneration = true;
 157  
 158      // Use a realistic lock timeout as the intention is to avoid lock contention.
 159      $locktimeout = rand(90, 120);
 160  }
 161  
 162  // Attempt to fetch the lock.
 163  $lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
 164  $lock = $lockfactory->get_lock($themename, $locktimeout);
 165  
 166  if ($sendaftergeneration || $lock) {
 167      // Either the lock was successful, or the lock was unsuccessful but the content *must* be sent.
 168  
 169      // The content does not exist locally.
 170      // Generate and save it.
 171      $candidatesheet = theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir);
 172  
 173      if ($lock) {
 174          $lock->release();
 175      }
 176  
 177      if ($sendaftergeneration) {
 178          if (!$cache) {
 179              // Do not pollute browser caches if invalid revision requested,
 180              // let's ignore legacy IE breakage here too.
 181              css_send_uncached_css(file_get_contents($candidatesheet));
 182  
 183          } else {
 184              // Real browsers - this is the expected result!
 185              css_send_cached_css($candidatesheet, $etag);
 186          }
 187      }
 188  }
 189  
 190  /**
 191   * Generate the theme CSS and store it.
 192   *
 193   * @param   theme_config    $theme The theme to be generated
 194   * @param   int             $rev The theme revision
 195   * @param   int             $themesubrev The theme sub-revision
 196   * @param   string          $candidatedir The directory that it should be stored in
 197   * @return  string          The path that the primary CSS was written to
 198   */
 199  function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir) {
 200      global $CFG;
 201      require_once("{$CFG->libdir}/filelib.php");
 202  
 203      // Generate the content first.
 204      if (!$csscontent = $theme->get_css_cached_content()) {
 205          $csscontent = $theme->get_css_content();
 206          $theme->set_css_content_cache($csscontent);
 207      }
 208  
 209      if ($theme->get_rtl_mode()) {
 210          $type = "all-rtl";
 211      } else {
 212          $type = "all";
 213      }
 214  
 215      // Determine the candidatesheet path.
 216      $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $theme->use_svg_icons());
 217  
 218      // Store the CSS.
 219      css_store_css($theme, $candidatesheet, $csscontent);
 220  
 221      // Store the fallback CSS in the temp directory.
 222      // This file is used as a fallback when waiting for a theme to compile and is not versioned in any way.
 223      $fallbacksheet = make_temp_directory("theme/{$theme->name}")
 224          . "/"
 225          . theme_styles_get_filename($type, 0, $theme->use_svg_icons());
 226      css_store_css($theme, $fallbacksheet, $csscontent);
 227  
 228      // Delete older revisions from localcache.
 229      $themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR);
 230      foreach ($themecachedirs as $localcachedir) {
 231          $cachedrev = [];
 232          preg_match("/\/theme\/([0-9]+)$/", $localcachedir, $cachedrev);
 233          $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0;
 234          if ($cachedrev > 0 && $cachedrev < $rev) {
 235              fulldelete($localcachedir);
 236          }
 237      }
 238  
 239      // Delete older theme subrevision CSS from localcache.
 240      $subrevfiles = glob("{$CFG->localcachedir}/theme/{$rev}/{$theme->name}/css/*.css");
 241      foreach ($subrevfiles as $subrevfile) {
 242          $cachedsubrev = [];
 243          preg_match("/_([0-9]+)\.([0-9]+\.)?css$/", $subrevfile, $cachedsubrev);
 244          $cachedsubrev = isset($cachedsubrev[1]) ? intval($cachedsubrev[1]) : 0;
 245          if ($cachedsubrev > 0 && $cachedsubrev < $themesubrev) {
 246              fulldelete($subrevfile);
 247          }
 248      }
 249  
 250      return $candidatesheet;
 251  }
 252  
 253  /**
 254   * Fetch the preferred fallback content location if available.
 255   *
 256   * @param   theme_config    $theme The theme to be generated
 257   * @return  string          The path to the fallback sheet on disk
 258   */
 259  function theme_styles_fallback_content($theme) {
 260      global $CFG;
 261  
 262      if (!$theme->usefallback) {
 263          // This theme does not support fallbacks.
 264          return false;
 265      }
 266  
 267      $type = $theme->get_rtl_mode() ? 'all-rtl' : 'all';
 268      $filename = theme_styles_get_filename($type);
 269  
 270      $fallbacksheet = "{$CFG->tempdir}/theme/{$theme->name}/{$filename}";
 271      if (file_exists($fallbacksheet)) {
 272          return $fallbacksheet;
 273      }
 274  
 275      return false;
 276  }
 277  
 278  /**
 279   * Get the filename for the specified configuration.
 280   *
 281   * @param   string  $type The requested sheet type
 282   * @param   int     $themesubrev The theme sub-revision
 283   * @param   bool    $usesvg Whether SVGs are allowed
 284   * @return  string  The filename for this sheet
 285   */
 286  function theme_styles_get_filename($type, $themesubrev = 0, $usesvg = true) {
 287      $filename = $type;
 288      $filename .= ($themesubrev > 0) ? "_{$themesubrev}" : '';
 289      $filename .= $usesvg ? '' : '-nosvg';
 290  
 291      return "{$filename}.css";
 292  }
 293  
 294  /**
 295   * Determine the correct etag for the specified configuration.
 296   *
 297   * @param   string  $themename The name of the theme
 298   * @param   int     $rev The revision number
 299   * @param   string  $type The requested sheet type
 300   * @param   int     $themesubrev The theme sub-revision
 301   * @param   bool    $usesvg Whether SVGs are allowed
 302   * @return  string  The etag to use for this request
 303   */
 304  function theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg) {
 305      $etag = [$rev, $themename, $type, $themesubrev];
 306  
 307      if (!$usesvg) {
 308          $etag[] = 'nosvg';
 309      }
 310  
 311      return sha1(implode('/', $etag));
 312  }