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

Last change on this file since 119818 was 119818, checked in by cedric@…, 3 months ago

Mise a jour de la lib ScssPHP (corrige un bug de compilation avec BootStrap? 4.4)

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