Server.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <?php
  2. /**
  3. * SCSSPHP
  4. *
  5. * @copyright 2012-2015 Leaf Corcoran
  6. *
  7. * @license http://opensource.org/licenses/MIT MIT
  8. *
  9. * @link http://leafo.github.io/scssphp
  10. */
  11. namespace Leafo\ScssPhp;
  12. use Leafo\ScssPhp\Compiler;
  13. use Leafo\ScssPhp\Exception\ServerException;
  14. use Leafo\ScssPhp\Version;
  15. /**
  16. * Server
  17. *
  18. * @author Leaf Corcoran <leafot@gmail.com>
  19. */
  20. class Server
  21. {
  22. /**
  23. * @var boolean
  24. */
  25. private $showErrorsAsCSS;
  26. /**
  27. * @var string
  28. */
  29. private $dir;
  30. /**
  31. * @var string
  32. */
  33. private $cacheDir;
  34. /**
  35. * @var \Leafo\ScssPhp\Compiler
  36. */
  37. private $scss;
  38. /**
  39. * Join path components
  40. *
  41. * @param string $left Path component, left of the directory separator
  42. * @param string $right Path component, right of the directory separator
  43. *
  44. * @return string
  45. */
  46. protected function join($left, $right)
  47. {
  48. return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
  49. }
  50. /**
  51. * Get name of requested .scss file
  52. *
  53. * @return string|null
  54. */
  55. protected function inputName()
  56. {
  57. switch (true) {
  58. case isset($_GET['p']):
  59. return $_GET['p'];
  60. case isset($_SERVER['PATH_INFO']):
  61. return $_SERVER['PATH_INFO'];
  62. case isset($_SERVER['DOCUMENT_URI']):
  63. return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
  64. }
  65. }
  66. /**
  67. * Get path to requested .scss file
  68. *
  69. * @return string
  70. */
  71. protected function findInput()
  72. {
  73. if (($input = $this->inputName()) && strpos($input, '..') === false && substr($input, -5) === '.scss') {
  74. $name = $this->join($this->dir, $input);
  75. if (is_file($name) && is_readable($name)) {
  76. return $name;
  77. }
  78. }
  79. return false;
  80. }
  81. /**
  82. * Get path to cached .css file
  83. *
  84. * @return string
  85. */
  86. protected function cacheName($fname)
  87. {
  88. return $this->join($this->cacheDir, md5($fname) . '.css');
  89. }
  90. /**
  91. * Get path to meta data
  92. *
  93. * @return string
  94. */
  95. protected function metadataName($out)
  96. {
  97. return $out . '.meta';
  98. }
  99. /**
  100. * Determine whether .scss file needs to be re-compiled.
  101. *
  102. * @param string $out Output path
  103. * @param string $etag ETag
  104. *
  105. * @return boolean True if compile required.
  106. */
  107. protected function needsCompile($out, &$etag)
  108. {
  109. if (!is_file($out)) {
  110. return true;
  111. }
  112. $mtime = filemtime($out);
  113. $metadataName = $this->metadataName($out);
  114. if (is_readable($metadataName)) {
  115. $metadata = unserialize(file_get_contents($metadataName));
  116. foreach ($metadata['imports'] as $import => $originalMtime) {
  117. $currentMtime = filemtime($import);
  118. if ($currentMtime !== $originalMtime || $currentMtime > $mtime) {
  119. return true;
  120. }
  121. }
  122. $metaVars = crc32(serialize($this->scss->getVariables()));
  123. if ($metaVars !== $metadata['vars']) {
  124. return true;
  125. }
  126. $etag = $metadata['etag'];
  127. return false;
  128. }
  129. return true;
  130. }
  131. /**
  132. * Get If-Modified-Since header from client request
  133. *
  134. * @return string|null
  135. */
  136. protected function getIfModifiedSinceHeader()
  137. {
  138. $modifiedSince = null;
  139. if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  140. $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
  141. if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
  142. $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
  143. }
  144. }
  145. return $modifiedSince;
  146. }
  147. /**
  148. * Get If-None-Match header from client request
  149. *
  150. * @return string|null
  151. */
  152. protected function getIfNoneMatchHeader()
  153. {
  154. $noneMatch = null;
  155. if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
  156. $noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
  157. }
  158. return $noneMatch;
  159. }
  160. /**
  161. * Compile .scss file
  162. *
  163. * @param string $in Input path (.scss)
  164. * @param string $out Output path (.css)
  165. *
  166. * @return array
  167. */
  168. protected function compile($in, $out)
  169. {
  170. $start = microtime(true);
  171. $css = $this->scss->compile(file_get_contents($in), $in);
  172. $elapsed = round(microtime(true) - $start, 4);
  173. $v = Version::VERSION;
  174. $t = date('r');
  175. $css = "/* compiled by scssphp {$v} on {$t} ({$elapsed}s) */\n\n" . $css;
  176. $etag = md5($css);
  177. file_put_contents($out, $css);
  178. file_put_contents($this->metadataName($out), serialize(array('etag' => $etag, 'imports' => $this->scss->getParsedFiles(), 'vars' => crc32(serialize($this->scss->getVariables())))));
  179. return array($css, $etag);
  180. }
  181. /**
  182. * Format error as a pseudo-element in CSS
  183. *
  184. * @param \Exception $error
  185. *
  186. * @return string
  187. */
  188. protected function createErrorCSS(\Exception $error)
  189. {
  190. $message = str_replace(array('\'', '
  191. '), array('\\\'', '\\A'), $error->getfile() . ':
  192. ' . $error->getMessage());
  193. return "body { display: none !important; }\n html:after {\n background: white;\n color: black;\n content: '{$message}';\n display: block !important;\n font-family: mono;\n padding: 1em;\n white-space: pre;\n }";
  194. }
  195. /**
  196. * Render errors as a pseudo-element within valid CSS, displaying the errors on any
  197. * page that includes this CSS.
  198. *
  199. * @param boolean $show
  200. */
  201. public function showErrorsAsCSS($show = true)
  202. {
  203. $this->showErrorsAsCSS = $show;
  204. }
  205. /**
  206. * Compile .scss file
  207. *
  208. * @param string $in Input file (.scss)
  209. * @param string $out Output file (.css) optional
  210. *
  211. * @return string|bool
  212. *
  213. * @throws \Leafo\ScssPhp\Exception\ServerException
  214. */
  215. public function compileFile($in, $out = null)
  216. {
  217. if (!is_readable($in)) {
  218. throw new ServerException('load error: failed to find ' . $in);
  219. }
  220. $pi = pathinfo($in);
  221. $this->scss->addImportPath($pi['dirname'] . '/');
  222. $compiled = $this->scss->compile(file_get_contents($in), $in);
  223. if ($out !== null) {
  224. return file_put_contents($out, $compiled);
  225. }
  226. return $compiled;
  227. }
  228. /**
  229. * Check if file need compiling
  230. *
  231. * @param string $in Input file (.scss)
  232. * @param string $out Output file (.css)
  233. *
  234. * @return bool
  235. */
  236. public function checkedCompile($in, $out)
  237. {
  238. if (!is_file($out) || filemtime($in) > filemtime($out)) {
  239. $this->compileFile($in, $out);
  240. return true;
  241. }
  242. return false;
  243. }
  244. /**
  245. * Compile requested scss and serve css. Outputs HTTP response.
  246. *
  247. * @param string $salt Prefix a string to the filename for creating the cache name hash
  248. */
  249. public function serve($salt = '')
  250. {
  251. $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';
  252. if ($input = $this->findInput()) {
  253. $output = $this->cacheName($salt . $input);
  254. $etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
  255. if ($this->needsCompile($output, $etag)) {
  256. try {
  257. list($css, $etag) = $this->compile($input, $output);
  258. $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
  259. header('Last-Modified: ' . $lastModified);
  260. header('Content-type: text/css');
  261. header('ETag: "' . $etag . '"');
  262. echo $css;
  263. } catch (\Exception $e) {
  264. if ($this->showErrorsAsCSS) {
  265. header('Content-type: text/css');
  266. echo $this->createErrorCSS($e);
  267. } else {
  268. header($protocol . ' 500 Internal Server Error');
  269. header('Content-type: text/plain');
  270. echo 'Parse error: ' . $e->getMessage() . '
  271. ';
  272. }
  273. }
  274. return;
  275. }
  276. header('X-SCSS-Cache: true');
  277. header('Content-type: text/css');
  278. header('ETag: "' . $etag . '"');
  279. if ($etag === $noneMatch) {
  280. header($protocol . ' 304 Not Modified');
  281. return;
  282. }
  283. $modifiedSince = $this->getIfModifiedSinceHeader();
  284. $mtime = filemtime($output);
  285. if (strtotime($modifiedSince) === $mtime) {
  286. header($protocol . ' 304 Not Modified');
  287. return;
  288. }
  289. $lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
  290. header('Last-Modified: ' . $lastModified);
  291. echo file_get_contents($output);
  292. return;
  293. }
  294. header($protocol . ' 404 Not Found');
  295. header('Content-type: text/plain');
  296. $v = Version::VERSION;
  297. echo "/* INPUT NOT FOUND scss {$v} */\n";
  298. }
  299. /**
  300. * Based on explicit input/output files does a full change check on cache before compiling.
  301. *
  302. * @param string $in
  303. * @param string $out
  304. * @param boolean $force
  305. *
  306. * @return string Compiled CSS results
  307. *
  308. * @throws \Leafo\ScssPhp\Exception\ServerException
  309. */
  310. public function checkedCachedCompile($in, $out, $force = false)
  311. {
  312. if (!is_file($in) || !is_readable($in)) {
  313. throw new ServerException('Invalid or unreadable input file specified.');
  314. }
  315. if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) {
  316. throw new ServerException('Invalid or unwritable output file specified.');
  317. }
  318. if ($force || $this->needsCompile($out, $etag)) {
  319. list($css, $etag) = $this->compile($in, $out);
  320. } else {
  321. $css = file_get_contents($out);
  322. }
  323. return $css;
  324. }
  325. /**
  326. * Constructor
  327. *
  328. * @param string $dir Root directory to .scss files
  329. * @param string $cacheDir Cache directory
  330. * @param \Leafo\ScssPhp\Compiler|null $scss SCSS compiler instance
  331. */
  332. public function __construct($dir, $cacheDir = null, $scss = null)
  333. {
  334. $this->dir = $dir;
  335. if (!isset($cacheDir)) {
  336. $cacheDir = $this->join($dir, 'scss_cache');
  337. }
  338. $this->cacheDir = $cacheDir;
  339. if (!is_dir($this->cacheDir)) {
  340. mkdir($this->cacheDir, 493, true);
  341. }
  342. if (!isset($scss)) {
  343. $scss = new Compiler();
  344. $scss->setImportPaths($this->dir);
  345. }
  346. $this->scss = $scss;
  347. $this->showErrorsAsCSS = false;
  348. if (!ini_get('date.timezone')) {
  349. date_default_timezone_set('UTC');
  350. }
  351. }
  352. /**
  353. * Helper method to serve compiled scss
  354. *
  355. * @param string $path Root path
  356. */
  357. public static function serveFrom($path)
  358. {
  359. $server = new self($path);
  360. $server->serve();
  361. }
  362. }