PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.3
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.3
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.2.3, at vendor/mrclay/minify/lib/Minify.php

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