mirror of
				https://github.com/mashirozx/sakura.git
				synced 2025-05-29 01:49:21 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			518 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			518 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * SCSSPHP
 | |
|  *
 | |
|  * @copyright 2012-2017 Leaf Corcoran
 | |
|  *
 | |
|  * @license http://opensource.org/licenses/MIT MIT
 | |
|  *
 | |
|  * @link http://leafo.github.io/scssphp
 | |
|  */
 | |
| 
 | |
| namespace Leafo\ScssPhp;
 | |
| 
 | |
| use Leafo\ScssPhp\Compiler;
 | |
| use Leafo\ScssPhp\Exception\ServerException;
 | |
| use Leafo\ScssPhp\Version;
 | |
| 
 | |
| /**
 | |
|  * Server
 | |
|  *
 | |
|  * @author Leaf Corcoran <leafot@gmail.com>
 | |
|  */
 | |
| class Server
 | |
| {
 | |
|     /**
 | |
|      * @var boolean
 | |
|      */
 | |
|     private $showErrorsAsCSS;
 | |
| 
 | |
|     /**
 | |
|      * @var string
 | |
|      */
 | |
|     private $dir;
 | |
| 
 | |
|     /**
 | |
|      * @var string
 | |
|      */
 | |
|     private $cacheDir;
 | |
| 
 | |
|     /**
 | |
|      * @var \Leafo\ScssPhp\Compiler
 | |
|      */
 | |
|     private $scss;
 | |
| 
 | |
|     /**
 | |
|      * Join path components
 | |
|      *
 | |
|      * @param string $left  Path component, left of the directory separator
 | |
|      * @param string $right Path component, right of the directory separator
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     protected function join($left, $right)
 | |
|     {
 | |
|         return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get name of requested .scss file
 | |
|      *
 | |
|      * @return string|null
 | |
|      */
 | |
|     protected function inputName()
 | |
|     {
 | |
|         switch (true) {
 | |
|             case isset($_GET['p']):
 | |
|                 return $_GET['p'];
 | |
|             case isset($_SERVER['PATH_INFO']):
 | |
|                 return $_SERVER['PATH_INFO'];
 | |
|             case isset($_SERVER['DOCUMENT_URI']):
 | |
|                 return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get path to requested .scss file
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     protected function findInput()
 | |
|     {
 | |
|         if (($input = $this->inputName())
 | |
|             && strpos($input, '..') === false
 | |
|             && substr($input, -5) === '.scss'
 | |
|         ) {
 | |
|             $name = $this->join($this->dir, $input);
 | |
| 
 | |
|             if (is_file($name) && is_readable($name)) {
 | |
|                 return $name;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get path to cached .css file
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     protected function cacheName($fname)
 | |
|     {
 | |
|         return $this->join($this->cacheDir, md5($fname) . '.css');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get path to meta data
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     protected function metadataName($out)
 | |
|     {
 | |
|         return $out . '.meta';
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Determine whether .scss file needs to be re-compiled.
 | |
|      *
 | |
|      * @param string $out  Output path
 | |
|      * @param string $etag ETag
 | |
|      *
 | |
|      * @return boolean True if compile required.
 | |
|      */
 | |
|     protected function needsCompile($out, &$etag)
 | |
|     {
 | |
|         if (! is_file($out)) {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         $mtime = filemtime($out);
 | |
| 
 | |
|         $metadataName = $this->metadataName($out);
 | |
| 
 | |
|         if (is_readable($metadataName)) {
 | |
|             $metadata = unserialize(file_get_contents($metadataName));
 | |
| 
 | |
|             foreach ($metadata['imports'] as $import => $originalMtime) {
 | |
|                 $currentMtime = filemtime($import);
 | |
| 
 | |
|                 if ($currentMtime !== $originalMtime || $currentMtime > $mtime) {
 | |
|                     return true;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             $metaVars = crc32(serialize($this->scss->getVariables()));
 | |
| 
 | |
|             if ($metaVars !== $metadata['vars']) {
 | |
|                 return true;
 | |
|             }
 | |
| 
 | |
|             $etag = $metadata['etag'];
 | |
| 
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get If-Modified-Since header from client request
 | |
|      *
 | |
|      * @return string|null
 | |
|      */
 | |
|     protected function getIfModifiedSinceHeader()
 | |
|     {
 | |
|         $modifiedSince = null;
 | |
| 
 | |
|         if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
 | |
|             $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
 | |
| 
 | |
|             if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
 | |
|                 $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return $modifiedSince;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get If-None-Match header from client request
 | |
|      *
 | |
|      * @return string|null
 | |
|      */
 | |
|     protected function getIfNoneMatchHeader()
 | |
|     {
 | |
|         $noneMatch = null;
 | |
| 
 | |
|         if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
 | |
|             $noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
 | |
|         }
 | |
| 
 | |
|         return $noneMatch;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Compile .scss file
 | |
|      *
 | |
|      * @param string $in  Input path (.scss)
 | |
|      * @param string $out Output path (.css)
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     protected function compile($in, $out)
 | |
|     {
 | |
|         $start   = microtime(true);
 | |
|         $css     = $this->scss->compile(file_get_contents($in), $in);
 | |
|         $elapsed = round((microtime(true) - $start), 4);
 | |
| 
 | |
|         $v    = Version::VERSION;
 | |
|         $t    = date('r');
 | |
|         $css  = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
 | |
|         $etag = md5($css);
 | |
| 
 | |
|         file_put_contents($out, $css);
 | |
|         file_put_contents(
 | |
|             $this->metadataName($out),
 | |
|             serialize([
 | |
|                 'etag'    => $etag,
 | |
|                 'imports' => $this->scss->getParsedFiles(),
 | |
|                 'vars'    => crc32(serialize($this->scss->getVariables())),
 | |
|             ])
 | |
|         );
 | |
| 
 | |
|         return [$css, $etag];
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Format error as a pseudo-element in CSS
 | |
|      *
 | |
|      * @param \Exception $error
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     protected function createErrorCSS(\Exception $error)
 | |
|     {
 | |
|         $message = str_replace(
 | |
|             ["'", "\n"],
 | |
|             ["\\'", "\\A"],
 | |
|             $error->getfile() . ":\n\n" . $error->getMessage()
 | |
|         );
 | |
| 
 | |
|         return "body { display: none !important; }
 | |
|                 html:after {
 | |
|                     background: white;
 | |
|                     color: black;
 | |
|                     content: '$message';
 | |
|                     display: block !important;
 | |
|                     font-family: mono;
 | |
|                     padding: 1em;
 | |
|                     white-space: pre;
 | |
|                 }";
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Render errors as a pseudo-element within valid CSS, displaying the errors on any
 | |
|      * page that includes this CSS.
 | |
|      *
 | |
|      * @param boolean $show
 | |
|      */
 | |
|     public function showErrorsAsCSS($show = true)
 | |
|     {
 | |
|         $this->showErrorsAsCSS = $show;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Compile .scss file
 | |
|      *
 | |
|      * @param string $in  Input file (.scss)
 | |
|      * @param string $out Output file (.css) optional
 | |
|      *
 | |
|      * @return string|bool
 | |
|      *
 | |
|      * @throws \Leafo\ScssPhp\Exception\ServerException
 | |
|      */
 | |
|     public function compileFile($in, $out = null)
 | |
|     {
 | |
|         if (! is_readable($in)) {
 | |
|             throw new ServerException('load error: failed to find ' . $in);
 | |
|         }
 | |
| 
 | |
|         $pi = pathinfo($in);
 | |
| 
 | |
|         $this->scss->addImportPath($pi['dirname'] . '/');
 | |
| 
 | |
|         $compiled = $this->scss->compile(file_get_contents($in), $in);
 | |
| 
 | |
|         if ($out !== null) {
 | |
|             return file_put_contents($out, $compiled);
 | |
|         }
 | |
| 
 | |
|         return $compiled;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check if file need compiling
 | |
|      *
 | |
|      * @param string $in  Input file (.scss)
 | |
|      * @param string $out Output file (.css)
 | |
|      *
 | |
|      * @return bool
 | |
|      */
 | |
|     public function checkedCompile($in, $out)
 | |
|     {
 | |
|         if (! is_file($out) || filemtime($in) > filemtime($out)) {
 | |
|             $this->compileFile($in, $out);
 | |
| 
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Compile requested scss and serve css.  Outputs HTTP response.
 | |
|      *
 | |
|      * @param string $salt Prefix a string to the filename for creating the cache name hash
 | |
|      */
 | |
|     public function serve($salt = '')
 | |
|     {
 | |
|         $protocol = isset($_SERVER['SERVER_PROTOCOL'])
 | |
|             ? $_SERVER['SERVER_PROTOCOL']
 | |
|             : 'HTTP/1.0';
 | |
| 
 | |
|         if ($input = $this->findInput()) {
 | |
|             $output = $this->cacheName($salt . $input);
 | |
|             $etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
 | |
| 
 | |
|             if ($this->needsCompile($output, $etag)) {
 | |
|                 try {
 | |
|                     list($css, $etag) = $this->compile($input, $output);
 | |
| 
 | |
|                     $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
 | |
| 
 | |
|                     header('Last-Modified: ' . $lastModified);
 | |
|                     header('Content-type: text/css');
 | |
|                     header('ETag: "' . $etag . '"');
 | |
| 
 | |
|                     echo $css;
 | |
|                 } catch (\Exception $e) {
 | |
|                     if ($this->showErrorsAsCSS) {
 | |
|                         header('Content-type: text/css');
 | |
| 
 | |
|                         echo $this->createErrorCSS($e);
 | |
|                     } else {
 | |
|                         header($protocol . ' 500 Internal Server Error');
 | |
|                         header('Content-type: text/plain');
 | |
| 
 | |
|                         echo 'Parse error: ' . $e->getMessage() . "\n";
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             header('X-SCSS-Cache: true');
 | |
|             header('Content-type: text/css');
 | |
|             header('ETag: "' . $etag . '"');
 | |
| 
 | |
|             if ($etag === $noneMatch) {
 | |
|                 header($protocol . ' 304 Not Modified');
 | |
| 
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             $modifiedSince = $this->getIfModifiedSinceHeader();
 | |
|             $mtime = filemtime($output);
 | |
| 
 | |
|             if (strtotime($modifiedSince) === $mtime) {
 | |
|                 header($protocol . ' 304 Not Modified');
 | |
| 
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             $lastModified  = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
 | |
|             header('Last-Modified: ' . $lastModified);
 | |
| 
 | |
|             echo file_get_contents($output);
 | |
| 
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         header($protocol . ' 404 Not Found');
 | |
|         header('Content-type: text/plain');
 | |
| 
 | |
|         $v = Version::VERSION;
 | |
|         echo "/* INPUT NOT FOUND scss $v */\n";
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Based on explicit input/output files does a full change check on cache before compiling.
 | |
|      *
 | |
|      * @param string  $in
 | |
|      * @param string  $out
 | |
|      * @param boolean $force
 | |
|      *
 | |
|      * @return string Compiled CSS results
 | |
|      *
 | |
|      * @throws \Leafo\ScssPhp\Exception\ServerException
 | |
|      */
 | |
|     public function checkedCachedCompile($in, $out, $force = false)
 | |
|     {
 | |
|         if (! is_file($in) || ! is_readable($in)) {
 | |
|             throw new ServerException('Invalid or unreadable input file specified.');
 | |
|         }
 | |
| 
 | |
|         if (is_dir($out) || ! is_writable(file_exists($out) ? $out : dirname($out))) {
 | |
|             throw new ServerException('Invalid or unwritable output file specified.');
 | |
|         }
 | |
| 
 | |
|         if ($force || $this->needsCompile($out, $etag)) {
 | |
|             list($css, $etag) = $this->compile($in, $out);
 | |
|         } else {
 | |
|             $css = file_get_contents($out);
 | |
|         }
 | |
| 
 | |
|         return $css;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Execute scssphp on a .scss file or a scssphp cache structure
 | |
|      *
 | |
|      * The scssphp cache structure contains information about a specific
 | |
|      * scss file having been parsed. It can be used as a hint for future
 | |
|      * calls to determine whether or not a rebuild is required.
 | |
|      *
 | |
|      * The cache structure contains two important keys that may be used
 | |
|      * externally:
 | |
|      *
 | |
|      * compiled: The final compiled CSS
 | |
|      * updated: The time (in seconds) the CSS was last compiled
 | |
|      *
 | |
|      * The cache structure is a plain-ol' PHP associative array and can
 | |
|      * be serialized and unserialized without a hitch.
 | |
|      *
 | |
|      * @param mixed   $in    Input
 | |
|      * @param boolean $force Force rebuild?
 | |
|      *
 | |
|      * @return array scssphp cache structure
 | |
|      */
 | |
|     public function cachedCompile($in, $force = false)
 | |
|     {
 | |
|         // assume no root
 | |
|         $root = null;
 | |
| 
 | |
|         if (is_string($in)) {
 | |
|             $root = $in;
 | |
|         } elseif (is_array($in) and isset($in['root'])) {
 | |
|             if ($force or ! isset($in['files'])) {
 | |
|                 // If we are forcing a recompile or if for some reason the
 | |
|                 // structure does not contain any file information we should
 | |
|                 // specify the root to trigger a rebuild.
 | |
|                 $root = $in['root'];
 | |
|             } elseif (isset($in['files']) and is_array($in['files'])) {
 | |
|                 foreach ($in['files'] as $fname => $ftime) {
 | |
|                     if (! file_exists($fname) or filemtime($fname) > $ftime) {
 | |
|                         // One of the files we knew about previously has changed
 | |
|                         // so we should look at our incoming root again.
 | |
|                         $root = $in['root'];
 | |
|                         break;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         } else {
 | |
|             // TODO: Throw an exception? We got neither a string nor something
 | |
|             // that looks like a compatible lessphp cache structure.
 | |
|             return null;
 | |
|         }
 | |
| 
 | |
|         if ($root !== null) {
 | |
|             // If we have a root value which means we should rebuild.
 | |
|             $out = array();
 | |
|             $out['root'] = $root;
 | |
|             $out['compiled'] = $this->compileFile($root);
 | |
|             $out['files'] = $this->scss->getParsedFiles();
 | |
|             $out['updated'] = time();
 | |
|             return $out;
 | |
|         } else {
 | |
|             // No changes, pass back the structure
 | |
|             // we were given initially.
 | |
|             return $in;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Constructor
 | |
|      *
 | |
|      * @param string                       $dir      Root directory to .scss files
 | |
|      * @param string                       $cacheDir Cache directory
 | |
|      * @param \Leafo\ScssPhp\Compiler|null $scss     SCSS compiler instance
 | |
|      */
 | |
|     public function __construct($dir, $cacheDir = null, $scss = null)
 | |
|     {
 | |
|         $this->dir = $dir;
 | |
| 
 | |
|         if (! isset($cacheDir)) {
 | |
|             $cacheDir = $this->join($dir, 'scss_cache');
 | |
|         }
 | |
| 
 | |
|         $this->cacheDir = $cacheDir;
 | |
| 
 | |
|         if (! is_dir($this->cacheDir)) {
 | |
|             throw new ServerException('Cache directory doesn\'t exist: ' . $cacheDir);
 | |
|         }
 | |
| 
 | |
|         if (! isset($scss)) {
 | |
|             $scss = new Compiler();
 | |
|             $scss->setImportPaths($this->dir);
 | |
|         }
 | |
| 
 | |
|         $this->scss = $scss;
 | |
|         $this->showErrorsAsCSS = false;
 | |
| 
 | |
|         if (! ini_get('date.timezone')) {
 | |
|             throw new ServerException('Default date.timezone not set');
 | |
|         }
 | |
|     }
 | |
| }
 |