Merge branch 'w10_MDL-44358_m27_stylesrefact' of https://github.com/skodak/moodle

This commit is contained in:
Dan Poltawski 2014-03-04 07:38:56 +08:00
commit d129de3761
5 changed files with 282 additions and 283 deletions

View file

@ -15,10 +15,12 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains CSS related class, and function for the CSS optimiser
* This file contains CSS related class, and function for the CSS optimiser.
*
* Please see the {@link css_optimiser} class for greater detail.
*
* NOTE: these functions are not expected to be used from any addons.
*
* @package core
* @subpackage cssoptimiser
* @copyright 2012 Sam Hemelryk
@ -27,6 +29,12 @@
defined('MOODLE_INTERNAL') || die();
if (!defined('THEME_DESIGNER_CACHE_LIFETIME')) {
// This can be also set in config.php file,
// it needs to be higher than the time it takes to generate all CSS content.
define('THEME_DESIGNER_CACHE_LIFETIME', 10);
}
/**
* Stores CSS in a file at the given path.
*
@ -34,50 +42,15 @@ defined('MOODLE_INTERNAL') || die();
*
* @param theme_config $theme The theme that the CSS belongs to.
* @param string $csspath The path to store the CSS at.
* @param array $cssfiles The CSS files to store.
* @param string $csscontent the complete CSS in one string
* @param bool $chunk If set to true these files will be chunked to ensure
* that no one file contains more than 4095 selectors.
* @param string $chunkurl If the CSS is be chunked then we need to know the URL
* to use for the chunked files.
*/
function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk = false, $chunkurl = null) {
function css_store_css(theme_config $theme, $csspath, $csscontent, $chunk = false, $chunkurl = null) {
global $CFG;
$css = '';
foreach ($cssfiles as $file) {
$css .= file_get_contents($file)."\n";
}
// Check if both the CSS optimiser is enabled and the theme supports it.
if (!empty($CFG->enablecssoptimiser) && $theme->supportscssoptimisation) {
// This is an experimental feature introduced in Moodle 2.3
// The CSS optimiser organises the CSS in order to reduce the overall number
// of rules and styles being sent to the client. It does this by collating
// the CSS before it is cached removing excess styles and rules and stripping
// out any extraneous content such as comments and empty rules.
$optimiser = new css_optimiser;
$css = $theme->post_process($css);
$css = $optimiser->process($css);
// If cssoptimisestats is set then stats from the optimisation are collected
// and output at the beginning of the CSS.
if (!empty($CFG->cssoptimiserstats)) {
$css = $optimiser->output_stats_css().$css;
}
} else {
// This is the default behaviour.
// The cssoptimise setting was introduced in Moodle 2.3 and will hopefully
// in the future be changed from an experimental setting to the default.
// The css_minify_css will method will use the Minify library remove
// comments, additional whitespace and other minor measures to reduce the
// the overall CSS being sent.
// However it has the distinct disadvantage of having to minify the CSS
// before running the post process functions. Potentially things may break
// here if theme designers try to push things with CSS post processing.
$css = $theme->post_process($css);
$css = core_minify::css($css);
}
clearstatcache();
if (!file_exists(dirname($csspath))) {
@mkdir(dirname($csspath), $CFG->directorypermissions, true);
@ -88,11 +61,11 @@ function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk =
ignore_user_abort(true);
// First up write out the single file for all those using decent browsers.
css_write_file($csspath, $css);
css_write_file($csspath, $csscontent);
if ($chunk) {
// If we need to chunk the CSS for browsers that are sub-par.
$css = css_chunk_by_selector_count($css, $chunkurl);
$css = css_chunk_by_selector_count($csscontent, $chunkurl);
$files = count($css);
$count = 1;
foreach ($css as $content) {
@ -240,6 +213,32 @@ function css_send_cached_css($csspath, $etag) {
die;
}
/**
* Sends a cached CSS content
*
* @param string $csscontent The actual CSS markup.
* @param string $etag The revision to make sure we utilise any caches.
*/
function css_send_cached_css_content($csscontent, $etag) {
// 60 days only - the revision may get incremented quite often.
$lifetime = 60*60*24*60;
header('Etag: "'.$etag.'"');
header('Content-Disposition: inline; filename="styles.php"');
header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
header('Pragma: ');
header('Cache-Control: public, max-age='.$lifetime);
header('Accept-Ranges: none');
header('Content-Type: text/css; charset=utf-8');
if (!min_enable_zlib_compression()) {
header('Content-Length: '.strlen($csscontent));
}
echo($csscontent);
die;
}
/**
* Sends CSS directly without caching it.
*

View file

@ -55,6 +55,11 @@ function theme_reset_all_caches() {
set_config('themerev', $next); // time is unique even when you reset/switch database
if (!empty($CFG->themedesignermode)) {
$cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner');
$cache->purge();
}
if ($PAGE) {
$PAGE->reload_theme();
}
@ -66,8 +71,9 @@ function theme_reset_all_caches() {
* @param bool $state
*/
function theme_set_designer_mod($state) {
theme_reset_all_caches();
set_config('themedesignermode', (int)!empty($state));
// Reset caches after switching mode so that any designer mode caches get purged too.
theme_reset_all_caches();
}
/**
@ -594,14 +600,12 @@ class theme_config {
/**
* Returns the content of the CSS to be used in editor content
*
* @return string
* @return array
*/
public function editor_css_files() {
global $CFG;
$files = array();
// first editor plugins
// First editor plugins.
$plugins = core_component::get_plugin_list('editor');
foreach ($plugins as $plugin=>$fulldir) {
$sheetfile = "$fulldir/editor_styles.css";
@ -609,8 +613,8 @@ class theme_config {
$files['plugin_'.$plugin] = $sheetfile;
}
}
// then parent themes
foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
// Then parent themes - base first, the immediate parent last.
foreach (array_reverse($this->parent_configs) as $parent_config) {
if (empty($parent_config->editor_sheets)) {
continue;
}
@ -621,7 +625,7 @@ class theme_config {
}
}
}
// finally this theme
// Finally this theme.
if (!empty($this->editor_sheets)) {
foreach ($this->editor_sheets as $sheet) {
$sheetfile = "$this->dir/style/$sheet.css";
@ -635,10 +639,10 @@ class theme_config {
}
/**
* Get the stylesheet URL of this theme
* Get the stylesheet URL of this theme.
*
* @param moodle_page $page Not used... deprecated?
* @return array of moodle_url
* @return moodle_url[]
*/
public function css_urls(moodle_page $page) {
global $CFG;
@ -679,50 +683,7 @@ class theme_config {
$urls[] = $url;
} else {
// find out the current CSS and cache it now for 5 seconds
// the point is to construct the CSS only once and pass it through the
// dataroot to the script that actually serves the sheets
if (!defined('THEME_DESIGNER_CACHE_LIFETIME')) {
define('THEME_DESIGNER_CACHE_LIFETIME', 4); // this can be also set in config.php
}
$candidatedir = "$CFG->cachedir/theme/$this->name";
if ($svg) {
$candidatesheet = "$candidatedir/designer.ser";
} else {
$candidatesheet = "$candidatedir/designer_nosvg.ser";
}
$rebuild = true;
if (file_exists($candidatesheet) and filemtime($candidatesheet) > time() - THEME_DESIGNER_CACHE_LIFETIME) {
if ($css = file_get_contents($candidatesheet)) {
$css = unserialize($css);
if (is_array($css)) {
$rebuild = false;
}
}
}
if ($rebuild) {
// Prepare the CSS optimiser if it is to be used,
// please note that it may be very slow and is therefore strongly discouraged in theme designer mode.
$optimiser = null;
if (!empty($CFG->enablecssoptimiser) && $this->supportscssoptimisation) {
require_once($CFG->dirroot.'/lib/csslib.php');
$optimiser = new css_optimiser;
}
$css = $this->css_content($optimiser);
// We do not want any errors here because this may fail easily because of the concurrent access.
$prevabort = ignore_user_abort(true);
check_dir_exists($candidatedir);
$tempfile = tempnam($candidatedir, 'tmpdesigner');
file_put_contents($tempfile, serialize($css));
$reporting = error_reporting(0);
chmod($tempfile, $CFG->filepermissions);
unlink($candidatesheet); // Do not rely on rename() deleting original, they may decide to change it at any time as usually.
rename($tempfile, $candidatesheet);
error_reporting($reporting);
ignore_user_abort($prevabort);
}
$css = $this->get_css_files(true);
$baseurl = new moodle_url($CFG->httpswwwroot.'/theme/styles_debug.php');
if (!$svg) {
// We add an SVG param so that we know not to serve SVG images.
@ -730,10 +691,10 @@ class theme_config {
$baseurl->param('svg', '0');
}
if (core_useragent::is_ie()) {
// lalala, IE does not allow more than 31 linked CSS files from main document
// Lalala, IE does not allow more than 31 linked CSS files from main document.
$urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins'));
foreach ($css['parents'] as $parent=>$sheets) {
// We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096)
// We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096).
$urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent));
}
$urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme'));
@ -748,7 +709,8 @@ class theme_config {
}
}
foreach ($css['theme'] as $sheet=>$unused) {
$urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme')); // sheet first in order to make long urls easier to read
// Sheet first in order to make long urls easier to read.
$urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme'));
}
}
}
@ -757,14 +719,169 @@ class theme_config {
}
/**
* Returns an array of organised CSS files required for this output
* Get the whole css stylesheet for production mode.
*
* @return array
* NOTE: this method is not expected to be used from any addons.
*
* @return string CSS markup, already optimised and compressed
*/
public function css_files() {
public function get_css_content() {
global $CFG;
require_once($CFG->dirroot.'/lib/csslib.php');
$csscontent = '';
foreach ($this->get_css_files(false) as $value) {
foreach ($value as $val) {
if (is_array($val)) {
foreach ($val as $v) {
$csscontent .= file_get_contents($v) . "\n";
}
} else {
$csscontent .= file_get_contents($val) . "\n";
}
}
}
$csscontent = $this->post_process($csscontent);
if (!empty($CFG->enablecssoptimiser) && $this->supportscssoptimisation) {
// This is an experimental feature introduced in Moodle 2.3
// The CSS optimiser organises the CSS in order to reduce the overall number
// of rules and styles being sent to the client. It does this by collating
// the CSS before it is cached removing excess styles and rules and stripping
// out any extraneous content such as comments and empty rules.
$optimiser = new css_optimiser();
$csscontent = $optimiser->process($csscontent);
} else {
$csscontent = core_minify::css($csscontent);
}
return $csscontent;
}
/**
* Get the theme designer css markup,
* the parameters are coming from css_urls().
*
* NOTE: this method is not expected to be used from any addons.
*
* @param string $type
* @param string $subtype
* @param string $sheet
* @return string CSS markup
*/
public function get_css_content_debug($type, $subtype, $sheet) {
global $CFG;
require_once($CFG->dirroot.'/lib/csslib.php');
$optimiser = null;
if (!empty($CFG->enablecssoptimiser) && $this->supportscssoptimisation) {
// This is an experimental feature introduced in Moodle 2.3
// The CSS optimiser organises the CSS in order to reduce the overall number
// of rules and styles being sent to the client. It does this by collating
// the CSS before it is cached removing excess styles and rules and stripping
// out any extraneous content such as comments and empty rules.
$optimiser = new css_optimiser();
}
$cssfiles = array();
$css = $this->get_css_files(true);
if ($type === 'ie') {
// IE is a sloppy browser with weird limits, sorry.
if ($subtype === 'plugins') {
$cssfiles = $css['plugins'];
} else if ($subtype === 'parents') {
if (empty($sheet)) {
// Do not bother with the empty parent here.
} else {
// Build up the CSS for that parent so we can serve it as one file.
foreach ($css[$subtype][$sheet] as $parent => $css) {
$cssfiles[] = $css;
}
}
} else if ($subtype === 'theme') {
$cssfiles = $css['theme'];
}
} else if ($type === 'plugin') {
if (isset($css['plugins'][$subtype])) {
$cssfiles[] = $css['plugins'][$subtype];
}
} else if ($type === 'parent') {
if (isset($css['parents'][$subtype][$sheet])) {
$cssfiles[] = $css['parents'][$subtype][$sheet];
}
} else if ($type === 'theme') {
if (isset($css['theme'][$sheet])) {
$cssfiles[] = $css['theme'][$sheet];
}
}
$csscontent = '';
foreach ($cssfiles as $file) {
$contents = file_get_contents($file);
$contents = $this->post_process($contents);
$comment = "/** Path: $type $subtype $sheet.' **/\n";
$stats = '';
if ($optimiser) {
$contents = $optimiser->process($contents);
if (!empty($CFG->cssoptimiserstats)) {
$stats = $optimiser->output_stats_css();
}
}
$csscontent .= $comment.$stats.$contents."\n\n";
}
return $csscontent;
}
/**
* Get the whole css stylesheet for editor iframe.
*
* NOTE: this method is not expected to be used from any addons.
*
* @return string CSS markup
*/
public function get_css_content_editor() {
// Do not bother to optimise anything here, just very basic stuff.
$cssfiles = $this->editor_css_files();
$css = '';
foreach ($cssfiles as $file) {
$css .= file_get_contents($file)."\n";
}
return $this->post_process($css);
}
/**
* Returns an array of organised CSS files required for this output.
*
* @param bool $themedesigner
* @return array nested array of file paths
*/
protected function get_css_files($themedesigner) {
global $CFG;
$cache = null;
if ($themedesigner) {
require_once($CFG->dirroot.'/lib/csslib.php');
// We need some kind of caching here because otherwise the page navigation becomes
// way too slow in theme designer mode. Feel free to create full cache definition later...
$cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner', array('theme' => $this->name));
if ($files = $cache->get('cssfiles')) {
if ($files['created'] > time() - THEME_DESIGNER_CACHE_LIFETIME) {
unset($files['created']);
return $files;
}
}
}
$cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array());
// get all plugin sheets
// Get all plugin sheets.
$excludes = $this->resolve_excludes('plugins_exclude_sheets');
if ($excludes !== true) {
foreach (core_component::get_plugin_types() as $type=>$unused) {
@ -802,10 +919,10 @@ class theme_config {
}
}
// find out wanted parent sheets
// Find out wanted parent sheets.
$excludes = $this->resolve_excludes('parents_exclude_sheets');
if ($excludes !== true) {
foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
$parent = $parent_config->name;
if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) {
continue;
@ -823,7 +940,7 @@ class theme_config {
}
}
// current theme sheets
// Current theme sheets.
if (is_array($this->sheets)) {
foreach ($this->sheets as $sheet) {
$sheetfile = "$this->dir/style/$sheet.css";
@ -833,57 +950,15 @@ class theme_config {
}
}
if ($cache) {
$files = $cssfiles;
$files['created'] = time();
$cache->set('cssfiles', $files);
}
return $cssfiles;
}
/**
* Returns the content of the one huge CSS merged from all style sheets.
*
* @param css_optimiser|null $optimiser A CSS optimiser to use during on the content. Null = don't optimise
* @return string
*/
public function css_content(css_optimiser $optimiser = null) {
$files = array_merge($this->css_files(), array('editor'=>$this->editor_css_files()));
$css = $this->css_files_get_contents($files, array(), $optimiser);
return $css;
}
/**
* Given an array of file paths or a single file path loads the contents of
* the CSS file, processes it then returns it in the same structure it was given.
*
* Can be used recursively on the results of {@link css_files}
*
* @param array|string $file An array of file paths or a single file path
* @param array $keys An array of previous array keys [recursive addition]
* @param css_optimiser|null $optimiser A CSS optimiser to use during on the content. Null = don't optimise
* @return The converted array or the contents of the single file ($file type)
*/
protected function css_files_get_contents($file, array $keys, css_optimiser $optimiser = null) {
global $CFG;
if (is_array($file)) {
// We use a separate array to keep everything in the exact same order.
$return = array();
foreach ($file as $key=>$f) {
$return[clean_param($key, PARAM_SAFEDIR)] = $this->css_files_get_contents($f, array_merge($keys, array($key)), $optimiser);
}
return $return;
} else {
$contents = file_get_contents($file);
$contents = $this->post_process($contents);
$comment = '/** Path: '.implode(' ', $keys).' **/'."\n";
$stats = '';
if (!is_null($optimiser)) {
$contents = $optimiser->process($contents);
if (!empty($CFG->cssoptimiserstats)) {
$stats = $optimiser->output_stats_css();
}
}
return $comment.$stats.$contents;
}
}
/**
* Generate a URL to the file that serves theme JavaScript files.
*
@ -1359,7 +1434,6 @@ class theme_config {
/**
* Return true if we should look for SVG images as well.
*
* @staticvar bool $svg
* @return bool
*/
public function use_svg_icons() {