MDL-59148 theme: Use fallback if compiled version unavailable
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 6 Jun 2017 07:29:09 +0000 (15:29 +0800)
committerDavid Monllao <david.monllao@gmail.com>
Wed, 28 Jun 2017 16:24:50 +0000 (18:24 +0200)
lib/csslib.php
lib/outputlib.php
theme/boost/config.php
theme/more/config.php
theme/styles.php

index a7c5e40..bec0ed8 100644 (file)
@@ -329,6 +329,26 @@ function css_send_cached_css_content($csscontent, $etag) {
     die;
 }
 
+/**
+ * Sends CSS directly and disables all caching.
+ * The Content-Length of the body is also included, but the script is not ended.
+ *
+ * @param string $css The CSS content to send
+ * @param int $expiry The anticipated expiry of the file
+ */
+function css_send_temporary_css($css) {
+    header('Cache-Control: no-cache, no-store, must-revalidate');
+    header('Pragma: no-cache');
+    header('Expires: 0');
+    header('Content-Disposition: inline; filename="styles_debug.php"');
+    header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
+    header('Accept-Ranges: none');
+    header('Content-Type: text/css; charset=utf-8');
+    header('Content-Length: ' . strlen($css));
+
+    echo $css;
+}
+
 /**
  * Sends CSS directly without caching it.
  *
@@ -382,4 +402,4 @@ function css_send_unmodified($lastmodified, $etag) {
 function css_send_css_not_found() {
     header('HTTP/1.0 404 not found');
     die('CSS was not found, sorry.');
-}
\ No newline at end of file
+}
index d91e27e..ca3b355 100644 (file)
@@ -363,6 +363,12 @@ class theme_config {
      */
     public $editor_sheets = array();
 
+    /**
+     * @var bool Whether a fallback version of the stylesheet will be used
+     * whilst the final version is generated.
+     */
+    public $usefallback = false;
+
     /**
      * @var array The names of all the javascript files this theme that you would
      * like included from head, in order. Give the names of the files without .js.
@@ -724,7 +730,7 @@ class theme_config {
         }
 
         $configurable = array(
-            'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets',
+            'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'usefallback',
             'javascripts', 'javascripts_footer', 'parents_exclude_javascripts',
             'layouts', 'enable_dock', 'enablecourseajax', 'requiredblocks',
             'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
@@ -1102,6 +1108,19 @@ class theme_config {
         return $cache->set($key, $csscontent);
     }
 
+    /**
+     * Return whether the post processed CSS content has been cached.
+     *
+     * @return bool Whether the post-processed CSS is available in the cache.
+     */
+    public function has_css_cached_content() {
+
+        $key = $this->get_css_cache_key();
+        $cache = cache::make('core', 'postprocessedcss');
+
+        return $cache->has($key);
+    }
+
     /**
      * Return cached post processed CSS content.
      *
@@ -2218,6 +2237,15 @@ class theme_config {
         $this->rtlmode = $inrtl;
     }
 
+    /**
+     * Whether the theme is being served in RTL mode.
+     *
+     * @return bool True when in RTL mode.
+     */
+    public function get_rtl_mode() {
+        return $this->rtlmode;
+    }
+
     /**
      * Checks if file with any image extension exists.
      *
index 57e81c4..a628ff8 100644 (file)
@@ -29,6 +29,7 @@ require_once(__DIR__ . '/lib.php');
 $THEME->name = 'boost';
 $THEME->sheets = [];
 $THEME->editor_sheets = [];
+$THEME->usefallback = true;
 $THEME->scss = function($theme) {
     return theme_boost_get_main_scss_content($theme);
 };
index 82c9073..09991ee 100644 (file)
@@ -28,6 +28,7 @@ $THEME->parents = array('clean', 'bootstrapbase');
 $THEME->doctype = 'html5';
 $THEME->sheets = array('custom');
 $THEME->lessfile = 'moodle';
+$THEME->usefallback = true;
 $THEME->parents_exclude_sheets = array('bootstrapbase' => array('moodle'), 'clean' => array('custom'));
 $THEME->lessvariablescallback = 'theme_more_less_variables';
 $THEME->extralesscallback = 'theme_more_extra_less';
index 85be315..4977095 100644 (file)
@@ -77,6 +77,7 @@ if (is_null($themesubrev)) {
     $themesubrev = min_clean_param($themesubrev, 'INT');
 }
 
+// Check that type fits into the expected values.
 if ($type === 'editor') {
     // The editor CSS is never chunked.
     $chunk = null;
@@ -96,25 +97,9 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
 }
 
 $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
-$etag = "$rev/$themename/$type/$themesubrev";
-$candidatesheet = "{$candidatedir}/{$type}";
-if ($themesubrev > 0) {
-    $candidatesheet .= "_{$themesubrev}";
-}
-if (!$usesvg) {
-    // Add to the sheet name, one day we'll be able to just drop this.
-    $candidatesheet .= "-nosvg";
-    $etag .= '/nosvg';
-}
-
-if ($chunk !== null) {
-    $etag .= "/chunk{$chunk}";
-    $chunkedcandidatesheet = "{$candidatesheet}.{$chunk}.css";
-    $candidatesheet = "{$candidatesheet}.css";
-} else {
-    $candidatesheet = $chunkedcandidatesheet = "{$candidatesheet}.css";
-}
-$etag = sha1($etag);
+$candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg);
+$chunkedcandidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg, $chunk);
+$etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg, $chunk);
 
 if (file_exists($chunkedcandidatesheet)) {
     if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
@@ -144,31 +129,15 @@ $cache = true;
 // If the client is requesting a revision that doesn't match both
 // the global theme revision and the theme specific revision then
 // tell the browser not to cache this style sheet because it's
-// likely being regnerated.
+// likely being regenerated.
 if ($themerev <= 0 or $themerev != $rev or $themesubrev != $currentthemesubrev) {
     $rev = $themerev;
     $cache = false;
 
     $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
-    $etag = "$rev/$themename/$type/$themesubrev";
-    $candidatesheet = "{$candidatedir}/{$type}";
-    if ($themesubrev > 0) {
-        $candidatesheet .= "_{$themesubrev}";
-    }
-    if (!$usesvg) {
-        // Add to the sheet name, one day we'll be able to just drop this.
-        $candidatesheet .= "-nosvg";
-        $etag .= '/nosvg';
-    }
-
-    if ($chunk !== null) {
-        $etag .= "/chunk{$chunk}";
-        $chunkedcandidatesheet = "{$candidatesheet}.{$chunk}.css";
-        $candidatesheet = "{$candidatesheet}.css";
-    } else {
-        $candidatesheet = $chunkedcandidatesheet = "{$candidatesheet}.css";
-    }
-    $etag = sha1($etag);
+    $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg);
+    $chunkedcandidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg, $chunk);
+    $etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg, $chunk);
 }
 
 make_localcache_directory('theme', false);
@@ -177,65 +146,191 @@ if ($type === 'editor') {
     $csscontent = $theme->get_css_content_editor();
     css_store_css($theme, $candidatesheet, $csscontent, false);
 
+    if ($cache) {
+        css_send_cached_css($candidatesheet, $etag);
+    } else {
+        css_send_uncached_css(file_get_contents($candidatesheet));
+    }
+
+}
+
+if (($fallbacksheet = theme_styles_fallback_content($theme)) && !$theme->has_css_cached_content()) {
+    // The theme is not yet available and a fallback is available.
+    // Return the fallback immediately, specifying the Content-Length, then generate in the background.
+    $css = file_get_contents($fallbacksheet);
+    css_send_temporary_css($css);
+
+    // The fallback content has now been sent.
+    // There will be an attempt to generate the content, but it should not be served.
+    // The Content-Length above means that the client will disregard it anyway.
+    $sendaftergeneration = false;
+
+    // There may be another client currently holding a lock and generating the stylesheet.
+    // Use a very low lock timeout as the connection will be ended immediately afterwards.
+    $locktimeout = 1;
 } else {
-    // Fetch a lock whilst the CSS is fetched as this can be slow and CPU intensive.
-    // Each client should wait for one to finish the compilation before starting the compiler.
-    $lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
-    $lock = $lockfactory->get_lock($themename, rand(90, 120));
-
-    if (file_exists($chunkedcandidatesheet)) {
-        // The file was built while we waited for the lock, we release the lock and serve the file.
-        if ($lock) {
-            $lock->release();
-        }
+    // There is no fallback content to be issued here, therefore the generated content must be output.
+    $sendaftergeneration = true;
+
+    // Use a realistic lock timeout as the intention is to avoid lock contention.
+    $locktimeout = rand(90, 120);
+}
+
+// Attempt to fetch the lock.
+$lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
+$lock = $lockfactory->get_lock($themename, $locktimeout);
 
-        if ($cache) {
+if ($sendaftergeneration || $lock) {
+    // Either the lock was successful, or the lock was unsuccessful but the content *must* be sent.
+    if (!file_exists($chunkedcandidatesheet)) {
+        // The content does not exist locally.
+        // Generate and save it.
+        $candidatesheet = theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir);
+    }
+
+    if ($lock) {
+        $lock->release();
+    }
+
+    if ($sendaftergeneration) {
+        if (!$cache) {
+            // Do not pollute browser caches if invalid revision requested,
+            // let's ignore legacy IE breakage here too.
+            css_send_uncached_css(file_get_contents($candidatesheet));
+
+        } else if ($chunk !== null and file_exists($chunkedcandidatesheet)) {
+            // Greetings stupid legacy IEs!
             css_send_cached_css($chunkedcandidatesheet, $etag);
+
         } else {
-            css_send_uncached_css(file_get_contents($chunkedcandidatesheet));
+            // Real browsers - this is the expected result!
+            css_send_cached_css($candidatesheet, $etag);
         }
     }
+}
+
+/**
+ * Generate the theme CSS and store it.
+ *
+ * @param   theme_config    $theme The theme to be generated
+ * @param   int             $rev The theme revision
+ * @param   int             $themesubrev The theme sub-revision
+ * @param   string          $candidatedir The directory that it should be stored in
+ * @return  string          The path that the primary (non-chunked) CSS was written to
+ */
+function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir) {
+    global $CFG;
 
-    // The lock is still held, and the sheet still does not exist.
-    // Compile the CSS content.
+    // Generate the content first.
     if (!$csscontent = $theme->get_css_cached_content()) {
         $csscontent = $theme->get_css_content();
         $theme->set_css_content_cache($csscontent);
     }
 
+    if ($theme->get_rtl_mode()) {
+        $type = "all-rtl";
+    } else {
+        $type = "all";
+    }
+
+    // Determine the candidatesheet path.
+    // Note: Do not pass any value for chunking as this is calcualted during css storage.
+    $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $theme->use_svg_icons());
+
+    // Determine the chunking URL.
+    // Note, this will be removed when support for IE9 is removed.
     $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot);
-    if (!empty($slashargument)) {
-        if ($usesvg) {
-            $chunkurl = "{$relroot}/theme/styles.php/{$themename}/{$rev}/$type";
+    if (!empty(min_get_slash_argument())) {
+        if ($theme->use_svg_icons()) {
+            $chunkurl = "{$relroot}/theme/styles.php/{$theme->name}/{$rev}/$type";
         } else {
-            $chunkurl = "{$relroot}/theme/styles.php/_s/{$themename}/{$rev}/$type";
+            $chunkurl = "{$relroot}/theme/styles.php/_s/{$theme->name}/{$rev}/$type";
         }
     } else {
-        if ($usesvg) {
-            $chunkurl = "{$relroot}/theme/styles.php?theme={$themename}&rev={$rev}&type=$type";
+        if ($theme->use_svg_icons()) {
+            $chunkurl = "{$relroot}/theme/styles.php?theme={$theme->name}&rev={$rev}&type=$type";
         } else {
-            $chunkurl = "{$relroot}/theme/styles.php?theme={$themename}&rev={$rev}&type=$type&svg=0";
+            $chunkurl = "{$relroot}/theme/styles.php?theme={$theme->name}&rev={$rev}&type=$type&svg=0";
         }
     }
+
+    // Store the CSS.
     css_store_css($theme, $candidatesheet, $csscontent, true, $chunkurl);
 
-    if ($lock) {
-        // Now that the CSS has been generated and/or stored, release the lock.
-        // This will allow waiting clients to use the newly generated and stored CSS.
-        $lock->release();
+    // Store the fallback CSS in the temp directory.
+    // This file is used as a fallback when waiting for a theme to compile and is not versioned in any way.
+    $fallbacksheet = make_temp_directory("theme/{$theme->name}")
+        . "/"
+        . theme_styles_get_filename($type, 0, $theme->use_svg_icons());
+    css_store_css($theme, $fallbacksheet, $csscontent, true, $chunkurl);
+
+    return $candidatesheet;
+}
+
+/**
+ * Fetch the preferred fallback content location if available.
+ *
+ * @param   theme_config    $theme The theme to be generated
+ * @return  string          The path to the fallback sheet on disk
+ */
+function theme_styles_fallback_content($theme) {
+    global $CFG;
+
+    if (!$theme->usefallback) {
+        // This theme does not support fallbacks.
+        return false;
+    }
+
+    $type = $theme->get_rtl_mode() ? 'all-rtl' : 'all';
+    $filename = theme_styles_get_filename($type);
+
+    $fallbacksheet = "{$CFG->tempdir}/theme/{$theme->name}/{$filename}";
+    if (file_exists($fallbacksheet)) {
+        return $fallbacksheet;
     }
+
+    return false;
 }
 
-if (!$cache) {
-    // Do not pollute browser caches if invalid revision requested,
-    // let's ignore legacy IE breakage here too.
-    css_send_uncached_css($csscontent);
+/**
+ * Get the filename for the specified configuration.
+ *
+ * @param   string  $type The requested sheet type
+ * @param   int     $themesubrev The theme sub-revision
+ * @param   bool    $usesvg Whether SVGs are allowed
+ * @param   int     $chunk The chunk number if specified
+ * @return  string  The filename for this sheet
+ */
+function theme_styles_get_filename($type, $themesubrev = 0, $usesvg = true, $chunk = null) {
+    $filename = $type;
+    $filename .= ($themesubrev > 0) ? "_{$themesubrev}" : '';
+    $filename .= $usesvg ? '' : '-nosvg';
+    $filename .= $chunk ? ".{$chunk}" : '';
 
-} else if ($chunk !== null and file_exists($chunkedcandidatesheet)) {
-    // Greetings stupid legacy IEs!
-    css_send_cached_css($chunkedcandidatesheet, $etag);
+    return "{$filename}.css";
+}
 
-} else {
-    // Real browsers - this is the expected result!
-    css_send_cached_css_content($csscontent, $etag);
+/**
+ * Determine the correct etag for the specified configuration.
+ *
+ * @param   string  $themename The name of the theme
+ * @param   int     $rev The revision number
+ * @param   string  $type The requested sheet type
+ * @param   int     $themesubrev The theme sub-revision
+ * @param   bool    $usesvg Whether SVGs are allowed
+ * @param   int     $chunk The chunk number if specified
+ * @return  string  The etag to use for this request
+ */
+function theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg, $chunk) {
+    $etag = [$rev, $themename, $type, $themesubrev];
+
+    if (!$usesvg) {
+        $etag[] = 'nosvg';
+    }
+
+    if ($chunk) {
+        $etag[] = "chunk{$chunk}";
+    }
+
+    return sha1(implode('/', $etag));
 }