source: spip-zone/_plugins_/scssphp/trunk/scssphp/example/Server.php @ 110441

Last change on this file since 110441 was 110441, checked in by arnaud.berard@…, 23 months ago

+z

  • Property svn:eol-style set to native
  • Property svn:executable set to *
File size: 13.6 KB
Line 
1<?php
2/**
3 * SCSSPHP
4 *
5 * @copyright 2012-2017 Leaf Corcoran
6 *
7 * @license http://opensource.org/licenses/MIT MIT
8 *
9 * @link http://leafo.github.io/scssphp
10 */
11
12namespace Leafo\ScssPhp;
13
14use Leafo\ScssPhp\Compiler;
15use Leafo\ScssPhp\Exception\ServerException;
16use Leafo\ScssPhp\Version;
17
18/**
19 * Server
20 *
21 * @author Leaf Corcoran <leafot@gmail.com>
22 */
23class Server
24{
25    /**
26     * @var boolean
27     */
28    private $showErrorsAsCSS;
29
30    /**
31     * @var string
32     */
33    private $dir;
34
35    /**
36     * @var string
37     */
38    private $cacheDir;
39
40    /**
41     * @var \Leafo\ScssPhp\Compiler
42     */
43    private $scss;
44
45    /**
46     * Join path components
47     *
48     * @param string $left  Path component, left of the directory separator
49     * @param string $right Path component, right of the directory separator
50     *
51     * @return string
52     */
53    protected function join($left, $right)
54    {
55        return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
56    }
57
58    /**
59     * Get name of requested .scss file
60     *
61     * @return string|null
62     */
63    protected function inputName()
64    {
65        switch (true) {
66            case isset($_GET['p']):
67                return $_GET['p'];
68            case isset($_SERVER['PATH_INFO']):
69                return $_SERVER['PATH_INFO'];
70            case isset($_SERVER['DOCUMENT_URI']):
71                return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
72        }
73    }
74
75    /**
76     * Get path to requested .scss file
77     *
78     * @return string
79     */
80    protected function findInput()
81    {
82        if (($input = $this->inputName())
83            && strpos($input, '..') === false
84            && substr($input, -5) === '.scss'
85        ) {
86            $name = $this->join($this->dir, $input);
87
88            if (is_file($name) && is_readable($name)) {
89                return $name;
90            }
91        }
92
93        return false;
94    }
95
96    /**
97     * Get path to cached .css file
98     *
99     * @return string
100     */
101    protected function cacheName($fname)
102    {
103        return $this->join($this->cacheDir, md5($fname) . '.css');
104    }
105
106    /**
107     * Get path to meta data
108     *
109     * @return string
110     */
111    protected function metadataName($out)
112    {
113        return $out . '.meta';
114    }
115
116    /**
117     * Determine whether .scss file needs to be re-compiled.
118     *
119     * @param string $out  Output path
120     * @param string $etag ETag
121     *
122     * @return boolean True if compile required.
123     */
124    protected function needsCompile($out, &$etag)
125    {
126        if (! is_file($out)) {
127            return true;
128        }
129
130        $mtime = filemtime($out);
131
132        $metadataName = $this->metadataName($out);
133
134        if (is_readable($metadataName)) {
135            $metadata = unserialize(file_get_contents($metadataName));
136
137            foreach ($metadata['imports'] as $import => $originalMtime) {
138                $currentMtime = filemtime($import);
139
140                if ($currentMtime !== $originalMtime || $currentMtime > $mtime) {
141                    return true;
142                }
143            }
144
145            $metaVars = crc32(serialize($this->scss->getVariables()));
146
147            if ($metaVars !== $metadata['vars']) {
148                return true;
149            }
150
151            $etag = $metadata['etag'];
152
153            return false;
154        }
155
156        return true;
157    }
158
159    /**
160     * Get If-Modified-Since header from client request
161     *
162     * @return string|null
163     */
164    protected function getIfModifiedSinceHeader()
165    {
166        $modifiedSince = null;
167
168        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
169            $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
170
171            if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
172                $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
173            }
174        }
175
176        return $modifiedSince;
177    }
178
179    /**
180     * Get If-None-Match header from client request
181     *
182     * @return string|null
183     */
184    protected function getIfNoneMatchHeader()
185    {
186        $noneMatch = null;
187
188        if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
189            $noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
190        }
191
192        return $noneMatch;
193    }
194
195    /**
196     * Compile .scss file
197     *
198     * @param string $in  Input path (.scss)
199     * @param string $out Output path (.css)
200     *
201     * @return array
202     */
203    protected function compile($in, $out)
204    {
205        $start   = microtime(true);
206        $css     = $this->scss->compile(file_get_contents($in), $in);
207        $elapsed = round((microtime(true) - $start), 4);
208
209        $v    = Version::VERSION;
210        $t    = gmdate('r');
211        $css  = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
212        $etag = md5($css);
213
214        file_put_contents($out, $css);
215        file_put_contents(
216            $this->metadataName($out),
217            serialize([
218                'etag'    => $etag,
219                'imports' => $this->scss->getParsedFiles(),
220                'vars'    => crc32(serialize($this->scss->getVariables())),
221            ])
222        );
223
224        return [$css, $etag];
225    }
226
227    /**
228     * Format error as a pseudo-element in CSS
229     *
230     * @param \Exception $error
231     *
232     * @return string
233     */
234    protected function createErrorCSS(\Exception $error)
235    {
236        $message = str_replace(
237            ["'", "\n"],
238            ["\\'", "\\A"],
239            $error->getfile() . ":\n\n" . $error->getMessage()
240        );
241
242        return "body { display: none !important; }
243                html:after {
244                    background: white;
245                    color: black;
246                    content: '$message';
247                    display: block !important;
248                    font-family: mono;
249                    padding: 1em;
250                    white-space: pre;
251                }";
252    }
253
254    /**
255     * Render errors as a pseudo-element within valid CSS, displaying the errors on any
256     * page that includes this CSS.
257     *
258     * @param boolean $show
259     */
260    public function showErrorsAsCSS($show = true)
261    {
262        $this->showErrorsAsCSS = $show;
263    }
264
265    /**
266     * Compile .scss file
267     *
268     * @param string $in  Input file (.scss)
269     * @param string $out Output file (.css) optional
270     *
271     * @return string|bool
272     *
273     * @throws \Leafo\ScssPhp\Exception\ServerException
274     */
275    public function compileFile($in, $out = null)
276    {
277        if (! is_readable($in)) {
278            throw new ServerException('load error: failed to find ' . $in);
279        }
280
281        $pi = pathinfo($in);
282
283        $this->scss->addImportPath($pi['dirname'] . '/');
284
285        $compiled = $this->scss->compile(file_get_contents($in), $in);
286
287        if ($out !== null) {
288            return file_put_contents($out, $compiled);
289        }
290
291        return $compiled;
292    }
293
294    /**
295     * Check if file need compiling
296     *
297     * @param string $in  Input file (.scss)
298     * @param string $out Output file (.css)
299     *
300     * @return bool
301     */
302    public function checkedCompile($in, $out)
303    {
304        if (! is_file($out) || filemtime($in) > filemtime($out)) {
305            $this->compileFile($in, $out);
306
307            return true;
308        }
309
310        return false;
311    }
312
313    /**
314     * Compile requested scss and serve css.  Outputs HTTP response.
315     *
316     * @param string $salt Prefix a string to the filename for creating the cache name hash
317     */
318    public function serve($salt = '')
319    {
320        $protocol = isset($_SERVER['SERVER_PROTOCOL'])
321            ? $_SERVER['SERVER_PROTOCOL']
322            : 'HTTP/1.0';
323
324        if ($input = $this->findInput()) {
325            $output = $this->cacheName($salt . $input);
326            $etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
327
328            if ($this->needsCompile($output, $etag)) {
329                try {
330                    list($css, $etag) = $this->compile($input, $output);
331
332                    $lastModified = gmdate('r', filemtime($output));
333
334                    header('Last-Modified: ' . $lastModified);
335                    header('Content-type: text/css');
336                    header('ETag: "' . $etag . '"');
337
338                    echo $css;
339                } catch (\Exception $e) {
340                    if ($this->showErrorsAsCSS) {
341                        header('Content-type: text/css');
342
343                        echo $this->createErrorCSS($e);
344                    } else {
345                        header($protocol . ' 500 Internal Server Error');
346                        header('Content-type: text/plain');
347
348                        echo 'Parse error: ' . $e->getMessage() . "\n";
349                    }
350                }
351
352                return;
353            }
354
355            header('X-SCSS-Cache: true');
356            header('Content-type: text/css');
357            header('ETag: "' . $etag . '"');
358
359            if ($etag === $noneMatch) {
360                header($protocol . ' 304 Not Modified');
361
362                return;
363            }
364
365            $modifiedSince = $this->getIfModifiedSinceHeader();
366            $mtime = filemtime($output);
367
368            if (strtotime($modifiedSince) === $mtime) {
369                header($protocol . ' 304 Not Modified');
370
371                return;
372            }
373
374            $lastModified  = gmdate('r', $mtime);
375            header('Last-Modified: ' . $lastModified);
376
377            echo file_get_contents($output);
378
379            return;
380        }
381
382        header($protocol . ' 404 Not Found');
383        header('Content-type: text/plain');
384
385        $v = Version::VERSION;
386        echo "/* INPUT NOT FOUND scss $v */\n";
387    }
388
389    /**
390     * Based on explicit input/output files does a full change check on cache before compiling.
391     *
392     * @param string  $in
393     * @param string  $out
394     * @param boolean $force
395     *
396     * @return string Compiled CSS results
397     *
398     * @throws \Leafo\ScssPhp\Exception\ServerException
399     */
400    public function checkedCachedCompile($in, $out, $force = false)
401    {
402        if (! is_file($in) || ! is_readable($in)) {
403            throw new ServerException('Invalid or unreadable input file specified.');
404        }
405
406        if (is_dir($out) || ! is_writable(file_exists($out) ? $out : dirname($out))) {
407            throw new ServerException('Invalid or unwritable output file specified.');
408        }
409
410        if ($force || $this->needsCompile($out, $etag)) {
411            list($css, $etag) = $this->compile($in, $out);
412        } else {
413            $css = file_get_contents($out);
414        }
415
416        return $css;
417    }
418
419    /**
420     * Execute scssphp on a .scss file or a scssphp cache structure
421     *
422     * The scssphp cache structure contains information about a specific
423     * scss file having been parsed. It can be used as a hint for future
424     * calls to determine whether or not a rebuild is required.
425     *
426     * The cache structure contains two important keys that may be used
427     * externally:
428     *
429     * compiled: The final compiled CSS
430     * updated: The time (in seconds) the CSS was last compiled
431     *
432     * The cache structure is a plain-ol' PHP associative array and can
433     * be serialized and unserialized without a hitch.
434     *
435     * @param mixed   $in    Input
436     * @param boolean $force Force rebuild?
437     *
438     * @return array scssphp cache structure
439     */
440    public function cachedCompile($in, $force = false)
441    {
442        // assume no root
443        $root = null;
444
445        if (is_string($in)) {
446            $root = $in;
447        } elseif (is_array($in) and isset($in['root'])) {
448            if ($force or ! isset($in['files'])) {
449                // If we are forcing a recompile or if for some reason the
450                // structure does not contain any file information we should
451                // specify the root to trigger a rebuild.
452                $root = $in['root'];
453            } elseif (isset($in['files']) and is_array($in['files'])) {
454                foreach ($in['files'] as $fname => $ftime) {
455                    if (! file_exists($fname) or filemtime($fname) > $ftime) {
456                        // One of the files we knew about previously has changed
457                        // so we should look at our incoming root again.
458                        $root = $in['root'];
459                        break;
460                    }
461                }
462            }
463        } else {
464            // TODO: Throw an exception? We got neither a string nor something
465            // that looks like a compatible lessphp cache structure.
466            return null;
467        }
468
469        if ($root !== null) {
470            // If we have a root value which means we should rebuild.
471            $out = array();
472            $out['root'] = $root;
473            $out['compiled'] = $this->compileFile($root);
474            $out['files'] = $this->scss->getParsedFiles();
475            $out['updated'] = time();
476            return $out;
477        } else {
478            // No changes, pass back the structure
479            // we were given initially.
480            return $in;
481        }
482    }
483
484    /**
485     * Constructor
486     *
487     * @param string                       $dir      Root directory to .scss files
488     * @param string                       $cacheDir Cache directory
489     * @param \Leafo\ScssPhp\Compiler|null $scss     SCSS compiler instance
490     */
491    public function __construct($dir, $cacheDir = null, $scss = null)
492    {
493        $this->dir = $dir;
494
495        if (! isset($cacheDir)) {
496            $cacheDir = $this->join($dir, 'scss_cache');
497        }
498
499        $this->cacheDir = $cacheDir;
500
501        if (! is_dir($this->cacheDir)) {
502            throw new ServerException('Cache directory doesn\'t exist: ' . $cacheDir);
503        }
504
505        if (! isset($scss)) {
506            $scss = new Compiler();
507            $scss->setImportPaths($this->dir);
508        }
509
510        $this->scss = $scss;
511        $this->showErrorsAsCSS = false;
512
513        date_default_timezone_set('UTC');
514    }
515}
Note: See TracBrowser for help on using the repository browser.