* @license PHP License * @package WB * @subpackage service */ WBClass::load('WBService'); /** * Service: deliver CSS file(s) * * Features: * - Deliver multiple files as one file * - Use cache * - minimize CSS (strip comments, remove blanks) * * @version 1.0.0 * @package WB * @subpackage service */ class WBService_Css extends WBService { /** * Display * * @param int $status * @param mixed $data to be send * @param string $checksum * @return bool true * @todo strip comments */ public function run(&$status, &$data, &$checksum = null) { $this->res->addHeader('Content-Type', 'text/css; charset=utf-8;'); // add caching header $useCache = WBParam::get('wb/cache/use', 1); if ($useCache) { $this->res->addHeader('Expires', gmdate('D, j M Y H:i:s', time() + 3600) . ' GMT'); } $css = $this->conf->get('skin/css', 'main'); if (!empty($this->path)) { $css = implode('/', $this->path); } // deliver a single file if (is_string($css)) { $css = array($css); } $file = WBClass::create('WBFile'); /** @var $file WBFile */ // cache multiple css files $cacheFile = 'var/cache/css/' . md5(implode(',', $css)); // use separate cache file for datauri if (WBParam::get('wb/html/datauri/use', false)) { $cacheFile .= '-datauri'; } if ($useCache && $file->exists($cacheFile)) { $status = $this->deliverFile($file->realpath(), $data, $checksum); return true; } $file->touch($cacheFile); $cache = fopen($file->realpath(), 'w'); fputs($cache, "@charset \"UTF-8\";\n"); foreach ($css as $sheet) { $status = $this->resolveCssFile($sheet, $data, $checksum); fputs($cache, "/* css: $sheet */\n"); if ($status != 200) { fclose($cache); if ($file->exists()) { unlink($file->realpath()); } return true; } fputs($cache, $this->loadCSSFile($data) . "\n"); continue; // collect and strip file while (!feof($data)) { $line = trim(fgets($data, 8192)); if (empty($line)) { continue; } if (strncmp(strtolower($line), "@charset ", 9) == 0) { continue; } fputs($cache, $line . "\n"); } } fclose($cache); $status = $this->deliverFile($file->realpath(), $data, $checksum); return true; } /** * load CSS file from resource * * Collect all data from CSS file, strip and add to buffer * * @param resource $data * @return string */ private function loadCSSFile($data) { $minimize = false; if('minimize' == WBParam::get('wb/html/compress', 'minimize')) { $minimize = true; } $buffer = array(); // collect and strip file $inComment = false; while (!feof($data)) { $line = trim(fgets($data, 8192)); if (empty($line)) { continue; } if (strncmp(strtolower($line), "@charset ", 9) == 0) { continue; } // import external files if (strncmp(strtolower($line), "@import ", 8) == 0) { $buffer[] = '/* IMPORT start */'; $buffer[] = $this->resolveImport($line); $buffer[] = '/* IMPORT end */'; continue; } $line = $this->resolveUrl($line); if (!$minimize) { $buffer[] = $line; continue; } // find end of multi-line comment if ($inComment) { $pos = strpos($line, '*/'); if (false === $pos) { continue; } $line = trim(substr($line, $pos + 2)); $inComment = false; if (!empty($line)) { $buffer[] = $line; } continue; } // find comment start $pos = strpos($line, '/*'); if (false !== $pos) { $start = substr($line, 0, $pos); $line = substr($line, $pos + 2); $end = ''; $inComment = true; // some comments end in the same line $pos = strpos($line, '*/'); if (false !== $pos) { $end = trim(substr($line, $pos + 2)); $inComment = false; } $line = $start . ' ' . $end; } $line = trim($line); if (!empty($line)) { $buffer[] = $line; } } return implode("\n", $buffer); } /** * Resolve @import in CSS file * * Include import CSS files to reduce number of requests. This applies for * local files only. * * @param string $line css line * @return string */ private function resolveImport($line) { $regex = '|@import\s+url\(\s*([\'"]?)([^\^\'^")]*)([\'"]?)\s*\);|i'; if (!preg_match($regex, $line, $match)) { return '/* import failed - bad line format */'; } $css = $match[2]; if (0 == strncmp('http://', $css, 7) || 0 == strncmp('https://', $css, 8)) { return sprintf('@import url("%s");', $css); } $this->resolveCssFile($css, $data, $checksum); return "/* css: $css */\n" . $this->loadCSSFile($data); } /** * Resolve URL * * Resolve url() and insert inline date instead, but ignore remote content * Actually remote content has URLs with "http://" or "https://" in it. * * @param string $line * @return string */ private function resolveUrl($line) { if (!WBParam::get('wb/html/datauri/use', false)) { return $line; } // extract url parameters if (!preg_match('/^(.*)(\s*)url\(\s*(.+)\s*\)\s*(.*)$/', $line, $match)) { return $line; } // ignore remote files like if (strstr($match[3], 'http://') || strstr($match[3], 'https://')) { return $line; } $file = WBClass::create('WBFile'); if (!$file->exists(str_replace('[[DOCROOT]]', 'htdoc', $match[3]))) { return $line; } $mime = $file->getMimeType(); $data = base64_encode(file_get_contents($file->realpath())); $line = sprintf('%s%surl(data:%s/%s;base64,%s) %s',$match[1], $match[2], $mime[0], $mime[1], $data, $match[4]); return $line . "\n"; } /** * find single CSS file * * @param string $css * @param resource $data * @param string $checksum * @return int $status */ protected function resolveCssFile($css, &$data, &$checksum) { // remove optional ".css" if (strpos($css, '.')) { $tmp = explode('.', $css); if (end($tmp) == 'css') { array_pop($tmp); $css = implode('.', $tmp); } } $base = realpath(WBParam::get('wb/dir/base') . '/resource/css'); $file = realpath($base . '/' . $css . '.css'); // check for file if (empty($file)) { $data = <<deliverFile($file, $data, $checksum); } /** * deliver real file * * @param string $file * @param resource $data * @param string $checksum * @return status */ protected function deliverFile($file, &$data, &$checksum) { $data = fopen($file, 'r'); $checksum = md5_file($file); return 200; } } ?>