1<?php 2/** 3 * DokuWiki StyleSheet creator 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9if(!defined('DOKU_INC')) define('DOKU_INC',dirname(__FILE__).'/../../'); 10if(!defined('NOSESSION')) define('NOSESSION',true); // we do not use a session or authentication here (better caching) 11if(!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT',1); // we gzip ourself here 12if(!defined('NL')) define('NL',"\n"); 13require_once(DOKU_INC.'inc/init.php'); 14 15// Main (don't run when UNIT test) 16if(!defined('SIMPLE_TEST')){ 17 header('Content-Type: text/css; charset=utf-8'); 18 css_out(); 19} 20 21 22// ---------------------- functions ------------------------------ 23 24/** 25 * Output all needed Styles 26 * 27 * @author Andreas Gohr <andi@splitbrain.org> 28 */ 29function css_out(){ 30 global $conf; 31 global $lang; 32 global $config_cascade; 33 global $INPUT; 34 35 if ($INPUT->str('s') == 'feed') { 36 $mediatypes = array('feed'); 37 $type = 'feed'; 38 } else { 39 $mediatypes = array('screen', 'all', 'print'); 40 $type = ''; 41 } 42 43 // decide from where to get the template 44 $tpl = trim(preg_replace('/[^\w-]+/','',$INPUT->str('t'))); 45 if(!$tpl) $tpl = $conf['template']; 46 47 // The generated script depends on some dynamic options 48 $cache = new cache('styles'.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'].DOKU_BASE.$tpl.$type,'.css'); 49 50 // load styl.ini 51 $styleini = css_styleini($tpl); 52 53 // if old 'default' userstyle setting exists, make it 'screen' userstyle for backwards compatibility 54 if (isset($config_cascade['userstyle']['default'])) { 55 $config_cascade['userstyle']['screen'] = $config_cascade['userstyle']['default']; 56 } 57 58 // cache influencers 59 $tplinc = tpl_basedir($tpl); 60 $cache_files = getConfigFiles('main'); 61 $cache_files[] = $tplinc.'style.ini'; 62 $cache_files[] = DOKU_CONF."tpl/$tpl/style.ini"; 63 $cache_files[] = __FILE__; 64 65 // Array of needed files and their web locations, the latter ones 66 // are needed to fix relative paths in the stylesheets 67 $files = array(); 68 foreach($mediatypes as $mediatype) { 69 $files[$mediatype] = array(); 70 // load core styles 71 $files[$mediatype][DOKU_INC.'lib/styles/'.$mediatype.'.css'] = DOKU_BASE.'lib/styles/'; 72 // load jQuery-UI theme 73 if ($mediatype == 'screen') { 74 $files[$mediatype][DOKU_INC.'lib/scripts/jquery/jquery-ui-theme/smoothness.css'] = DOKU_BASE.'lib/scripts/jquery/jquery-ui-theme/'; 75 } 76 // load plugin styles 77 $files[$mediatype] = array_merge($files[$mediatype], css_pluginstyles($mediatype)); 78 // load template styles 79 if (isset($styleini['stylesheets'][$mediatype])) { 80 $files[$mediatype] = array_merge($files[$mediatype], $styleini['stylesheets'][$mediatype]); 81 } 82 // load user styles 83 if(isset($config_cascade['userstyle'][$mediatype])){ 84 $files[$mediatype][$config_cascade['userstyle'][$mediatype]] = DOKU_BASE; 85 } 86 // load rtl styles 87 // note: this adds the rtl styles only to the 'screen' media type 88 // @deprecated 2012-04-09: rtl will cease to be a mode of its own, 89 // please use "[dir=rtl]" in any css file in all, screen or print mode instead 90 if ($mediatype=='screen') { 91 if($lang['direction'] == 'rtl'){ 92 if (isset($styleini['stylesheets']['rtl'])) $files[$mediatype] = array_merge($files[$mediatype], $styleini['stylesheets']['rtl']); 93 if (isset($config_cascade['userstyle']['rtl'])) $files[$mediatype][$config_cascade['userstyle']['rtl']] = DOKU_BASE; 94 } 95 } 96 97 $cache_files = array_merge($cache_files, array_keys($files[$mediatype])); 98 } 99 100 // check cache age & handle conditional request 101 // This may exit if a cache can be used 102 http_cached($cache->cache, 103 $cache->useCache(array('files' => $cache_files))); 104 105 // start output buffering 106 ob_start(); 107 108 // build the stylesheet 109 foreach ($mediatypes as $mediatype) { 110 111 // print the default classes for interwiki links and file downloads 112 if ($mediatype == 'screen') { 113 print '@media screen {'; 114 css_interwiki(); 115 css_filetypes(); 116 print '}'; 117 } 118 119 // load files 120 $css_content = ''; 121 foreach($files[$mediatype] as $file => $location){ 122 $display = str_replace(fullpath(DOKU_INC), '', fullpath($file)); 123 $css_content .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n"; 124 $css_content .= css_loadfile($file, $location); 125 } 126 switch ($mediatype) { 127 case 'screen': 128 print NL.'@media screen { /* START screen styles */'.NL.$css_content.NL.'} /* /@media END screen styles */'.NL; 129 break; 130 case 'print': 131 print NL.'@media print { /* START print styles */'.NL.$css_content.NL.'} /* /@media END print styles */'.NL; 132 break; 133 case 'all': 134 case 'feed': 135 default: 136 print NL.'/* START rest styles */ '.NL.$css_content.NL.'/* END rest styles */'.NL; 137 break; 138 } 139 } 140 // end output buffering and get contents 141 $css = ob_get_contents(); 142 ob_end_clean(); 143 144 // apply style replacements 145 $css = css_applystyle($css, $styleini['replacements']); 146 147 // parse less 148 $css = css_parseless($css); 149 150 // place all remaining @import statements at the top of the file 151 $css = css_moveimports($css); 152 153 // compress whitespace and comments 154 if($conf['compress']){ 155 $css = css_compress($css); 156 } 157 158 // embed small images right into the stylesheet 159 if($conf['cssdatauri']){ 160 $base = preg_quote(DOKU_BASE,'#'); 161 $css = preg_replace_callback('#(url\([ \'"]*)('.$base.')(.*?(?:\.(png|gif)))#i','css_datauri',$css); 162 } 163 164 http_cached_finish($cache->cache, $css); 165} 166 167/** 168 * Uses phpless to parse LESS in our CSS 169 * 170 * most of this function is error handling to show a nice useful error when 171 * LESS compilation fails 172 * 173 * @param $css 174 * @return string 175 */ 176function css_parseless($css) { 177 $less = new lessc(); 178 try { 179 return $less->compile($css); 180 } catch(Exception $e) { 181 // get exception message 182 $msg = str_replace(array("\n", "\r", "'"), array(), $e->getMessage()); 183 184 // try to use line number to find affected file 185 if(preg_match('/line: (\d+)$/', $msg, $m)){ 186 $msg = substr($msg, 0, -1* strlen($m[0])); //remove useless linenumber 187 $lno = $m[1]; 188 189 // walk upwards to last include 190 $lines = explode("\n", $css); 191 for($i=$lno-1; $i>=0; $i--){ 192 if(preg_match('/\/(\* XXXXXXXXX )(.*?)( XXXXXXXXX \*)\//', $lines[$i], $m)){ 193 // we found it, add info to message 194 $msg .= ' in '.$m[2].' at line '.($lno-$i); 195 break; 196 } 197 } 198 } 199 200 // something went wrong 201 $error = 'A fatal error occured during compilation of the CSS files. '. 202 'If you recently installed a new plugin or template it '. 203 'might be broken and you should try disabling it again. ['.$msg.']'; 204 205 echo ".dokuwiki:before { 206 content: '$error'; 207 background-color: red; 208 display: block; 209 background-color: #fcc; 210 border-color: #ebb; 211 color: #000; 212 padding: 0.5em; 213 }"; 214 215 exit; 216 } 217} 218 219/** 220 * Does placeholder replacements in the style according to 221 * the ones defined in a templates style.ini file 222 * 223 * This also adds the ini defined placeholders as less variables 224 * (sans the surrounding __ and with a ini_ prefix) 225 * 226 * @author Andreas Gohr <andi@splitbrain.org> 227 */ 228function css_applystyle($css, $replacements) { 229 // we convert ini replacements to LESS variable names 230 // and build a list of variable: value; pairs 231 $less = ''; 232 foreach((array) $replacements as $key => $value) { 233 $lkey = trim($key, '_'); 234 $lkey = '@ini_'.$lkey; 235 $less .= "$lkey: $value;\n"; 236 237 $replacements[$key] = $lkey; 238 } 239 240 // we now replace all old ini replacements with LESS variables 241 $css = strtr($css, $replacements); 242 243 // now prepend the list of LESS variables as the very first thing 244 $css = $less.$css; 245 return $css; 246} 247 248/** 249 * Load style ini contents 250 * 251 * Loads and merges style.ini files from template and config and prepares 252 * the stylesheet modes 253 * 254 * @author Andreas Gohr <andi@splitbrain.org> 255 * @param string $tpl the used template 256 * @return array with keys 'stylesheets' and 'replacements' 257 */ 258function css_styleini($tpl) { 259 $stylesheets = array(); // mode, file => base 260 $replacements = array(); // placeholder => value 261 262 // load template's style.ini 263 $incbase = tpl_incdir($tpl); 264 $webbase = tpl_basedir($tpl); 265 $ini = $incbase.'style.ini'; 266 if(file_exists($ini)){ 267 $data = parse_ini_file($ini, true); 268 269 // stylesheets 270 if(is_array($data['stylesheets'])) foreach($data['stylesheets'] as $file => $mode){ 271 $stylesheets[$mode][$incbase.$file] = $webbase; 272 } 273 274 // replacements 275 if(is_array($data['replacements'])){ 276 $replacements = array_merge($replacements, $data['replacements']); 277 } 278 } 279 280 // load template's style.local.ini 281 // @deprecated 2013-08-03 282 $ini = $incbase.'style.local.ini'; 283 if(file_exists($ini)){ 284 $data = parse_ini_file($ini, true); 285 286 // stylesheets 287 if(is_array($data['stylesheets'])) foreach($data['stylesheets'] as $file => $mode){ 288 $stylesheets[$mode][$incbase.$file] = $webbase; 289 } 290 291 // replacements 292 if(is_array($data['replacements'])){ 293 $replacements = array_merge($replacements, $data['replacements']); 294 } 295 } 296 297 // load configs's style.ini 298 $incbase = dirname($ini).'/'; 299 $webbase = DOKU_BASE; 300 $ini = DOKU_CONF."/tpl/$tpl/style.ini"; 301 if(file_exists($ini)){ 302 $data = parse_ini_file($ini, true); 303 304 // stylesheets 305 if(is_array($data['stylesheets'])) foreach($data['stylesheets'] as $file => $mode){ 306 $stylesheets[$mode][$incbase.$file] = $webbase; 307 } 308 309 // replacements 310 if(is_array($data['replacements'])){ 311 $replacements = array_merge($replacements, $data['replacements']); 312 } 313 } 314 315 return array( 316 'stylesheets' => $stylesheets, 317 'replacements' => $replacements 318 ); 319} 320 321/** 322 * Prints classes for interwikilinks 323 * 324 * Interwiki links have two classes: 'interwiki' and 'iw_$name>' where 325 * $name is the identifier given in the config. All Interwiki links get 326 * an default style with a default icon. If a special icon is available 327 * for an interwiki URL it is set in it's own class. Both classes can be 328 * overwritten in the template or userstyles. 329 * 330 * @author Andreas Gohr <andi@splitbrain.org> 331 */ 332function css_interwiki(){ 333 334 // default style 335 echo 'a.interwiki {'; 336 echo ' background: transparent url('.DOKU_BASE.'lib/images/interwiki.png) 0px 1px no-repeat;'; 337 echo ' padding: 1px 0px 1px 16px;'; 338 echo '}'; 339 340 // additional styles when icon available 341 $iwlinks = getInterwiki(); 342 foreach(array_keys($iwlinks) as $iw){ 343 $class = preg_replace('/[^_\-a-z0-9]+/i','_',$iw); 344 if(@file_exists(DOKU_INC.'lib/images/interwiki/'.$iw.'.png')){ 345 echo "a.iw_$class {"; 346 echo ' background-image: url('.DOKU_BASE.'lib/images/interwiki/'.$iw.'.png)'; 347 echo '}'; 348 }elseif(@file_exists(DOKU_INC.'lib/images/interwiki/'.$iw.'.gif')){ 349 echo "a.iw_$class {"; 350 echo ' background-image: url('.DOKU_BASE.'lib/images/interwiki/'.$iw.'.gif)'; 351 echo '}'; 352 } 353 } 354} 355 356/** 357 * Prints classes for file download links 358 * 359 * @author Andreas Gohr <andi@splitbrain.org> 360 */ 361function css_filetypes(){ 362 363 // default style 364 echo '.mediafile {'; 365 echo ' background: transparent url('.DOKU_BASE.'lib/images/fileicons/file.png) 0px 1px no-repeat;'; 366 echo ' padding-left: 18px;'; 367 echo ' padding-bottom: 1px;'; 368 echo '}'; 369 370 // additional styles when icon available 371 // scan directory for all icons 372 $exts = array(); 373 if($dh = opendir(DOKU_INC.'lib/images/fileicons')){ 374 while(false !== ($file = readdir($dh))){ 375 if(preg_match('/([_\-a-z0-9]+(?:\.[_\-a-z0-9]+)*?)\.(png|gif)/i',$file,$match)){ 376 $ext = strtolower($match[1]); 377 $type = '.'.strtolower($match[2]); 378 if($ext!='file' && (!isset($exts[$ext]) || $type=='.png')){ 379 $exts[$ext] = $type; 380 } 381 } 382 } 383 closedir($dh); 384 } 385 foreach($exts as $ext=>$type){ 386 $class = preg_replace('/[^_\-a-z0-9]+/','_',$ext); 387 echo ".mf_$class {"; 388 echo ' background-image: url('.DOKU_BASE.'lib/images/fileicons/'.$ext.$type.')'; 389 echo '}'; 390 } 391} 392 393/** 394 * Loads a given file and fixes relative URLs with the 395 * given location prefix 396 */ 397function css_loadfile($file,$location=''){ 398 if(!@file_exists($file)) return ''; 399 $css = io_readFile($file); 400 if(!$location) return $css; 401 402 $css = preg_replace('#(url\([ \'"]*)(?!/|data:|http://|https://| |\'|")#','\\1'.$location,$css); 403 $css = preg_replace('#(@import\s+[\'"])(?!/|data:|http://|https://)#', '\\1'.$location, $css); 404 405 return $css; 406} 407 408/** 409 * Converte local image URLs to data URLs if the filesize is small 410 * 411 * Callback for preg_replace_callback 412 */ 413function css_datauri($match){ 414 global $conf; 415 416 $pre = unslash($match[1]); 417 $base = unslash($match[2]); 418 $url = unslash($match[3]); 419 $ext = unslash($match[4]); 420 421 $local = DOKU_INC.$url; 422 $size = @filesize($local); 423 if($size && $size < $conf['cssdatauri']){ 424 $data = base64_encode(file_get_contents($local)); 425 } 426 if($data){ 427 $url = '\'data:image/'.$ext.';base64,'.$data.'\''; 428 }else{ 429 $url = $base.$url; 430 } 431 return $pre.$url; 432} 433 434 435/** 436 * Returns a list of possible Plugin Styles (no existance check here) 437 * 438 * @author Andreas Gohr <andi@splitbrain.org> 439 */ 440function css_pluginstyles($mediatype='screen'){ 441 global $lang; 442 $list = array(); 443 $plugins = plugin_list(); 444 foreach ($plugins as $p){ 445 $list[DOKU_PLUGIN."$p/$mediatype.css"] = DOKU_BASE."lib/plugins/$p/"; 446 $list[DOKU_PLUGIN."$p/$mediatype.less"] = DOKU_BASE."lib/plugins/$p/"; 447 // alternative for screen.css 448 if ($mediatype=='screen') { 449 $list[DOKU_PLUGIN."$p/style.css"] = DOKU_BASE."lib/plugins/$p/"; 450 $list[DOKU_PLUGIN."$p/style.less"] = DOKU_BASE."lib/plugins/$p/"; 451 } 452 // @deprecated 2012-04-09: rtl will cease to be a mode of its own, 453 // please use "[dir=rtl]" in any css file in all, screen or print mode instead 454 if($lang['direction'] == 'rtl'){ 455 $list[DOKU_PLUGIN."$p/rtl.css"] = DOKU_BASE."lib/plugins/$p/"; 456 } 457 } 458 return $list; 459} 460 461/** 462 * Move all @import statements in a combined stylesheet to the top so they 463 * aren't ignored by the browser. 464 * 465 * @author Gabriel Birke <birke@d-scribe.de> 466 */ 467function css_moveimports($css) 468{ 469 if(!preg_match_all('/@import\s+(?:url\([^)]+\)|"[^"]+")\s*[^;]*;\s*/', $css, $matches, PREG_OFFSET_CAPTURE)) { 470 return $css; 471 } 472 $newCss = ""; 473 $imports = ""; 474 $offset = 0; 475 foreach($matches[0] as $match) { 476 $newCss .= substr($css, $offset, $match[1] - $offset); 477 $imports .= $match[0]; 478 $offset = $match[1] + strlen($match[0]); 479 } 480 $newCss .= substr($css, $offset); 481 return $imports.$newCss; 482} 483 484/** 485 * Very simple CSS optimizer 486 * 487 * @author Andreas Gohr <andi@splitbrain.org> 488 */ 489function css_compress($css){ 490 //strip comments through a callback 491 $css = preg_replace_callback('#(/\*)(.*?)(\*/)#s','css_comment_cb',$css); 492 493 //strip (incorrect but common) one line comments 494 $css = preg_replace('/(?<!:)\/\/.*$/m','',$css); 495 496 // strip whitespaces 497 $css = preg_replace('![\r\n\t ]+!',' ',$css); 498 $css = preg_replace('/ ?([;,{}\/]) ?/','\\1',$css); 499 $css = preg_replace('/ ?: /',':',$css); 500 501 // shorten colors 502 $css = preg_replace("/#([0-9a-fA-F]{1})\\1([0-9a-fA-F]{1})\\2([0-9a-fA-F]{1})\\3/", "#\\1\\2\\3",$css); 503 504 return $css; 505} 506 507/** 508 * Callback for css_compress() 509 * 510 * Keeps short comments (< 5 chars) to maintain typical browser hacks 511 * 512 * @author Andreas Gohr <andi@splitbrain.org> 513 */ 514function css_comment_cb($matches){ 515 if(strlen($matches[2]) > 4) return ''; 516 return $matches[0]; 517} 518 519//Setup VIM: ex: et ts=4 : 520