PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.4.2
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.4.2
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / mrclay / minify / lib / Minify.php

Minify.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 3.4.2, at vendor/mrclay/minify/lib/Minify.php

762 lines 26.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Minify
4 * @package Minify
5 */
6
7 use Psr\Log\LoggerInterface;
8
9 /**
10 * Minify - Combines, minifies, and caches JavaScript and CSS files on demand.
11 *
12 * See README for usage instructions (for now).
13 *
14 * This library was inspired by {@link mailto:[email protected] jscsscomp by Maxim Martynyuk}
15 * and by the article {@link http://www.hunlock.com/blogs/Supercharged_Javascript "Supercharged JavaScript" by Patrick Hunlock}.
16 *
17 * @package Minify
18 * @author Ryan Grove <[email protected]>
19 * @author Stephen Clay <[email protected]>
20 * @copyright 2008 Ryan Grove, Stephen Clay. All rights reserved.
21 * @license http://opensource.org/licenses/bsd-license.php New BSD License
22 * @link https://github.com/mrclay/minify
23 */
24 class Minify
25 {
26
27 /**
28 * API version
29 *
30 * This is only bumped when API breaks are done and should follow the major version of the library
31 *
32 * @var int
33 */
34 const VERSION = 3;
35
36 const TYPE_CSS = 'text/css';
37 const TYPE_HTML = 'text/html';
38 // there is some debate over the ideal JS Content-Type, but this is the
39 // Apache default and what Yahoo! uses..
40 const TYPE_JS = 'application/x-javascript';
41 const URL_DEBUG = 'https://github.com/mrclay/minify/blob/master/docs/Debugging.wiki.md';
42
43 /**
44 * Any Minify_Cache_* object or null (i.e. no server cache is used)
45 *
46 * @var Minify_CacheInterface
47 */
48 private $cache;
49
50 /**
51 * Active controller for current request
52 *
53 * @var Minify_Controller_Base
54 */
55 protected $controller;
56
57 /**
58 * @var Minify_Env
59 */
60 protected $env;
61
62 /**
63 * @var Minify_SourceInterface[]
64 */
65 protected $sources;
66
67 /**
68 * @var string
69 */
70 protected $selectionId;
71
72 /**
73 * Options for current request
74 *
75 * @var array
76 */
77 protected $options;
78
79 /**
80 * @var LoggerInterface|null
81 */
82 protected $logger;
83
84 /**
85 * @param Minify_CacheInterface $cache
86 * @param LoggerInterface $logger
87 */
88 public function __construct(Minify_CacheInterface $cache, LoggerInterface $logger = null)
89 {
90 $this->cache = $cache;
91 $this->logger = $logger;
92 }
93
94 /**
95 * Get default Minify options.
96 *
97 * @return array options for Minify
98 */
99 public function getDefaultOptions()
100 {
101 return array(
102 'isPublic' => true,
103 'encodeOutput' => function_exists('gzdeflate'),
104 'encodeMethod' => null, // determine later
105 'encodeLevel' => 9,
106
107 'minifiers' => array(
108 Minify::TYPE_JS => array('JSMin\\JSMin', 'minify'),
109 Minify::TYPE_CSS => array('Minify_CSSmin', 'minify'),
110 Minify::TYPE_HTML => array('Minify_HTML', 'minify'),
111 ),
112 'minifierOptions' => array(), // no minifier options
113
114 'contentTypeCharset' => 'utf-8',
115 'maxAge' => 1800, // 30 minutes
116 'rewriteCssUris' => true,
117 'bubbleCssImports' => false,
118 'quiet' => false, // serve() will send headers and output
119 'debug' => false,
120 'concatOnly' => false,
121 'invalidate' => false,
122
123 // if you override these, the response codes MUST be directly after
124 // the first space.
125 'badRequestHeader' => 'HTTP/1.0 400 Bad Request',
126 'errorHeader' => 'HTTP/1.0 500 Internal Server Error',
127
128 // callback function to see/modify content of all sources
129 'postprocessor' => null,
130 // file to require to load preprocessor
131 'postprocessorRequire' => null,
132
133 /**
134 * If this string is not empty AND the serve() option 'bubbleCssImports' is
135 * NOT set, then serve() will check CSS files for @import declarations that
136 * appear too late in the combined stylesheet. If found, serve() will prepend
137 * the output with this warning.
138 */
139 'importWarning' => "/* See https://github.com/mrclay/minify/blob/master/docs/CommonProblems.wiki.md#imports-can-appear-in-invalid-locations-in-combined-css-files */\n"
140 );
141 }
142
143 /**
144 * Serve a request for a minified file.
145 *
146 * Here are the available options and defaults:
147 *
148 * 'isPublic' : send "public" instead of "private" in Cache-Control
149 * headers, allowing shared caches to cache the output. (default true)
150 *
151 * 'quiet' : set to true to have serve() return an array rather than sending
152 * any headers/output (default false)
153 *
154 * 'encodeOutput' : set to false to disable content encoding, and not send
155 * the Vary header (default true)
156 *
157 * 'encodeMethod' : generally you should let this be determined by
158 * HTTP_Encoder (leave null), but you can force a particular encoding
159 * to be returned, by setting this to 'gzip' or '' (no encoding)
160 *
161 * 'encodeLevel' : level of encoding compression (0 to 9, default 9)
162 *
163 * 'contentTypeCharset' : appended to the Content-Type header sent. Set to a falsey
164 * value to remove. (default 'utf-8')
165 *
166 * 'maxAge' : set this to the number of seconds the client should use its cache
167 * before revalidating with the server. This sets Cache-Control: max-age and the
168 * Expires header. Unlike the old 'setExpires' setting, this setting will NOT
169 * prevent conditional GETs. Note this has nothing to do with server-side caching.
170 *
171 * 'rewriteCssUris' : If true, serve() will automatically set the 'currentDir'
172 * minifier option to enable URI rewriting in CSS files (default true)
173 *
174 * 'bubbleCssImports' : If true, all @import declarations in combined CSS
175 * files will be move to the top. Note this may alter effective CSS values
176 * due to a change in order. (default false)
177 *
178 * 'debug' : set to true to minify all sources with the 'Lines' controller, which
179 * eases the debugging of combined files. This also prevents 304 responses.
180 * @see Minify_Lines::minify()
181 *
182 * 'concatOnly' : set to true to disable minification and simply concatenate the files.
183 * For JS, no minifier will be used. For CSS, only URI rewriting is still performed.
184 *
185 * 'minifiers' : to override Minify's default choice of minifier function for
186 * a particular content-type, specify your callback under the key of the
187 * content-type:
188 * <code>
189 * // call customCssMinifier($css) for all CSS minification
190 * $options['minifiers'][Minify::TYPE_CSS] = 'customCssMinifier';
191 *
192 * // don't minify Javascript at all
193 * $options['minifiers'][Minify::TYPE_JS] = 'Minify::nullMinifier';
194 * </code>
195 *
196 * 'minifierOptions' : to send options to the minifier function, specify your options
197 * under the key of the content-type. E.g. To send the CSS minifier an option:
198 * <code>
199 * // give CSS minifier array('optionName' => 'optionValue') as 2nd argument
200 * $options['minifierOptions'][Minify::TYPE_CSS]['optionName'] = 'optionValue';
201 * </code>
202 *
203 * 'contentType' : (optional) this is only needed if your file extension is not
204 * js/css/html. The given content-type will be sent regardless of source file
205 * extension, so this should not be used in a Groups config with other
206 * Javascript/CSS files.
207 *
208 * 'importWarning' : serve() will check CSS files for @import declarations that
209 * appear too late in the combined stylesheet. If found, serve() will prepend
210 * the output with this warning. To disable this, set this option to empty string.
211 *
212 * Any controller options are documented in that controller's createConfiguration() method.
213 *
214 * @param Minify_ControllerInterface $controller instance of subclass of Minify_Controller_Base
215 *
216 * @param array $options controller/serve options
217 *
218 * @return null|array if the 'quiet' option is set to true, an array
219 * with keys "success" (bool), "statusCode" (int), "content" (string), and
220 * "headers" (array).
221 *
222 * @throws Exception
223 */
224 public function serve(Minify_ControllerInterface $controller, $options = array())
225 {
226 $this->env = $controller->getEnv();
227
228 $options = array_merge($this->getDefaultOptions(), $options);
229
230 $config = $controller->createConfiguration($options);
231
232 $this->sources = $config->getSources();
233 $this->selectionId = $config->getSelectionId();
234 $this->options = $this->analyzeSources($config->getOptions());
235
236 if (!$this->options['quiet'] && !headers_sent()) {
237 ini_set('zlib.output_compression', '0');
238 }
239
240 // check request validity
241 if (!$this->sources) {
242 // invalid request!
243 if (! $this->options['quiet']) {
244 $this->errorExit($this->options['badRequestHeader'], self::URL_DEBUG);
245 } else {
246 list(, $statusCode) = explode(' ', $this->options['badRequestHeader']);
247
248 return array(
249 'success' => false,
250 'statusCode' => (int)$statusCode,
251 'content' => '',
252 'headers' => array(),
253 );
254 }
255 }
256
257 $this->controller = $controller;
258
259 if ($this->options['debug']) {
260 $this->setupDebug();
261 $this->options['maxAge'] = 0;
262 }
263
264 // determine encoding
265 if ($this->options['encodeOutput']) {
266 $sendVary = true;
267 if ($this->options['encodeMethod'] !== null) {
268 // controller specifically requested this
269 $contentEncoding = $this->options['encodeMethod'];
270 } else {
271 // sniff request header
272 // depending on what the client accepts, $contentEncoding may be
273 // 'x-gzip' while our internal encodeMethod is 'gzip'. Calling
274 // getAcceptedEncoding(false, false) leaves out compress and deflate as options.
275 $list = HTTP_Encoder::getAcceptedEncoding(false, false);
276 list($this->options['encodeMethod'], $contentEncoding) = $list;
277 $sendVary = ! HTTP_Encoder::isBuggyIe();
278 }
279 } else {
280 $this->options['encodeMethod'] = ''; // identity (no encoding)
281 }
282
283 // check client cache
284 $cgOptions = array(
285 'lastModifiedTime' => $this->options['lastModifiedTime'],
286 'isPublic' => $this->options['isPublic'],
287 'encoding' => $this->options['encodeMethod'],
288 'invalidate' => $this->options['invalidate'],
289 );
290
291 if ($this->options['maxAge'] > 0) {
292 $cgOptions['maxAge'] = $this->options['maxAge'];
293 } elseif ($this->options['debug']) {
294 $cgOptions['invalidate'] = true;
295 }
296
297 $cg = new HTTP_ConditionalGet($cgOptions);
298 if ($cg->cacheIsValid) {
299 // client's cache is valid
300 if (! $this->options['quiet']) {
301 $cg->sendHeaders();
302
303 return;
304 }
305
306 return array(
307 'success' => true,
308 'statusCode' => 304,
309 'content' => '',
310 'headers' => $cg->getHeaders(),
311 );
312 }
313
314 // client will need output
315 $headers = $cg->getHeaders();
316 unset($cg);
317
318 if ($this->options['contentType'] === self::TYPE_CSS && $this->options['rewriteCssUris']) {
319 $this->setupUriRewrites();
320 }
321
322 if ($this->options['concatOnly']) {
323 $this->options['minifiers'][self::TYPE_JS] = false;
324 foreach ($this->sources as $key => $source) {
325 if ($this->options['contentType'] === self::TYPE_JS) {
326 $source->setMinifier('Minify::nullMinifier');
327 } elseif ($this->options['contentType'] === self::TYPE_CSS) {
328 $source->setMinifier(array('Minify_CSSmin', 'minify'));
329 $sourceOpts = $source->getMinifierOptions();
330 $sourceOpts['compress'] = false;
331 $source->setMinifierOptions($sourceOpts);
332 }
333 }
334 }
335
336 // check server cache
337 if (! $this->options['debug']) {
338 // using cache
339 // the goal is to use only the cache methods to sniff the length and
340 // output the content, as they do not require ever loading the file into
341 // memory.
342 $cacheId = $this->_getCacheId();
343 $fullCacheId = ($this->options['encodeMethod']) ? $cacheId . '.gz' : $cacheId;
344
345 // check cache for valid entry
346 $cacheIsReady = $this->cache->isValid($fullCacheId, $this->options['lastModifiedTime']);
347 if ($cacheIsReady) {
348 $cacheContentLength = $this->cache->getSize($fullCacheId);
349 } else {
350 // generate & cache content
351 try {
352 $content = $this->combineMinify();
353 } catch (Exception $e) {
354 $this->logger && $this->logger->critical($e->getMessage());
355 if (! $this->options['quiet']) {
356 $this->errorExit($this->options['errorHeader'], self::URL_DEBUG);
357 }
358 throw $e;
359 }
360 $this->cache->store($cacheId, $content);
361 if (function_exists('gzencode') && $this->options['encodeMethod']) {
362 $this->cache->store($cacheId . '.gz', gzencode($content, $this->options['encodeLevel']));
363 }
364 }
365 } else {
366 // no cache
367 $cacheIsReady = false;
368 try {
369 $content = $this->combineMinify();
370 } catch (Exception $e) {
371 $this->logger && $this->logger->critical($e->getMessage());
372 if (! $this->options['quiet']) {
373 $this->errorExit($this->options['errorHeader'], self::URL_DEBUG);
374 }
375 throw $e;
376 }
377 }
378 if (! $cacheIsReady && $this->options['encodeMethod']) {
379 // still need to encode
380 $content = gzencode($content, $this->options['encodeLevel']);
381 }
382
383 // add headers
384 if ($cacheIsReady) {
385 $headers['Content-Length'] = $cacheContentLength;
386 } else {
387 if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) {
388 $headers['Content-Length'] = mb_strlen($content, '8bit');
389 } else {
390 $headers['Content-Length'] = strlen($content);
391 }
392 }
393
394 $headers['Content-Type'] = $this->options['contentType'];
395 if ($this->options['contentTypeCharset']) {
396 $headers['Content-Type'] .= '; charset=' . $this->options['contentTypeCharset'];
397 }
398
399 if ($this->options['encodeMethod'] !== '') {
400 $headers['Content-Encoding'] = $contentEncoding;
401 }
402 if ($this->options['encodeOutput'] && $sendVary) {
403 $headers['Vary'] = 'Accept-Encoding';
404 }
405
406 if (! $this->options['quiet']) {
407 // output headers & content
408 foreach ($headers as $name => $val) {
409 header($name . ': ' . $val);
410 }
411 if ($cacheIsReady) {
412 $this->cache->display($fullCacheId);
413 } else {
414 echo $content;
415 }
416 } else {
417 return array(
418 'success' => true,
419 'statusCode' => 200,
420 'content' => $cacheIsReady ? $this->cache->fetch($fullCacheId) : $content,
421 'headers' => $headers,
422 );
423 }
424 }
425
426 /**
427 * Return combined minified content for a set of sources
428 *
429 * No internal caching will be used and the content will not be HTTP encoded.
430 *
431 * @param array $sources array of filepaths and/or Minify_Source objects
432 *
433 * @param array $options (optional) array of options for serve.
434 *
435 * @return string
436 */
437 public function combine($sources, $options = array())
438 {
439 $tmpCache = $this->cache;
440 $this->cache = new Minify_Cache_Null();
441
442 $env = new Minify_Env();
443 $sourceFactory = new Minify_Source_Factory($env, array(
444 'checkAllowDirs' => false,
445 ), $this->cache);
446 $controller = new Minify_Controller_Files($env, $sourceFactory, $this->logger);
447
448 $options = array_merge($options, array(
449 'files' => (array)$sources,
450 'quiet' => true,
451 'encodeMethod' => '',
452 'lastModifiedTime' => 0,
453 ));
454 $out = $this->serve($controller, $options);
455
456 $this->cache = $tmpCache;
457
458 return $out['content'];
459 }
460
461 /**
462 * Show an error page
463 *
464 * @param string $header Full header. E.g. 'HTTP/1.0 500 Internal Server Error'
465 * @param string $url URL to direct the user to
466 * @param string $msgHtml HTML message for the client
467 *
468 * @return void
469 * @internal This is not part of the public API and is subject to change
470 * @access private
471 */
472 public function errorExit($header, $url = '', $msgHtml = '')
473 {
474 $url = htmlspecialchars($url, ENT_QUOTES);
475 list(, $h1) = explode(' ', $header, 2);
476 $h1 = htmlspecialchars($h1);
477 // FastCGI environments require 3rd arg to header() to be set
478 list(, $code) = explode(' ', $header, 3);
479 header($header, true, $code);
480 header('Content-Type: text/html; charset=utf-8');
481 echo "<h1>$h1</h1>";
482 if ($msgHtml) {
483 echo $msgHtml;
484 }
485 if ($url) {
486 echo "<p>Please see <a href='$url'>$url</a>.</p>";
487 }
488 exit;
489 }
490
491 /**
492 * Default minifier for .min or -min JS files.
493 *
494 * @param string $content
495 * @return string
496 */
497 public static function nullMinifier($content)
498 {
499 if (isset($content[0]) && $content[0] === "\xef") {
500 $content = substr($content, 3);
501 }
502 $content = str_replace("\r\n", "\n", $content);
503
504 return trim($content);
505 }
506
507 /**
508 * Setup CSS sources for URI rewriting
509 */
510 protected function setupUriRewrites()
511 {
512 foreach ($this->sources as $key => $source) {
513 $file = $this->env->normalizePath($source->getFilePath());
514 $minifyOptions = $source->getMinifierOptions();
515
516 if ($file
517 && !isset($minifyOptions['currentDir'])
518 && !isset($minifyOptions['prependRelativePath'])) {
519 $minifyOptions['currentDir'] = dirname($file);
520 $source->setMinifierOptions($minifyOptions);
521 }
522 }
523 }
524
525 /**
526 * Set up sources to use Minify_Lines
527 */
528 protected function setupDebug()
529 {
530 foreach ($this->sources as $source) {
531 $source->setMinifier(array('Minify_Lines', 'minify'));
532 $id = $source->getId();
533 $source->setMinifierOptions(array(
534 'id' => (is_file($id) ? basename($id) : $id),
535 ));
536 }
537 }
538
539 /**
540 * Combines sources and minifies the result.
541 *
542 * @return string
543 *
544 * @throws Exception
545 */
546 protected function combineMinify()
547 {
548 $type = $this->options['contentType']; // ease readability
549
550 // when combining scripts, make sure all statements separated and
551 // trailing single line comment is terminated
552 $implodeSeparator = ($type === self::TYPE_JS) ? "\n;" : '';
553
554 // allow the user to pass a particular array of options to each
555 // minifier (designated by type). source objects may still override
556 // these
557 if (isset($this->options['minifierOptions'][$type])) {
558 $defaultOptions = $this->options['minifierOptions'][$type];
559 } else {
560 $defaultOptions = array();
561 }
562
563 // if minifier not set, default is no minification. source objects
564 // may still override this
565 if (isset($this->options['minifiers'][$type])) {
566 $defaultMinifier = $this->options['minifiers'][$type];
567 } else {
568 $defaultMinifier = false;
569 }
570
571 // process groups of sources with identical minifiers/options
572 $content = array();
573 $i = 0;
574 $l = count($this->sources);
575 $groupToProcessTogether = array();
576 $lastMinifier = null;
577 $lastOptions = null;
578 do {
579 // get next source
580 $source = null;
581 if ($i < $l) {
582 $source = $this->sources[$i];
583 $sourceContent = $source->getContent();
584
585 // allow the source to override our minifier and options
586 $minifier = $source->getMinifier();
587 if (!$minifier) {
588 $minifier = $defaultMinifier;
589 }
590 $options = array_merge($defaultOptions, $source->getMinifierOptions());
591 }
592 // do we need to process our group right now?
593 if ($i > 0 // yes, we have at least the first group populated
594 && (
595 ! $source // yes, we ran out of sources
596 || $type === self::TYPE_CSS // yes, to process CSS individually (avoiding PCRE bugs/limits)
597 || $minifier !== $lastMinifier // yes, minifier changed
598 || $options !== $lastOptions // yes, options changed
599 )) {
600 // minify previous sources with last settings
601 $imploded = implode($implodeSeparator, $groupToProcessTogether);
602 $groupToProcessTogether = array();
603 if ($lastMinifier) {
604 try {
605 $content[] = call_user_func($lastMinifier, $imploded, $lastOptions);
606 } catch (Exception $e) {
607 throw new Exception("Exception in minifier: " . $e->getMessage());
608 }
609 } else {
610 $content[] = $imploded;
611 }
612 }
613 // add content to the group
614 if ($source) {
615 $groupToProcessTogether[] = $sourceContent;
616 $lastMinifier = $minifier;
617 $lastOptions = $options;
618 }
619 $i++;
620 } while ($source);
621
622 $content = implode($implodeSeparator, $content);
623
624 if ($type === self::TYPE_CSS && false !== strpos($content, '@import')) {
625 $content = $this->handleCssImports($content);
626 }
627
628 // do any post-processing (esp. for editing build URIs)
629 if ($this->options['postprocessorRequire']) {
630 require_once $this->options['postprocessorRequire'];
631 }
632 if ($this->options['postprocessor']) {
633 $content = call_user_func($this->options['postprocessor'], $content, $type);
634 }
635
636 return $content;
637 }
638
639 /**
640 * Make a unique cache id for for this request.
641 *
642 * Any settings that could affect output are taken into consideration
643 *
644 * @param string $prefix
645 *
646 * @return string
647 */
648 protected function _getCacheId($prefix = 'minify')
649 {
650 $name = preg_replace('/[^a-zA-Z0-9\\.=_,]/', '', $this->selectionId);
651 $name = preg_replace('/\\.+/', '.', $name);
652 $name = substr($name, 0, 100 - 34 - strlen($prefix));
653 $md5 = md5(serialize(array(
654 Minify_SourceSet::getDigest($this->sources),
655 $this->options['minifiers'],
656 $this->options['minifierOptions'],
657 $this->options['postprocessor'],
658 $this->options['bubbleCssImports'],
659 Minify::VERSION,
660 )));
661
662 return "{$prefix}_{$name}_{$md5}";
663 }
664
665 /**
666 * Bubble CSS @imports to the top or prepend a warning if an import is detected not at the top.
667 *
668 * @param string $css
669 *
670 * @return string
671 */
672 protected function handleCssImports($css)
673 {
674 if ($this->options['bubbleCssImports']) {
675 // bubble CSS imports
676 preg_match_all('/@import.*?;/', $css, $imports);
677 $css = implode('', $imports[0]) . preg_replace('/@import.*?;/', '', $css);
678
679 return $css;
680 }
681
682 if ('' === $this->options['importWarning']) {
683 return $css;
684 }
685
686 // remove comments so we don't mistake { in a comment as a block
687 $noCommentCss = preg_replace('@/\\*[\\s\\S]*?\\*/@', '', $css);
688 $lastImportPos = strrpos($noCommentCss, '@import');
689 $firstBlockPos = strpos($noCommentCss, '{');
690 if (false !== $lastImportPos
691 && false !== $firstBlockPos
692 && $firstBlockPos < $lastImportPos
693 ) {
694 // { appears before @import : prepend warning
695 $css = $this->options['importWarning'] . $css;
696 }
697
698 return $css;
699 }
700
701 /**
702 * Analyze sources (if there are any) and set $options 'contentType'
703 * and 'lastModifiedTime' if they already aren't.
704 *
705 * @param array $options options for Minify
706 *
707 * @return array options for Minify
708 */
709 protected function analyzeSources($options = array())
710 {
711 if (!$this->sources) {
712 return $options;
713 }
714
715 $type = null;
716 foreach ($this->sources as $source) {
717 $sourceType = $source->getContentType();
718
719 if (!empty($options['contentType'])) {
720 // just verify sources have null content type or match the options
721 if ($sourceType !== null && $sourceType !== $options['contentType']) {
722 $this->logger && $this->logger->warning("ContentType mismatch: '{$sourceType}' != '{$options['contentType']}'");
723
724 $this->sources = array();
725
726 return $options;
727 }
728
729 continue;
730 }
731
732 if ($type === null) {
733 $type = $sourceType;
734 } elseif ($sourceType !== $type) {
735 $this->logger && $this->logger->warning("ContentType mismatch: '{$sourceType}' != '{$type}'");
736
737 $this->sources = array();
738
739 return $options;
740 }
741 }
742
743 if (empty($options['contentType'])) {
744 if (null === $type) {
745 $type = 'text/plain';
746 }
747 $options['contentType'] = $type;
748 }
749
750 // last modified is needed for caching, even if setExpires is set
751 if (!isset($options['lastModifiedTime'])) {
752 $max = 0;
753 foreach ($this->sources as $source) {
754 $max = max($source->getLastModified(), $max);
755 }
756 $options['lastModifiedTime'] = $max;
757 }
758
759 return $options;
760 }
761 }
762