PluginProbe
PhastPress / 1.77
PhastPress v1.77
3.12 3.11 1.75 1.76 1.77 1.78 1.79 1.80 1.81 1.82 1.83 1.84 1.85 1.86 1.87 1.88 1.89 1.90 1.91 1.92 1.93 1.94 1.95 1.96 1.97 All 119 releases
phastpress / sdk / phast.php

phast.php in PhastPress 1.77, at sdk/phast.php

10,023 lines 392.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Kibo\Phast\HTTP;
4
5 class Response
6 {
7 /**
8 * @var int
9 */
10 private $code = 200;
11 /**
12 * @var array
13 */
14 private $headers = array();
15 /**
16 * @var string|iterable
17 */
18 private $content;
19 /**
20 * @return int
21 */
22 public function getCode()
23 {
24 return $this->code;
25 }
26 /**
27 * @param int $code
28 */
29 public function setCode($code)
30 {
31 $this->code = $code;
32 }
33 /**
34 * @return array
35 */
36 public function getHeaders()
37 {
38 return $this->headers;
39 }
40 /**
41 * @param string $name
42 * @return string|null
43 */
44 public function getHeader($name)
45 {
46 foreach ($this->headers as $k => $v) {
47 if (strcasecmp($name, $k) === 0) {
48 return $v;
49 }
50 }
51 return null;
52 }
53 public function setHeaders(array $headers)
54 {
55 $this->headers = $headers;
56 }
57 /**
58 * @param $name
59 * @param $value
60 */
61 public function setHeader($name, $value)
62 {
63 $this->headers[$name] = $value;
64 }
65 /**
66 * @return string|iterable
67 */
68 public function getContent()
69 {
70 return $this->content;
71 }
72 /**
73 * @param string|iterable $content
74 */
75 public function setContent($content)
76 {
77 $this->content = $content;
78 }
79 public function isCompressible()
80 {
81 return strpos($this->getHeader('Content-Type'), 'image/') === false;
82 }
83 }
84 namespace Kibo\Phast\HTTP;
85
86 interface Client
87 {
88 /**
89 * Retrieve a URL using the GET HTTP method
90 *
91 * @param URL $url
92 * @param array $headers - headers to send in headerName => headerValue format
93 * @return Response
94 * @throws \Exception
95 */
96 public function get(\Kibo\Phast\ValueObjects\URL $url, array $headers = array());
97 /**
98 * Send data to a URL using the POST HTTP method
99 *
100 * @param URL $url
101 * @param array|string $data - if array, it will be encoded as form data, if string - will be sent as is
102 * @param array $headers - headers to send in headerName => headerValue format
103 * @return Response
104 * @throws \Exception
105 */
106 public function post(\Kibo\Phast\ValueObjects\URL $url, $data, array $headers = array());
107 }
108 namespace Kibo\Phast\HTTP;
109
110 class CURLClient implements \Kibo\Phast\HTTP\Client
111 {
112 public function get(\Kibo\Phast\ValueObjects\URL $url, array $headers = array())
113 {
114 $this->checkCURL();
115 return $this->request($url, $headers);
116 }
117 public function post(\Kibo\Phast\ValueObjects\URL $url, $data, array $headers = array())
118 {
119 $this->checkCURL();
120 return $this->request($url, $headers, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $data]);
121 }
122 private function checkCURL()
123 {
124 if (!function_exists('curl_init')) {
125 throw new \Kibo\Phast\HTTP\Exceptions\NetworkError('cURL is not installed');
126 }
127 }
128 private function request(\Kibo\Phast\ValueObjects\URL $url, array $headers = array(), array $opts = array())
129 {
130 $response = new \Kibo\Phast\HTTP\Response();
131 $readHeader = function ($_, $headerLine) use($response) {
132 if (strpos($headerLine, 'HTTP/') === 0) {
133 $response->setHeaders([]);
134 } else {
135 list($name, $value) = explode(':', $headerLine, 2);
136 if (trim($name) !== '') {
137 $response->setHeader($name, trim($value));
138 }
139 }
140 return strlen($headerLine);
141 };
142 $ch = curl_init((string) $url);
143 curl_setopt_array($ch, $opts + [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $this->makeHeaders($headers), CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_HEADERFUNCTION => $readHeader, CURLOPT_CAINFO => __DIR__ . '/cacert.pem', CURLOPT_ENCODING => '']);
144 $responseText = @curl_exec($ch);
145 if ($responseText === false) {
146 throw new \Kibo\Phast\HTTP\Exceptions\NetworkError(curl_error($ch), curl_errno($ch));
147 }
148 $info = curl_getinfo($ch);
149 if (!preg_match('/^2/', $info['http_code'])) {
150 throw new \Kibo\Phast\HTTP\Exceptions\HTTPError($info['http_code']);
151 }
152 $response->setCode($info['http_code']);
153 $response->setContent($responseText);
154 return $response;
155 }
156 private function makeHeaders(array $headers)
157 {
158 $result = [];
159 foreach ($headers as $k => $v) {
160 $result[] = "{$k}: {$v}";
161 }
162 return $result;
163 }
164 }
165 namespace Kibo\Phast\HTTP;
166
167 class Request
168 {
169 /**
170 * @var array
171 */
172 private $env;
173 /**
174 * @var array
175 */
176 private $cookie;
177 /**
178 * @var string
179 */
180 private $query;
181 private function __construct()
182 {
183 }
184 public static function fromGlobals()
185 {
186 $instance = new self();
187 $instance->env = $_SERVER;
188 $instance->cookie = $_COOKIE;
189 return $instance;
190 }
191 public static function fromArray(array $get = array(), array $env = array(), array $cookie = array())
192 {
193 if ($get) {
194 $url = isset($env['REQUEST_URI']) ? $env['REQUEST_URI'] : '';
195 $env['REQUEST_URI'] = \Kibo\Phast\ValueObjects\URL::fromString($url)->withQuery(http_build_query($get))->toString();
196 }
197 $instance = new self();
198 $instance->env = $env;
199 $instance->cookie = $cookie;
200 return $instance;
201 }
202 /**
203 * @return array
204 */
205 public function getGet()
206 {
207 return $this->getQuery()->toAssoc();
208 }
209 /**
210 * @return Query
211 */
212 public function getQuery()
213 {
214 return \Kibo\Phast\ValueObjects\Query::fromString($this->getQueryString());
215 }
216 /**
217 * @param $name string
218 * @return string|null
219 */
220 public function getHeader($name)
221 {
222 $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
223 return $this->getEnvValue($key);
224 }
225 public function getPathInfo()
226 {
227 $pathInfo = $this->getEnvValue('PATH_INFO');
228 if ($pathInfo) {
229 return $pathInfo;
230 }
231 $script = $this->getEnvValue('PHP_SELF');
232 $uri = $this->getEnvValue('DOCUMENT_URI');
233 if ($script !== null && $uri !== null && strpos($uri, $script . '/') === 0) {
234 return substr($uri, strlen($script));
235 }
236 }
237 public function getCookie($name)
238 {
239 if (isset($this->cookie[$name])) {
240 return $this->cookie[$name];
241 }
242 }
243 public function getQueryString()
244 {
245 $parsed = parse_url($this->getEnvValue('REQUEST_URI'));
246 if (isset($parsed['query'])) {
247 return $parsed['query'];
248 }
249 }
250 public function getAbsoluteURI()
251 {
252 return ($this->getEnvValue('HTTPS') ? 'https' : 'http') . '://' . $this->getHost() . $this->getURI();
253 }
254 public function getHost()
255 {
256 return $this->getHeader('Host');
257 }
258 public function getURI()
259 {
260 return $this->getEnvValue('REQUEST_URI');
261 }
262 private function getEnvValue($key)
263 {
264 if (isset($this->env[$key])) {
265 return $this->env[$key];
266 }
267 }
268 public function getDocumentRoot()
269 {
270 $scriptName = (string) $this->getEnvValue('SCRIPT_NAME');
271 $scriptFilename = $this->normalizePath((string) $this->getEnvValue('SCRIPT_FILENAME'));
272 if (strpos($scriptName, '/') === 0 && $this->isAbsolutePath($scriptFilename) && $this->isSuffix($scriptName, $scriptFilename)) {
273 return substr($scriptFilename, 0, strlen($scriptFilename) - strlen($scriptName));
274 }
275 return $this->getEnvValue('DOCUMENT_ROOT');
276 }
277 private function normalizePath($path)
278 {
279 return str_replace('\\', '/', $path);
280 }
281 private function isAbsolutePath($path)
282 {
283 return preg_match('~^/|^[a-z]:/~i', $path);
284 }
285 private function isSuffix($suffix, $string)
286 {
287 return substr($string, -strlen($suffix)) === $suffix;
288 }
289 public function isCloudflare()
290 {
291 return !!$this->getHeader('CF-Ray');
292 }
293 }
294 namespace Kibo\Phast\HTTP;
295
296 class ClientFactory
297 {
298 const CONFIG_KEY = 'httpClient';
299 /**
300 * @param array $config
301 * @return Client
302 */
303 public function make(array $config)
304 {
305 $spec = $config[self::CONFIG_KEY];
306 if (is_callable($spec)) {
307 $client = $spec();
308 } elseif (class_exists($spec)) {
309 $client = new $spec();
310 } else {
311 throw new \Kibo\Phast\Exceptions\RuntimeException(self::CONFIG_KEY . ' config value must be either callable or a class name');
312 }
313 return $client;
314 }
315 }
316 namespace Kibo\Phast\HTTP\Exceptions;
317
318 class HTTPError extends \RuntimeException
319 {
320 }
321 namespace Kibo\Phast\HTTP\Exceptions;
322
323 class NetworkError extends \RuntimeException
324 {
325 }
326 namespace Kibo\Phast\Environment;
327
328 class DefaultConfiguration
329 {
330 public static function get()
331 {
332 $request = \Kibo\Phast\HTTP\Request::fromGlobals();
333 return ['securityToken' => null, 'retrieverMap' => [$request->getHost() => $request->getDocumentRoot()], 'httpClient' => \Kibo\Phast\HTTP\CURLClient::class, 'cache' => ['cacheRoot' => sys_get_temp_dir() . '/phast-cache-' . (new \Kibo\Phast\Common\System())->getUserId(), 'shardingDepth' => 1, 'garbageCollection' => ['maxItems' => 100, 'probability' => 0.1, 'maxAge' => 86400 * 365], 'diskCleanup' => ['maxSize' => 500 * pow(1024, 2), 'probability' => 0.02, 'portionToFree' => 0.5]], 'servicesUrl' => '/phast.php', 'serviceRequestFormat' => \Kibo\Phast\Services\ServiceRequest::FORMAT_PATH, 'compressServiceResponse' => true, 'optimizeHTMLDocumentsOnly' => true, 'outputServerSideStats' => true, 'documents' => ['maxBufferSizeToApply' => pow(1024, 2), 'baseUrl' => $request->getAbsoluteURI(), 'filters' => [\Kibo\Phast\Filters\HTML\CommentsRemoval\Filter::class => [], \Kibo\Phast\Filters\HTML\Minify\Filter::class => [], \Kibo\Phast\Filters\HTML\MinifyScripts\Filter::class => [], \Kibo\Phast\Filters\HTML\BaseURLSetter\Filter::class => [], \Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags\Filter::class => [], \Kibo\Phast\Filters\HTML\LazyImageLoading\Filter::class => [], \Kibo\Phast\Filters\HTML\CSSInlining\Filter::class => ['optimizerSizeDiffThreshold' => 1024, 'whitelist' => ['~^https?://fonts\\.googleapis\\.com/~' => ['ieCompatible' => false], '~^https?://ajax\\.googleapis\\.com/ajax/libs/jqueryui/~', '~^https?://maxcdn\\.bootstrapcdn\\.com/[^?#]*\\.css~', '~^https?://idangero\\.us/~', '~^https?://[^/]*\\.github\\.io/~', '~^https?://\\w+\\.typekit\\.net/~' => ['ieCompatible' => false], '~^https?://stackpath\\.bootstrapcdn\\.com/~', '~^https?://cdnjs\\.cloudflare\\.com/~']], \Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter::class => [], \Kibo\Phast\Filters\HTML\DelayedIFrameLoading\Filter::class => [], \Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class => ['urlRefreshTime' => 7200], \Kibo\Phast\Filters\HTML\Diagnostics\Filter::class => ['enabled' => 'diagnostics'], \Kibo\Phast\Filters\HTML\ScriptsDeferring\Filter::class => [], \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\Filter::class => []]], 'images' => ['enable-cache' => 'imgcache', 'api-mode' => false, 'factory' => \Kibo\Phast\Filters\Image\ImageFactory::class, 'maxImageInliningSize' => 512, 'whitelist' => ['~^https?://ajax\\.googleapis\\.com/ajax/libs/jqueryui/~'], 'filters' => [\Kibo\Phast\Filters\Image\ImageAPIClient\Filter::class => ['api-url' => 'https://optimize.phast.io/?service=images', 'host-name' => $request->getHost(), 'request-uri' => $request->getURI(), 'plugin-version' => 'phast-core-1.0']]], 'styles' => ['filters' => [\Kibo\Phast\Filters\Text\Decode\Filter::class => [], \Kibo\Phast\Filters\CSS\ImportsStripper\Filter::class => [], \Kibo\Phast\Filters\CSS\CSSMinifier\Filter::class => [], \Kibo\Phast\Filters\CSS\CSSURLRewriter\Filter::class => [], \Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter::class => ['maxImageInliningSize' => 512], \Kibo\Phast\Filters\CSS\FontSwap\Filter::class => []]], 'logging' => ['logWriters' => [['class' => \Kibo\Phast\Logging\LogWriters\PHPError\Writer::class, 'levelMask' => \Kibo\Phast\Logging\LogLevel::EMERGENCY | \Kibo\Phast\Logging\LogLevel::ALERT | \Kibo\Phast\Logging\LogLevel::CRITICAL | \Kibo\Phast\Logging\LogLevel::ERROR | \Kibo\Phast\Logging\LogLevel::WARNING], ['enabled' => 'diagnostics', 'class' => \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer::class, 'logRoot' => sys_get_temp_dir() . '/phast-logs']]], 'switches' => ['phast' => true, 'diagnostics' => false], 'scripts' => ['removeLicenseHeaders' => false, 'whitelist' => ['~^https?://' . preg_quote($request->getHost(), '~') . '/~', '~^https?://(ssl|www)\\.google-analytics\\.com/(analytics\\.js|ga\\.js|gtm/js)($|\\?)~', '~^https?://www\\.googletagmanager\\.com/~', '~^https?://www\\.googleadservices\\.com/~', '~^https?://pixel\\.adcrowd\\.com/~', '~^https?://connect\\.facebook\\.net/~', '~^https?://static\\.hotjar\\.com/~', '~^https?://v2\\.zopim\\.com/~', '~^https?://stats\\.g\\.doubleclick\\.net/dc\\.js$~', '~^https?://s\\.pinimg\\.com/~']]];
334 }
335 }
336 namespace Kibo\Phast\Environment;
337
338 class Package
339 {
340 /**
341 * @var string
342 */
343 protected $type;
344 /**
345 * @var string
346 */
347 protected $namespace;
348 /**
349 * @param $className
350 * @param string|null $type
351 * @return Package
352 */
353 public static function fromPackageClass($className, $type = null)
354 {
355 $instance = new self();
356 $lastSeparatorPosition = strrpos($className, '\\');
357 $instance->type = empty($type) ? substr($className, $lastSeparatorPosition + 1) : $type;
358 $instance->namespace = substr($className, 0, $lastSeparatorPosition);
359 return $instance;
360 }
361 /**
362 * @return string
363 */
364 public function getType()
365 {
366 return $this->type;
367 }
368 /**
369 * @return string
370 */
371 public function getNamespace()
372 {
373 return $this->namespace;
374 }
375 /**
376 * @return bool
377 */
378 public function hasFactory()
379 {
380 return $this->classExists($this->getFactoryClassName());
381 }
382 /**
383 * @return mixed
384 */
385 public function getFactory()
386 {
387 if ($this->hasFactory()) {
388 $class = $this->getFactoryClassName();
389 return new $class();
390 }
391 throw new \Kibo\Phast\Environment\Exceptions\PackageHasNoFactoryException("Package {$this->namespace} has no factory");
392 }
393 /**
394 * @return bool
395 */
396 public function hasDiagnostics()
397 {
398 return $this->classExists($this->getDiagnosticsClassName());
399 }
400 /**
401 * @return Diagnostics
402 */
403 public function getDiagnostics()
404 {
405 if ($this->hasDiagnostics()) {
406 $class = $this->getDiagnosticsClassName();
407 return new $class();
408 }
409 throw new \Kibo\Phast\Environment\Exceptions\PackageHasNoDiagnosticsException("Package {$this->namespace} has no diagnostics");
410 }
411 private function getFactoryClassName()
412 {
413 return $this->getClassName('Factory');
414 }
415 private function getDiagnosticsClassName()
416 {
417 return $this->getClassName('Diagnostics');
418 }
419 private function getClassName($class)
420 {
421 return $this->namespace . '\\' . $class;
422 }
423 private function classExists($class)
424 {
425 // Don't trigger any autoloaders if Phast has been compiled into a
426 // single file, and avoid triggering Magento code generation.
427 $useAutoloader = basename(__FILE__) == 'Package.php';
428 return class_exists($class, $useAutoloader);
429 }
430 }
431 namespace Kibo\Phast\Environment;
432
433 class Switches
434 {
435 const SWITCH_PHAST = 'phast';
436 const SWITCH_DIAGNOSTICS = 'diagnostics';
437 private static $defaults = array(self::SWITCH_PHAST => true, self::SWITCH_DIAGNOSTICS => false);
438 private $switches = array();
439 public static function fromArray(array $switches)
440 {
441 $instance = new self();
442 $instance->switches = array_merge($instance->switches, $switches);
443 return $instance;
444 }
445 public static function fromString($switches)
446 {
447 $instance = new self();
448 if (empty($switches)) {
449 return $instance;
450 }
451 foreach (explode(',', $switches) as $switch) {
452 if ($switch[0] == '-') {
453 $instance->switches[substr($switch, 1)] = false;
454 } else {
455 $instance->switches[$switch] = true;
456 }
457 }
458 return $instance;
459 }
460 public function merge(\Kibo\Phast\Environment\Switches $switches)
461 {
462 $instance = new self();
463 $instance->switches = array_merge($this->switches, $switches->switches);
464 return $instance;
465 }
466 public function isOn($switch)
467 {
468 if (isset($this->switches[$switch])) {
469 return (bool) $this->switches[$switch];
470 }
471 if (isset(self::$defaults[$switch])) {
472 return (bool) self::$defaults[$switch];
473 }
474 return true;
475 }
476 public function toArray()
477 {
478 return array_merge(self::$defaults, $this->switches);
479 }
480 }
481 namespace Kibo\Phast\Environment;
482
483 class Configuration
484 {
485 /**
486 * @var array
487 */
488 private $sourceConfig;
489 /**
490 * @var Switches
491 */
492 private $switches;
493 /**
494 * @return Configuration
495 */
496 public static function fromDefaults()
497 {
498 return new self(\Kibo\Phast\Environment\DefaultConfiguration::get());
499 }
500 /**
501 * Configuration constructor.
502 * @param array $sourceConfig
503 */
504 public function __construct(array $sourceConfig)
505 {
506 $this->sourceConfig = $sourceConfig;
507 if (!isset($this->sourceConfig['switches'])) {
508 $this->switches = new \Kibo\Phast\Environment\Switches();
509 } else {
510 $this->switches = \Kibo\Phast\Environment\Switches::fromArray($this->sourceConfig['switches']);
511 }
512 }
513 /**
514 * @param Configuration $config
515 * @return $this
516 */
517 public function withUserConfiguration(\Kibo\Phast\Environment\Configuration $config)
518 {
519 $result = $this->recursiveMerge($this->sourceConfig, $config->sourceConfig);
520 return new self($result);
521 }
522 public function withServiceRequest(\Kibo\Phast\Services\ServiceRequest $request)
523 {
524 $clone = clone $this;
525 $clone->switches = $this->switches->merge($request->getSwitches());
526 return $clone;
527 }
528 public function getRuntimeConfig()
529 {
530 $config = $this->sourceConfig;
531 $switchables = [&$config['documents']['filters'], &$config['images']['filters'], &$config['logging']['logWriters'], &$config['styles']['filters']];
532 foreach ($switchables as &$switchable) {
533 if (!is_array($switchable)) {
534 continue;
535 }
536 $switchable = array_filter($switchable, function ($item) {
537 if (!isset($item['enabled'])) {
538 return true;
539 }
540 if ($item['enabled'] === false) {
541 return false;
542 }
543 return $this->switches->isOn($item['enabled']);
544 });
545 }
546 if (isset($config['images']['enable-cache']) && is_string($config['images']['enable-cache'])) {
547 $config['images']['enable-cache'] = $this->switches->isOn($config['images']['enable-cache']);
548 }
549 $config['switches'] = $this->switches->toArray();
550 return new \Kibo\Phast\Environment\Configuration($config);
551 }
552 public function toArray()
553 {
554 return $this->sourceConfig;
555 }
556 private function recursiveMerge(array $a1, array $a2)
557 {
558 foreach ($a2 as $key => $value) {
559 if (isset($a1[$key]) && is_array($a1[$key]) && is_array($value)) {
560 $a1[$key] = $this->recursiveMerge($a1[$key], $value);
561 } elseif (is_string($key)) {
562 $a1[$key] = $value;
563 } else {
564 $a1[] = $value;
565 }
566 }
567 return $a1;
568 }
569 }
570 namespace Kibo\Phast\Cache;
571
572 interface Cache
573 {
574 /**
575 * @param string $key
576 * @param callable|null $cached
577 * @param int $expiresIn
578 * @return mixed
579 */
580 public function get($key, callable $cached = null, $expiresIn = 0);
581 /**
582 * @param string $key
583 * @param mixed $value
584 * @param int $expiresIn
585 * @return mixed
586 */
587 public function set($key, $value, $expiresIn = 0);
588 }
589 namespace Kibo\Phast\Cache\File;
590
591 abstract class ProbabilisticExecutor
592 {
593 /**
594 * @var string
595 */
596 protected $cacheRoot;
597 /**
598 * @var float
599 */
600 protected $probability = 0;
601 /**
602 * @var ObjectifiedFunctions
603 */
604 protected $functions;
605 protected abstract function execute();
606 protected function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
607 {
608 $this->cacheRoot = $config['cacheRoot'];
609 $this->functions = is_null($functions) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $functions;
610 }
611 public function __destruct()
612 {
613 if ($this->shouldExecute()) {
614 $this->execute();
615 }
616 }
617 private function shouldExecute()
618 {
619 if (!$this->functions->file_exists($this->cacheRoot)) {
620 return false;
621 }
622 if ($this->probability <= 0) {
623 return false;
624 }
625 if ($this->probability >= 1) {
626 return true;
627 }
628 return $this->functions->mt_rand(1, round(1 / $this->probability)) == 1;
629 }
630 protected function getCacheFiles($path)
631 {
632 /** @var \SplFileInfo $item */
633 foreach ($this->makeFileSystemIterator($path) as $item) {
634 if ($this->isShard($item)) {
635 foreach ($this->getCacheFiles($item->getRealPath()) as $item) {
636 (yield $item);
637 }
638 } elseif ($this->isCacheEntry($item)) {
639 (yield $item);
640 }
641 }
642 }
643 /**
644 * @return \Iterator
645 */
646 protected function makeFileSystemIterator($path)
647 {
648 try {
649 $items = iterator_to_array(new \FilesystemIterator($path));
650 shuffle($items);
651 return new \ArrayIterator($items);
652 } catch (\Exception $e) {
653 return new \ArrayIterator([]);
654 }
655 }
656 protected function isShard(\SplFileInfo $item)
657 {
658 return $item->isDir() && !$item->isLink() && preg_match('/^[a-f\\d]{2}$/', $item->getFilename());
659 }
660 protected function isCacheEntry(\SplFileInfo $item)
661 {
662 return $item->isFile() && preg_match('/^[a-f\\d]{32}-/', $item->getFilename());
663 }
664 }
665 namespace Kibo\Phast\Cache\File;
666
667 class GarbageCollector extends \Kibo\Phast\Cache\File\ProbabilisticExecutor
668 {
669 /**
670 * @var integer
671 */
672 private $shardingDepth;
673 /**
674 * @var integer
675 */
676 private $gcMaxAge;
677 /**
678 * @var integer
679 */
680 private $gcMaxItems;
681 public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
682 {
683 $this->shardingDepth = $config['shardingDepth'];
684 $this->gcMaxAge = $config['garbageCollection']['maxAge'];
685 $this->gcMaxItems = $config['garbageCollection']['maxItems'];
686 $this->probability = $config['garbageCollection']['probability'];
687 parent::__construct($config, $functions);
688 }
689 protected function execute()
690 {
691 $files = $this->getCacheFiles($this->cacheRoot);
692 $deleted = 0;
693 /** @var \SplFileInfo $file */
694 foreach ($this->filterOldFiles($files) as $file) {
695 @$this->functions->unlink($file->getRealPath());
696 $deleted++;
697 if ($deleted == $this->gcMaxItems) {
698 break;
699 }
700 }
701 }
702 /**
703 * @param \Iterator $files
704 * @return \Generator
705 */
706 private function filterOldFiles(\Iterator $files)
707 {
708 $maxTimeModified = time() - $this->gcMaxAge;
709 /** @var \SplFileInfo $file */
710 foreach ($files as $file) {
711 if ($file->getMTime() < $maxTimeModified) {
712 (yield $file);
713 }
714 }
715 }
716 }
717 namespace Kibo\Phast\Cache\File;
718
719 class Cache implements \Kibo\Phast\Cache\Cache
720 {
721 use \Kibo\Phast\Logging\LoggingTrait;
722 const VERSION = '3';
723 /**
724 * @var GarbageCollector
725 */
726 private static $garbageCollector;
727 /**
728 * @var DiskCleanup
729 */
730 private static $diskCleanup;
731 /**
732 * @var string
733 */
734 private $cacheRoot;
735 /**
736 * @var string
737 */
738 private $cacheNS;
739 /**
740 * @var integer
741 */
742 private $shardingDepth;
743 /**
744 * @var integer
745 */
746 private $gcMaxAge;
747 /**
748 * @var ObjectifiedFunctions
749 */
750 private $functions;
751 /**
752 * @var System
753 */
754 private $system;
755 public function __construct(array $config, $cacheNamespace, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
756 {
757 $this->cacheRoot = $config['cacheRoot'];
758 $this->shardingDepth = $config['shardingDepth'];
759 $this->gcMaxAge = $config['garbageCollection']['maxAge'];
760 $this->cacheNS = $cacheNamespace;
761 if ($functions) {
762 $this->functions = $functions;
763 } else {
764 $this->functions = new \Kibo\Phast\Common\ObjectifiedFunctions();
765 }
766 $this->system = new \Kibo\Phast\Common\System($this->functions);
767 if (!isset(self::$garbageCollector)) {
768 self::$garbageCollector = new \Kibo\Phast\Cache\File\GarbageCollector($config, $this->functions);
769 self::$diskCleanup = new \Kibo\Phast\Cache\File\DiskCleanup($config, $this->functions);
770 }
771 }
772 public function get($key, callable $cached = null, $expiresIn = 0)
773 {
774 $contents = $this->getFromCache($key);
775 if (!is_null($contents)) {
776 return $contents;
777 }
778 if (is_null($cached)) {
779 return null;
780 }
781 $contents = $cached();
782 $this->storeCache($key, $contents, $expiresIn);
783 return $contents;
784 }
785 public function set($key, $value, $expiresIn = 0)
786 {
787 $this->storeCache($key, $value, $expiresIn);
788 }
789 /**
790 * @return GarbageCollector
791 */
792 public function getGarbageCollector()
793 {
794 return self::$garbageCollector;
795 }
796 /**
797 * @return DiskCleanup
798 */
799 public function getDiskCleanup()
800 {
801 return self::$diskCleanup;
802 }
803 private function getCacheDir($key)
804 {
805 $hashedKey = $this->getHashedKey($key);
806 $parts = [$this->cacheRoot];
807 for ($i = 0; $i < $this->shardingDepth * 2; $i += 2) {
808 $parts[] = substr($hashedKey, $i, 2);
809 }
810 return join('/', $parts);
811 }
812 private function getCacheFilename($key)
813 {
814 return $this->getCacheDir($key) . '/' . $this->getHashedKey($key) . '-' . ltrim($this->cacheNS, '/');
815 }
816 private function getHashedKey($key)
817 {
818 return md5($key);
819 }
820 private function storeCache($key, $contents, $expiresIn)
821 {
822 $dir = $this->getCacheDir($key);
823 if (!file_exists($dir)) {
824 @mkdir($dir, 0700, true);
825 }
826 if (($uid = $this->system->getUserId()) && $uid !== $this->functions->fileowner($this->cacheRoot)) {
827 $this->logger()->critical('Phast: FileCache: Cache root {cacheRoot} owned by {fileOwner}, but process user is {userId}!', ['cacheRoot' => $this->cacheRoot, 'fileOwner' => fileowner($this->cacheRoot), 'userId' => $uid]);
828 return;
829 }
830 $file = $this->getCacheFilename($key);
831 $expirationTime = $expiresIn > 0 ? $this->functions->time() + $expiresIn : 0;
832 $serialized = serialize($contents);
833 $serialized = implode(' ', [$expirationTime, self::VERSION, md5($serialized), $serialized]);
834 $result = @$this->functions->file_put_contents($file, $serialized);
835 if ($result === false) {
836 @chmod($file, 0600);
837 @unlink($file);
838 $result = @$this->functions->file_put_contents($file, $serialized);
839 }
840 if ($result !== strlen($serialized)) {
841 $this->logger()->critical('Phast: FileCache: Error writing to file {filename}. {written} of {total} bytes written!', ['filename' => $file, 'written' => json_encode($result), 'total' => strlen($serialized)]);
842 }
843 }
844 private function getFromCache($key)
845 {
846 $file = $this->getCacheFilename($key);
847 $contents = @$this->functions->file_get_contents($file);
848 if ($contents === false) {
849 return null;
850 }
851 @(list($expirationTime, $version, $data) = explode(' ', $contents, 3));
852 if ($version === '2') {
853 $data = unserialize($data);
854 } elseif ($version === self::VERSION) {
855 @(list($hash, $data) = explode(' ', $data, 2));
856 if (md5($data) != $hash) {
857 $this->logger()->error('Phast: FileCache: Cache file was corrupted: {file}', ['file' => $file]);
858 return null;
859 }
860 $data = unserialize($data);
861 } else {
862 $this->logger()->debug('Phast: FileCache: Refusing to read old cache file {file}', ['file' => $file]);
863 return null;
864 }
865 if ($expirationTime > $this->functions->time() || $expirationTime == 0) {
866 if ($this->functions->time() - @$this->functions->filectime($file) >= round($this->gcMaxAge / 10)) {
867 @$this->functions->touch($file);
868 }
869 return $data;
870 }
871 return null;
872 }
873 }
874 namespace Kibo\Phast\Cache\File;
875
876 class DiskCleanup extends \Kibo\Phast\Cache\File\ProbabilisticExecutor
877 {
878 /**
879 * @var integer
880 */
881 private $maxSize;
882 /**
883 * @var float
884 */
885 private $portionToFree;
886 public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
887 {
888 $this->maxSize = $config['diskCleanup']['maxSize'];
889 $this->probability = $config['diskCleanup']['probability'];
890 $this->portionToFree = $config['diskCleanup']['portionToFree'];
891 parent::__construct($config, $functions);
892 }
893 protected function execute()
894 {
895 $usedSpace = $this->calculateUsedSpace();
896 $neededSpace = round($this->portionToFree * $this->maxSize);
897 $bytesToDelete = $usedSpace - $this->maxSize + $neededSpace;
898 $deletedBytes = 0;
899 /** @var \SplFileInfo $file */
900 foreach ($this->getCacheFiles($this->cacheRoot) as $file) {
901 if ($deletedBytes >= $bytesToDelete) {
902 break;
903 }
904 $deletedBytes += $file->getSize();
905 @unlink($file->getRealPath());
906 }
907 }
908 private function calculateUsedSpace()
909 {
910 $size = 0;
911 /** @var \SplFileInfo $file */
912 foreach ($this->getCacheFiles($this->cacheRoot) as $file) {
913 $size += $file->getSize();
914 }
915 return $size;
916 }
917 }
918 namespace Kibo\Phast;
919
920 class PhastDocumentFilters
921 {
922 const DOCUMENT_PATTERN = "~\n \\s* (<\\?xml[^>]*>)?\n (\\s* <!--(.*?)-->)*\n \\s* (<!doctype\\s+html[^>]*>)?\n (\\s* <!--(.*?)-->)*\n \\s* <html (?<amp> [^>]* \\s ( amp | \342\232\241 ) [\\s=>] )?\n .*\n ( </body> | </html> )\n ~xsiA";
923 /**
924 * @return ?OutputBufferHandler
925 */
926 public static function deploy(array $userConfig = array())
927 {
928 $runtimeConfig = self::configure($userConfig);
929 if (!$runtimeConfig) {
930 return null;
931 }
932 $handler = new \Kibo\Phast\Common\OutputBufferHandler($runtimeConfig['documents']['maxBufferSizeToApply'], function ($html, $applyCheckBuffer) use($runtimeConfig) {
933 return self::applyWithRuntimeConfig($html, $runtimeConfig, $applyCheckBuffer);
934 });
935 $handler->install();
936 \Kibo\Phast\Logging\Log::info('Phast deployed!');
937 return $handler;
938 }
939 public static function apply($html, array $userConfig)
940 {
941 $runtimeConfig = self::configure($userConfig);
942 if (!$runtimeConfig) {
943 return $html;
944 }
945 return self::applyWithRuntimeConfig($html, $runtimeConfig);
946 }
947 private static function configure(array $userConfig)
948 {
949 $request = \Kibo\Phast\Services\ServiceRequest::fromHTTPRequest(\Kibo\Phast\HTTP\Request::fromGlobals());
950 $runtimeConfig = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration(new \Kibo\Phast\Environment\Configuration($userConfig))->withServiceRequest($request)->getRuntimeConfig()->toArray();
951 \Kibo\Phast\Logging\Log::init($runtimeConfig['logging'], $request, 'dom-filters');
952 \Kibo\Phast\Services\ServiceRequest::setDefaultSerializationMode($runtimeConfig['serviceRequestFormat']);
953 if ($request->hasRequestSwitchesSet()) {
954 \Kibo\Phast\Logging\Log::info('Request has switches set! Sending "noindex" header!');
955 header('X-Robots-Tag: noindex');
956 }
957 if (!$runtimeConfig['switches']['phast']) {
958 \Kibo\Phast\Logging\Log::info('Phast is off. Skipping document filter deployment!');
959 return;
960 }
961 return $runtimeConfig;
962 }
963 private static function applyWithRuntimeConfig($buffer, $runtimeConfig, $applyCheckBuffer = null)
964 {
965 if (is_null($applyCheckBuffer)) {
966 $applyCheckBuffer = $buffer;
967 }
968 if (!self::shouldApply($applyCheckBuffer, $runtimeConfig)) {
969 \Kibo\Phast\Logging\Log::info("Buffer ({bufferSize} bytes) doesn't look like html! Not applying filters", ['bufferSize' => strlen($applyCheckBuffer)]);
970 return $buffer;
971 }
972 $compositeFilter = (new \Kibo\Phast\Filters\HTML\Composite\Factory())->make($runtimeConfig);
973 if (self::isAMP($applyCheckBuffer)) {
974 $compositeFilter->selectFilters(function ($filter) {
975 return $filter instanceof \Kibo\Phast\Filters\HTML\AMPCompatibleFilter;
976 });
977 }
978 return $compositeFilter->apply($buffer);
979 }
980 private static function shouldApply($buffer, $runtimeConfig)
981 {
982 if ($runtimeConfig['optimizeHTMLDocumentsOnly']) {
983 return preg_match(self::DOCUMENT_PATTERN, $buffer);
984 }
985 return strpos($buffer, '<') !== false;
986 }
987 private static function isAMP($buffer)
988 {
989 return preg_match(self::DOCUMENT_PATTERN, $buffer, $match) && !empty($match['amp']);
990 }
991 }
992 namespace Kibo\Phast\Diagnostics;
993
994 interface Diagnostics
995 {
996 /**
997 * @param array $config
998 */
999 public function diagnose(array $config);
1000 }
1001 namespace Kibo\Phast\Diagnostics;
1002
1003 class SystemDiagnostics
1004 {
1005 /**
1006 * @param array $userConfigArr
1007 * @return Status[]
1008 */
1009 public function run(array $userConfigArr)
1010 {
1011 $results = [];
1012 $userConfig = new \Kibo\Phast\Environment\Configuration($userConfigArr);
1013 $config = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration($userConfig);
1014 foreach ($this->getExaminedItems($config) as $type => $group) {
1015 foreach ($group['items'] as $name) {
1016 $enabled = call_user_func($group['enabled'], $name);
1017 $package = \Kibo\Phast\Environment\Package::fromPackageClass($name, $type);
1018 try {
1019 $diagnostic = $package->getDiagnostics();
1020 $diagnostic->diagnose($config->toArray());
1021 $results[] = new \Kibo\Phast\Diagnostics\Status($package, true, '', $enabled);
1022 } catch (\Kibo\Phast\Environment\Exceptions\PackageHasNoDiagnosticsException $e) {
1023 $results[] = new \Kibo\Phast\Diagnostics\Status($package, true, '', $enabled);
1024 } catch (\Kibo\Phast\Exceptions\RuntimeException $e) {
1025 $results[] = new \Kibo\Phast\Diagnostics\Status($package, false, $e->getMessage(), $enabled);
1026 } catch (\Exception $e) {
1027 $results[] = new \Kibo\Phast\Diagnostics\Status($package, false, sprintf('Unknown error: Exception: %s, Message: %s, Code: %s', get_class($e), $e->getMessage(), $e->getCode()), $enabled);
1028 }
1029 }
1030 }
1031 return $results;
1032 }
1033 private function getExaminedItems(\Kibo\Phast\Environment\Configuration $config)
1034 {
1035 $runtimeConfig = $config->getRuntimeConfig()->toArray();
1036 $configArr = $config->toArray();
1037 return ['HTMLFilter' => ['items' => array_keys($configArr['documents']['filters']), 'enabled' => function ($filter) use($runtimeConfig) {
1038 return isset($runtimeConfig['documents']['filters'][$filter]);
1039 }], 'ImageFilter' => ['items' => array_keys($configArr['images']['filters']), 'enabled' => function ($filter) use($runtimeConfig) {
1040 return isset($runtimeConfig['images']['filters'][$filter]);
1041 }], 'Cache' => ['items' => [\Kibo\Phast\Cache\File\Cache::class], 'enabled' => function () {
1042 return true;
1043 }]];
1044 }
1045 }
1046 namespace Kibo\Phast\Diagnostics;
1047
1048 class Status implements \JsonSerializable
1049 {
1050 /**
1051 * @var Package
1052 */
1053 private $package;
1054 /**
1055 * @var bool
1056 */
1057 private $available;
1058 /**
1059 * @var string
1060 */
1061 private $reason;
1062 /**
1063 * @var bool
1064 */
1065 private $enabled;
1066 /**
1067 * Status constructor.
1068 * @param Package $package
1069 * @param bool $available
1070 * @param string $reason
1071 * @param bool $enabled
1072 */
1073 public function __construct(\Kibo\Phast\Environment\Package $package, $available, $reason, $enabled)
1074 {
1075 $this->package = $package;
1076 $this->available = $available;
1077 $this->reason = $reason;
1078 $this->enabled = $enabled;
1079 }
1080 /**
1081 * @return Package
1082 */
1083 public function getPackage()
1084 {
1085 return $this->package;
1086 }
1087 /**
1088 * @return bool
1089 */
1090 public function isAvailable()
1091 {
1092 return $this->available;
1093 }
1094 /**
1095 * @return string
1096 */
1097 public function getReason()
1098 {
1099 return $this->reason;
1100 }
1101 /**
1102 * @return bool
1103 */
1104 public function isEnabled()
1105 {
1106 return $this->enabled;
1107 }
1108 /**
1109 * @return array
1110 */
1111 public function toArray()
1112 {
1113 return ['package' => ['type' => $this->package->getType(), 'name' => $this->package->getNamespace()], 'available' => $this->available, 'reason' => $this->reason, 'enabled' => $this->enabled];
1114 }
1115 public function jsonSerialize()
1116 {
1117 return $this->toArray();
1118 }
1119 }
1120 namespace Kibo\Phast\Retrievers;
1121
1122 interface Retriever
1123 {
1124 /**
1125 * @param URL $url
1126 * @return string|bool
1127 */
1128 public function retrieve(\Kibo\Phast\ValueObjects\URL $url);
1129 /**
1130 * @param URL $url
1131 * @return integer|bool
1132 */
1133 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url);
1134 }
1135 namespace Kibo\Phast\Retrievers;
1136
1137 trait DynamicCacheSaltTrait
1138 {
1139 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
1140 {
1141 return md5($url->toString()) . '-' . floor(time() / 7200);
1142 }
1143 }
1144 namespace Kibo\Phast\Retrievers;
1145
1146 class RemoteRetriever implements \Kibo\Phast\Retrievers\Retriever
1147 {
1148 use \Kibo\Phast\Retrievers\DynamicCacheSaltTrait;
1149 use \Kibo\Phast\Logging\LoggingTrait;
1150 private $client;
1151 public function __construct(\Kibo\Phast\HTTP\Client $client)
1152 {
1153 $this->client = $client;
1154 }
1155 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
1156 {
1157 try {
1158 $response = $this->client->get($url, ['User-Agent' => 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0']);
1159 } catch (\Exception $e) {
1160 $this->logger()->warning('Caught {cls} while fetching {url}: ({code}) {message}', ['cls' => get_class($e), 'url' => (string) $url, 'code' => $e->getCode(), 'message' => $e->getMessage()]);
1161 return false;
1162 }
1163 return $response->getContent();
1164 }
1165 }
1166 namespace Kibo\Phast\Retrievers;
1167
1168 class RemoteRetrieverFactory
1169 {
1170 public function make(array $config)
1171 {
1172 return new \Kibo\Phast\Retrievers\RemoteRetriever((new \Kibo\Phast\HTTP\ClientFactory())->make($config));
1173 }
1174 }
1175 namespace Kibo\Phast\Retrievers;
1176
1177 class UniversalRetriever implements \Kibo\Phast\Retrievers\Retriever
1178 {
1179 /**
1180 * @var Retriever[]
1181 */
1182 private $retrievers = array();
1183 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
1184 {
1185 return $this->iterateRetrievers(function (\Kibo\Phast\Retrievers\Retriever $retriever) use($url) {
1186 return $retriever->retrieve($url);
1187 });
1188 }
1189 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
1190 {
1191 return $this->iterateRetrievers(function (\Kibo\Phast\Retrievers\Retriever $retriever) use($url) {
1192 return $retriever->getCacheSalt($url);
1193 });
1194 }
1195 private function iterateRetrievers(callable $callback)
1196 {
1197 foreach ($this->retrievers as $retriever) {
1198 $result = $callback($retriever);
1199 if ($result !== false) {
1200 return $result;
1201 }
1202 }
1203 return false;
1204 }
1205 public function addRetriever(\Kibo\Phast\Retrievers\Retriever $retriever)
1206 {
1207 $this->retrievers[] = $retriever;
1208 }
1209 }
1210 namespace Kibo\Phast\Retrievers;
1211
1212 class LocalRetriever implements \Kibo\Phast\Retrievers\Retriever
1213 {
1214 /**
1215 * @var array
1216 */
1217 private $map;
1218 /**
1219 * @var ObjectifiedFunctions
1220 */
1221 private $funcs;
1222 /**
1223 * LocalRetriever constructor.
1224 *
1225 * @param array $map
1226 * @param ObjectifiedFunctions|null $functions
1227 */
1228 public function __construct(array $map, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
1229 {
1230 $this->map = $map;
1231 if ($functions) {
1232 $this->funcs = $functions;
1233 } else {
1234 $this->funcs = new \Kibo\Phast\Common\ObjectifiedFunctions();
1235 }
1236 }
1237 public static function getAllowedExtensions()
1238 {
1239 return ['css', 'js', 'bmp', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico', 'svg', 'txt'];
1240 }
1241 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
1242 {
1243 return $this->guard($url, function ($file) {
1244 return @$this->funcs->file_get_contents($file);
1245 });
1246 }
1247 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
1248 {
1249 return $this->guard($url, function ($file) {
1250 $size = @$this->funcs->filesize($file);
1251 $mtime = @$this->funcs->filectime($file);
1252 if ($size === false && $mtime === false) {
1253 return '';
1254 }
1255 return "{$mtime}-{$size}";
1256 });
1257 }
1258 public function getSize(\Kibo\Phast\ValueObjects\URL $url)
1259 {
1260 return $this->guard($url, function ($file) {
1261 return @$this->funcs->filesize($file);
1262 });
1263 }
1264 private function guard(\Kibo\Phast\ValueObjects\URL $url, callable $cb)
1265 {
1266 if (!in_array($this->getExtensionForURL($url), self::getAllowedExtensions())) {
1267 return false;
1268 }
1269 $file = $this->getFileForURL($url);
1270 if ($file === false) {
1271 return false;
1272 }
1273 return $cb($file);
1274 }
1275 private function getExtensionForURL(\Kibo\Phast\ValueObjects\URL $url)
1276 {
1277 $dotPosition = strrpos($url->getPath(), '.');
1278 if ($dotPosition === false) {
1279 return '';
1280 }
1281 return strtolower(substr($url->getPath(), $dotPosition + 1));
1282 }
1283 private function getFileForURL(\Kibo\Phast\ValueObjects\URL $url)
1284 {
1285 if (!isset($this->map[$url->getHost()])) {
1286 return false;
1287 }
1288 $submap = $this->map[$url->getHost()];
1289 if (!is_array($submap)) {
1290 return $this->appendNormalized($submap, $url->getPath());
1291 }
1292 $selectedPath = null;
1293 $selectedRoot = null;
1294 foreach ($submap as $prefix => $root) {
1295 $pattern = '~^(?=/)/*?(?:' . str_replace('~', '\\~', $prefix) . ')(?<path>/*(?<=/).*)~';
1296 if (preg_match($pattern, $url->getPath(), $match) && ($selectedPath === null || strlen($match['path']) < strlen($selectedPath))) {
1297 $selectedRoot = $root;
1298 $selectedPath = $match['path'];
1299 }
1300 }
1301 if ($selectedPath === null) {
1302 return false;
1303 }
1304 return $this->appendNormalized($selectedRoot, $selectedPath);
1305 }
1306 private function appendNormalized($target, $appended)
1307 {
1308 $appended = explode("\0", $appended)[0];
1309 $appended = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $appended);
1310 $absolutes = [];
1311 foreach (explode(DIRECTORY_SEPARATOR, $appended) as $part) {
1312 if ($part == '' || $part == '.') {
1313 } elseif ($part == '..') {
1314 if (array_pop($absolutes) === null) {
1315 return false;
1316 }
1317 } else {
1318 $absolutes[] = $part;
1319 }
1320 }
1321 return $target . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $absolutes);
1322 }
1323 }
1324 namespace Kibo\Phast\Retrievers;
1325
1326 class CachingRetriever implements \Kibo\Phast\Retrievers\Retriever
1327 {
1328 use \Kibo\Phast\Retrievers\DynamicCacheSaltTrait {
1329 getCacheSalt as getDynamicCacheSalt;
1330 }
1331 /**
1332 * @var Cache
1333 */
1334 private $cache;
1335 /**
1336 * @var Retriever
1337 */
1338 private $retriever;
1339 /**
1340 * CachingRetriever constructor.
1341 *
1342 * @param Retriever $retriever
1343 * @param Cache $cache
1344 * @param int $defaultCacheTime
1345 */
1346 public function __construct(\Kibo\Phast\Cache\Cache $cache, \Kibo\Phast\Retrievers\Retriever $retriever = null, $defaultCacheTime = 0)
1347 {
1348 $this->cache = $cache;
1349 $this->retriever = $retriever;
1350 }
1351 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
1352 {
1353 if ($this->retriever) {
1354 return $this->getCachedWithRetriever($url);
1355 }
1356 return $this->getFromCacheOnly($url);
1357 }
1358 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
1359 {
1360 if ($this->retriever) {
1361 return $this->retriever->getCacheSalt($url);
1362 }
1363 return $this->getDynamicCacheSalt($url);
1364 }
1365 private function getCachedWithRetriever(\Kibo\Phast\ValueObjects\URL $url)
1366 {
1367 return $this->cache->get($this->getCacheKey($url), function () use($url) {
1368 return $this->retriever->retrieve($url);
1369 });
1370 }
1371 private function getFromCacheOnly(\Kibo\Phast\ValueObjects\URL $url)
1372 {
1373 $cached = $this->cache->get($this->getCacheKey($url));
1374 if (!$cached) {
1375 return false;
1376 }
1377 return $cached;
1378 }
1379 private function getCacheKey(\Kibo\Phast\ValueObjects\URL $url)
1380 {
1381 return $url . '-' . $this->getCacheSalt($url);
1382 }
1383 }
1384 namespace Kibo\Phast\Retrievers;
1385
1386 class PostDataRetriever implements \Kibo\Phast\Retrievers\Retriever
1387 {
1388 /**
1389 * @var ObjectifiedFunctions
1390 */
1391 private $funcs;
1392 private $content;
1393 /**
1394 * PostDataRetriever constructor.
1395 * @param ObjectifiedFunctions $funcs
1396 */
1397 public function __construct(\Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
1398 {
1399 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
1400 }
1401 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
1402 {
1403 if (!isset($this->content)) {
1404 $this->content = $this->funcs->file_get_contents('php://input');
1405 }
1406 return $this->content;
1407 }
1408 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
1409 {
1410 return md5($this->retrieve($url));
1411 }
1412 }
1413 namespace Kibo\Phast\Filters\HTML\Composite;
1414
1415 class Factory
1416 {
1417 use \Kibo\Phast\Logging\LoggingTrait;
1418 public function make(array $config)
1419 {
1420 $composite = new \Kibo\Phast\Filters\HTML\Composite\Filter(\Kibo\Phast\ValueObjects\URL::fromString($config['documents']['baseUrl']), $config['outputServerSideStats']);
1421 foreach (array_keys($config['documents']['filters']) as $class) {
1422 $package = \Kibo\Phast\Environment\Package::fromPackageClass($class);
1423 if ($package->hasFactory()) {
1424 $filter = $package->getFactory()->make($config);
1425 } elseif (!class_exists($class)) {
1426 $this->logger(__METHOD__, __LINE__)->error("Skipping non-existent filter class: {$class}");
1427 continue;
1428 } else {
1429 $filter = new $class();
1430 }
1431 $composite->addHTMLFilter($filter);
1432 }
1433 return $composite;
1434 }
1435 }
1436 namespace Kibo\Phast\Filters\HTML\Composite;
1437
1438 class Filter
1439 {
1440 use \Kibo\Phast\Logging\LoggingTrait;
1441 /**
1442 * @var URL
1443 */
1444 private $baseUrl;
1445 private $outputStats;
1446 /**
1447 * @var HTMLStreamFilter[]
1448 */
1449 private $filters = array();
1450 private $timings = array();
1451 /**
1452 * Filter constructor.
1453 * @param URL $baseUrl
1454 * @param $outputStats
1455 */
1456 public function __construct(\Kibo\Phast\ValueObjects\URL $baseUrl, $outputStats)
1457 {
1458 $this->baseUrl = $baseUrl;
1459 $this->outputStats = $outputStats;
1460 }
1461 /**
1462 * @param string $buffer
1463 * @return string
1464 */
1465 public function apply($buffer)
1466 {
1467 $timeStart = microtime(true);
1468 try {
1469 return $this->tryToApply($buffer, $timeStart);
1470 } catch (\Exception $e) {
1471 $this->logger()->critical('Phast: CompositeHTMLFilter: {exception} Msg: {message}, Code: {code}, File: {file}, Line: {line}', ['exception' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
1472 return $buffer;
1473 }
1474 }
1475 public function addHTMLFilter(\Kibo\Phast\Filters\HTML\HTMLStreamFilter $filter)
1476 {
1477 $this->filters[] = $filter;
1478 }
1479 private function tryToApply($buffer, $timeStart)
1480 {
1481 $context = new \Kibo\Phast\Filters\HTML\HTMLPageContext($this->baseUrl);
1482 $elements = (new \Kibo\Phast\Parsing\HTML\PCRETokenizer())->tokenize($buffer);
1483 foreach ($this->filters as $filter) {
1484 $this->logger()->info('Starting {filter}', ['filter' => get_class($filter)]);
1485 $elements = $filter->transformElements($elements, $context);
1486 }
1487 $output = '';
1488 foreach ($elements as $element) {
1489 $output .= $element;
1490 }
1491 $timeDelta = microtime(true) - $timeStart;
1492 if ($this->outputStats) {
1493 $output .= sprintf("\n<!-- [Phast] Document optimized in %dms -->\n", $timeDelta * 1000);
1494 }
1495 return $output;
1496 }
1497 public function selectFilters($callback)
1498 {
1499 $this->filters = array_filter($this->filters, $callback);
1500 }
1501 }
1502 namespace Kibo\Phast\Filters\HTML;
1503
1504 interface AMPCompatibleFilter
1505 {
1506 }
1507 namespace Kibo\Phast\Filters\HTML;
1508
1509 interface HTMLStreamFilter
1510 {
1511 /**
1512 * @param \Traversable $elements
1513 * @param HTMLPageContext $context
1514 * @return \Traversable
1515 */
1516 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context);
1517 }
1518 namespace Kibo\Phast\Filters\HTML\MinifyScripts;
1519
1520 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
1521 {
1522 use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait;
1523 private $cache;
1524 public function __construct(\Kibo\Phast\Cache\File\Cache $cache)
1525 {
1526 $this->cache = $cache;
1527 }
1528 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
1529 {
1530 $inTags = ['pre' => 0, 'textarea' => 0];
1531 foreach ($elements as $element) {
1532 if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag && $element->getTagName() === 'script' && ($content = $element->getTextContent()) !== '') {
1533 $content = trim($content);
1534 if ($this->isJSElement($element) && preg_match('~[()[\\]{};]\\s~', $content)) {
1535 $content = preg_replace('~^\\s*<!--\\s*\\n(.*)\\n\\s*-->\\s*$~s', '$1', $content);
1536 $content = $this->cache->get(md5($content), function () use($content) {
1537 return (new \Kibo\Phast\Common\JSMinifier($content, true))->min();
1538 });
1539 } elseif (($data = @json_decode($content)) !== null && ($newContent = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) !== false) {
1540 $content = str_replace('</', '<\\/', $newContent);
1541 }
1542 $element->setTextContent($content);
1543 }
1544 (yield $element);
1545 }
1546 }
1547 }
1548 namespace Kibo\Phast\Filters\HTML\MinifyScripts;
1549
1550 class Factory
1551 {
1552 public function make(array $config)
1553 {
1554 return new \Kibo\Phast\Filters\HTML\MinifyScripts\Filter(new \Kibo\Phast\Cache\File\Cache($config['cache'], 'minified-inline-scripts'));
1555 }
1556 }
1557 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService;
1558
1559 class ImageURLRewriterFactory
1560 {
1561 public function make(array $config, $filterClass = '')
1562 {
1563 $signature = (new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config);
1564 if (isset($config['documents']['filters'][$filterClass])) {
1565 $classConfig = $config['documents']['filters'][$filterClass];
1566 } elseif (isset($config['styles']['filters'][$filterClass])) {
1567 $classConfig = $config['styles']['filters'][$filterClass];
1568 } else {
1569 $classConfig = [];
1570 }
1571 if (isset($classConfig['serviceUrl'])) {
1572 $serviceUrl = $classConfig['serviceUrl'];
1573 } else {
1574 $serviceUrl = $config['servicesUrl'] . '?service=images';
1575 }
1576 return new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter($signature, new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']), (new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManagerFactory())->make($config), \Kibo\Phast\ValueObjects\URL::fromString($config['documents']['baseUrl']), \Kibo\Phast\ValueObjects\URL::fromString($serviceUrl), $config['images']['whitelist']);
1577 }
1578 }
1579 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService;
1580
1581 class ImageInliningManager
1582 {
1583 use \Kibo\Phast\Logging\LoggingTrait;
1584 /**
1585 * @var Cache
1586 */
1587 private $cache;
1588 /**
1589 * @var int
1590 */
1591 private $maxImageInliningSize;
1592 /**
1593 * ImageInliningManager constructor.
1594 * @param Cache $cache
1595 * @param int $maxImageInliningSize
1596 */
1597 public function __construct(\Kibo\Phast\Cache\Cache $cache, $maxImageInliningSize)
1598 {
1599 $this->cache = $cache;
1600 $this->maxImageInliningSize = $maxImageInliningSize;
1601 }
1602 /**
1603 * @return int
1604 */
1605 public function getMaxImageInliningSize()
1606 {
1607 return $this->maxImageInliningSize;
1608 }
1609 /**
1610 * @param Resource $resource
1611 * @return string|null
1612 */
1613 public function getUrlForInlining(\Kibo\Phast\ValueObjects\Resource $resource)
1614 {
1615 if ($resource->getMimeType() !== 'image/svg+xml') {
1616 return $this->cache->get($this->getCacheKey($resource));
1617 }
1618 try {
1619 if ($this->hasSizeForInlining($resource)) {
1620 return $resource->toDataURL();
1621 }
1622 } catch (\Kibo\Phast\Exceptions\ItemNotFoundException $e) {
1623 $this->logger()->warning('Could not fetch contents for {url}. Message is {message}', ['url' => $resource->getUrl()->toString(), 'message' => $e->getMessage()]);
1624 }
1625 return null;
1626 }
1627 public function maybeStoreForInlining(\Kibo\Phast\ValueObjects\Resource $resource)
1628 {
1629 if ($this->shouldStoreForInlining($resource)) {
1630 $this->logger()->info('Storing {url} for inlining', ['url' => $resource->getUrl()->toString()]);
1631 $this->cache->set($this->getCacheKey($resource), $resource->toDataURL());
1632 } else {
1633 $this->logger()->info('Not storing {url} for inlining', ['url' => $resource->getUrl()->toString()]);
1634 }
1635 }
1636 private function shouldStoreForInlining(\Kibo\Phast\ValueObjects\Resource $resource)
1637 {
1638 return $this->hasSizeForInlining($resource) && strpos($resource->getMimeType(), 'image/') === 0 && $resource->getMimeType() !== 'image/webp';
1639 }
1640 private function hasSizeForInlining(\Kibo\Phast\ValueObjects\Resource $resource)
1641 {
1642 $size = $resource->getSize();
1643 return $size !== false && $size <= $this->maxImageInliningSize;
1644 }
1645 private function getCacheKey(\Kibo\Phast\ValueObjects\Resource $resource)
1646 {
1647 return $resource->getUrl()->toString() . '|' . $resource->getCacheSalt() . '|' . $this->maxImageInliningSize;
1648 }
1649 }
1650 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService;
1651
1652 class ImageURLRewriter
1653 {
1654 use \Kibo\Phast\Logging\LoggingTrait;
1655 /**
1656 * @var ServiceSignature
1657 */
1658 protected $signature;
1659 /**
1660 * @var Retriever
1661 */
1662 protected $retriever;
1663 /**
1664 * @var ImageInliningManager
1665 */
1666 protected $inliningManager;
1667 /**
1668 * @var URL
1669 */
1670 protected $baseUrl;
1671 /**
1672 * @var URL
1673 */
1674 protected $serviceUrl;
1675 /**
1676 * @var string[]
1677 */
1678 protected $whitelist;
1679 /**
1680 * @var Resource[]
1681 */
1682 protected $inlinedResources;
1683 /**
1684 * ImageURLRewriter constructor.
1685 * @param ServiceSignature $signature
1686 * @param LocalRetriever $retriever
1687 * @param ImageInliningManager $inliningManager
1688 * @param URL $baseUrl
1689 * @param URL $serviceUrl
1690 * @param array $whitelist
1691 */
1692 public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\Retrievers\LocalRetriever $retriever, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManager $inliningManager, \Kibo\Phast\ValueObjects\URL $baseUrl, \Kibo\Phast\ValueObjects\URL $serviceUrl, array $whitelist)
1693 {
1694 $this->signature = $signature;
1695 $this->retriever = $retriever;
1696 $this->inliningManager = $inliningManager;
1697 $this->baseUrl = $baseUrl;
1698 $this->serviceUrl = $serviceUrl;
1699 $this->whitelist = $whitelist;
1700 }
1701 /**
1702 * @param string $url
1703 * @param URL|null $baseUrl
1704 * @param array $params
1705 * @param bool $mustExist
1706 * @return string
1707 */
1708 public function rewriteUrl($url, \Kibo\Phast\ValueObjects\URL $baseUrl = null, array $params = array(), $mustExist = false)
1709 {
1710 if (strpos($url, '#') === 0) {
1711 return $url;
1712 }
1713 $this->inlinedResources = [];
1714 $absolute = $this->makeURLAbsoluteToBase($url, $baseUrl);
1715 if (!$this->shouldRewriteUrl($absolute)) {
1716 return $url;
1717 }
1718 $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever($absolute, $this->retriever);
1719 if ($mustExist && $resource->getSize() === false) {
1720 return $url;
1721 }
1722 $dataUrl = $this->inliningManager->getUrlForInlining($resource);
1723 if ($dataUrl) {
1724 $this->inlinedResources = [$resource];
1725 return $dataUrl;
1726 }
1727 $params['src'] = $absolute->toString();
1728 return $this->makeSignedUrl($params);
1729 }
1730 /**
1731 * @param $styleContent
1732 * @return string
1733 */
1734 public function rewriteStyle($styleContent)
1735 {
1736 $allInlined = [];
1737 $result = preg_replace_callback('~
1738 (\\b (?: image | background ):)
1739 ([^;}]*)
1740 ~xiS', function ($match) use(&$allInlined) {
1741 return $match[1] . $this->rewriteStyleRule($match[2], $allInlined);
1742 }, $styleContent);
1743 $this->inlinedResources = array_values($allInlined);
1744 return $result;
1745 }
1746 private function rewriteStyleRule($ruleContent, &$allInlined)
1747 {
1748 return preg_replace_callback('~
1749 ( \\b url \\( [\'"]? )
1750 ( [^\'")] ++ )
1751 ~xiS', function ($match) use(&$allInlined) {
1752 $url = $match[1] . $this->rewriteUrl($match[2]);
1753 if (!empty($this->inlinedResources)) {
1754 $inlined = $this->inlinedResources[0];
1755 $allInlined[$inlined->getUrl()->toString()] = $inlined;
1756 }
1757 return $url;
1758 }, $ruleContent);
1759 }
1760 /**
1761 * @return Resource[]
1762 */
1763 public function getInlinedResources()
1764 {
1765 return $this->inlinedResources;
1766 }
1767 /**
1768 * @return string
1769 */
1770 public function getCacheSalt()
1771 {
1772 $parts = array_merge([$this->signature->getCacheSalt(), $this->baseUrl->toString(), $this->serviceUrl->toString(), $this->inliningManager->getMaxImageInliningSize(), '20180413'], array_keys($this->whitelist), array_values($this->whitelist));
1773 return join('-', $parts);
1774 }
1775 /**
1776 * @param string $url
1777 * @param URL|null $baseUrl
1778 * @return URL
1779 */
1780 private function makeURLAbsoluteToBase($url, \Kibo\Phast\ValueObjects\URL $baseUrl = null)
1781 {
1782 $url = trim($url);
1783 if (!$url || substr($url, 0, 5) === 'data:') {
1784 return null;
1785 }
1786 $this->logger()->info('Rewriting img {url}', ['url' => $url]);
1787 $baseUrl = is_null($baseUrl) ? $this->baseUrl : $baseUrl;
1788 return \Kibo\Phast\ValueObjects\URL::fromString($url)->withBase($baseUrl);
1789 }
1790 /**
1791 * @param string $url
1792 * @return bool
1793 */
1794 private function shouldRewriteUrl($url)
1795 {
1796 if (!$url) {
1797 return false;
1798 }
1799 foreach ($this->whitelist as $pattern) {
1800 if (preg_match($pattern, $url)) {
1801 return true;
1802 }
1803 }
1804 $urlObject = \Kibo\Phast\ValueObjects\URL::fromString($url);
1805 if (preg_match('~\\.(jpe?g|gif|png)$~i', $urlObject->getPath()) && $this->retriever->getCacheSalt($urlObject)) {
1806 return true;
1807 }
1808 return false;
1809 }
1810 /**
1811 * @param array $params
1812 * @return string
1813 */
1814 private function makeSignedUrl(array $params)
1815 {
1816 $params['cacheMarker'] = $this->retriever->getCacheSalt(\Kibo\Phast\ValueObjects\URL::fromString($params['src']));
1817 return (new \Kibo\Phast\Services\ServiceRequest())->withParams($params)->withUrl($this->serviceUrl)->sign($this->signature)->serialize();
1818 }
1819 }
1820 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags;
1821
1822 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter, \Kibo\Phast\Filters\HTML\AMPCompatibleFilter
1823 {
1824 const IMG_SRC_ATTR_PATTERN = '~^(|data-(|lazy-|wood-))src$~i';
1825 const IMG_SRCSET_ATTR_PATTERN = '~^(|data-(|lazy-|wood-))srcset$~i';
1826 /**
1827 * @var ImageURLRewriter
1828 */
1829 private $rewriter;
1830 private $inPictureTag = false;
1831 private $inBody = false;
1832 private $imagePathPattern;
1833 /**
1834 * Filter constructor.
1835 * @param ImageURLRewriter $rewriter
1836 */
1837 public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter)
1838 {
1839 $this->rewriter = $rewriter;
1840 $this->imagePathPattern = $this->makeImagePathPattern();
1841 }
1842 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
1843 {
1844 foreach ($elements as $element) {
1845 if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag) {
1846 $this->handleTag($element, $context);
1847 } elseif ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag) {
1848 $this->handleClosingTag($element);
1849 }
1850 (yield $element);
1851 }
1852 }
1853 private function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
1854 {
1855 $isImage = false;
1856 if ($tag->getTagName() == 'img' || $this->inPictureTag && $tag->getTagName() == 'source' || $tag->getTagName() == 'amp-img') {
1857 $isImage = true;
1858 } elseif ($tag->getTagName() == 'picture') {
1859 $this->inPictureTag = true;
1860 } elseif ($tag->getTagName() == 'video' || $tag->getTagName() == 'audio') {
1861 $this->inPictureTag = false;
1862 } elseif ($tag->getTagName() == 'body') {
1863 $this->inBody = true;
1864 } elseif ($tag->getTagName() == 'meta') {
1865 return;
1866 }
1867 foreach ($tag->getAttributes() as $k => $v) {
1868 if (!$v) {
1869 continue;
1870 }
1871 if ($isImage && preg_match(self::IMG_SRC_ATTR_PATTERN, $k)) {
1872 $this->rewriteSrc($tag, $context, $k);
1873 } elseif ($isImage && preg_match(self::IMG_SRCSET_ATTR_PATTERN, $k)) {
1874 $this->rewriteSrcset($tag, $context, $k);
1875 } elseif ($this->inBody && preg_match($this->imagePathPattern, parse_url($v, PHP_URL_PATH))) {
1876 $this->rewriteArbitraryAttribute($tag, $context, $k);
1877 }
1878 }
1879 }
1880 private function makeImagePathPattern()
1881 {
1882 $pieces = [];
1883 foreach (\Kibo\Phast\ValueObjects\Resource::EXTENSION_TO_MIME_TYPE as $ext => $mime) {
1884 if (strpos($mime, 'image/') === 0) {
1885 $pieces[] = preg_quote($ext, '~');
1886 }
1887 }
1888 return '~\\.(?:' . implode('|', $pieces) . ')$~';
1889 }
1890 private function handleClosingTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag $closingTag)
1891 {
1892 if ($closingTag->getTagName() == 'picture') {
1893 $this->inPictureTag = false;
1894 }
1895 }
1896 private function rewriteSrc(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $img, \Kibo\Phast\Filters\HTML\HTMLPageContext $context, $attribute)
1897 {
1898 $url = $img->getAttribute($attribute);
1899 $params = [];
1900 foreach (['width', 'height'] as $attr) {
1901 $value = $img->getAttribute($attr);
1902 if (preg_match('/^[1-9][0-9]*$/', $value)) {
1903 $params[$attr] = $value;
1904 }
1905 }
1906 $newURL = $this->rewriter->rewriteUrl($url, $context->getBaseUrl(), $params);
1907 $img->setAttribute($attribute, $newURL);
1908 }
1909 private function rewriteSrcset(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $img, \Kibo\Phast\Filters\HTML\HTMLPageContext $context, $attribute)
1910 {
1911 $srcset = $img->getAttribute($attribute);
1912 $rewritten = preg_replace_callback('/([^,\\s]+)(\\s+(?:[^,]+))?/', function ($match) use($context) {
1913 $url = $this->rewriter->rewriteUrl($match[1], $context->getBaseUrl());
1914 if (isset($match[2])) {
1915 return $url . $match[2];
1916 }
1917 return $url;
1918 }, $srcset);
1919 $img->setAttribute($attribute, $rewritten);
1920 }
1921 private function rewriteArbitraryAttribute(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $element, \Kibo\Phast\Filters\HTML\HTMLPageContext $context, $attribute)
1922 {
1923 $url = $element->getAttribute($attribute);
1924 $newUrl = $this->rewriter->rewriteUrl($url, $context->getBaseUrl(), [], true);
1925 $element->setAttribute($attribute, $newUrl);
1926 }
1927 }
1928 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService;
1929
1930 class ImageInliningManagerFactory
1931 {
1932 public function make(array $config)
1933 {
1934 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'inline-images-1');
1935 return new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManager($cache, $config['images']['maxImageInliningSize']);
1936 }
1937 }
1938 namespace Kibo\Phast\Filters\HTML;
1939
1940 class HTMLPageContext
1941 {
1942 /**
1943 * @var URL
1944 */
1945 private $baseUrl;
1946 /**
1947 * @var PhastJavaScript[]
1948 */
1949 private $phastJavaScripts = array();
1950 /**
1951 * HTMLPageContext constructor.
1952 * @param URL $baseUrl
1953 */
1954 public function __construct(\Kibo\Phast\ValueObjects\URL $baseUrl)
1955 {
1956 $this->baseUrl = $baseUrl;
1957 }
1958 /**
1959 * @param URL $baseUrl
1960 */
1961 public function setBaseUrl(\Kibo\Phast\ValueObjects\URL $baseUrl)
1962 {
1963 $this->baseUrl = $baseUrl;
1964 }
1965 /**
1966 * @return URL
1967 */
1968 public function getBaseUrl()
1969 {
1970 return $this->baseUrl;
1971 }
1972 /**
1973 * @param PhastJavaScript $script
1974 */
1975 public function addPhastJavascript(\Kibo\Phast\ValueObjects\PhastJavaScript $script)
1976 {
1977 $this->phastJavaScripts[] = $script;
1978 }
1979 /**
1980 * @return PhastJavaScript[]
1981 */
1982 public function getPhastJavaScripts()
1983 {
1984 return $this->phastJavaScripts;
1985 }
1986 }
1987 namespace Kibo\Phast\Filters\HTML\Helpers;
1988
1989 trait JSDetectorTrait
1990 {
1991 /**
1992 * @param Tag $element
1993 * @return bool
1994 */
1995 private function isJSElement(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $element)
1996 {
1997 if (!$element->hasAttribute('type')) {
1998 return true;
1999 }
2000 return (bool) preg_match('~^(text|application)/javascript(;|$)~i', $element->getAttribute('type'));
2001 }
2002 }
2003 namespace Kibo\Phast\Filters\HTML;
2004
2005 abstract class BaseHTMLStreamFilter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
2006 {
2007 /**
2008 * @var HTMLPageContext
2009 */
2010 protected $context;
2011 /**
2012 * @var \Traversable
2013 */
2014 protected $elements;
2015 /**
2016 * @param Tag $tag
2017 * @return Element[]|\Generator
2018 */
2019 protected abstract function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag);
2020 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
2021 {
2022 $this->context = $context;
2023 $this->elements = $elements;
2024 $this->beforeLoop();
2025 foreach ($this->elements as $element) {
2026 if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag && $this->isTagOfInterest($element)) {
2027 foreach ($this->handleTag($element) as $item) {
2028 (yield $item);
2029 }
2030 } else {
2031 (yield $element);
2032 }
2033 }
2034 $this->afterLoop();
2035 }
2036 /**
2037 * @param Tag $tag
2038 * @return bool
2039 */
2040 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2041 {
2042 return true;
2043 }
2044 protected function beforeLoop()
2045 {
2046 }
2047 protected function afterLoop()
2048 {
2049 }
2050 }
2051 namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler;
2052
2053 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
2054 {
2055 /**
2056 * @var PhastJavaScriptCompiler
2057 */
2058 private $compiler;
2059 /**
2060 * Filter constructor.
2061 * @param PhastJavaScriptCompiler $compiler
2062 */
2063 public function __construct(\Kibo\Phast\Filters\HTML\PhastScriptsCompiler\PhastJavaScriptCompiler $compiler)
2064 {
2065 $this->compiler = $compiler;
2066 }
2067 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
2068 {
2069 $buffered = [];
2070 $buffering = false;
2071 foreach ($elements as $element) {
2072 if ($this->isClosingBodyTag($element)) {
2073 if ($buffering) {
2074 foreach ($buffered as $bufElement) {
2075 (yield $bufElement);
2076 }
2077 $buffered = [];
2078 }
2079 $buffering = true;
2080 }
2081 if ($buffering) {
2082 $buffered[] = $element;
2083 } else {
2084 (yield $element);
2085 }
2086 }
2087 $scripts = $context->getPhastJavaScripts();
2088 if (!empty($scripts)) {
2089 (yield $this->compileScript($scripts));
2090 }
2091 foreach ($buffered as $element) {
2092 (yield $element);
2093 }
2094 }
2095 /**
2096 * @param PhastJavaScript[] $scripts
2097 * @return Tag
2098 */
2099 private function compileScript(array $scripts)
2100 {
2101 $names = array_map(function (\Kibo\Phast\ValueObjects\PhastJavaScript $script) {
2102 $matches = [];
2103 preg_match('~[^/]*?\\/?[^/]+$~', $script->getFilename(), $matches);
2104 return $matches[0];
2105 }, $scripts);
2106 $script = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag('script');
2107 $script->setAttribute('data-phast-compiled-js-names', join(',', $names));
2108 $compiled = $this->compiler->compileScriptsWithConfig($scripts);
2109 $script->setTextContent($compiled);
2110 return $script;
2111 }
2112 private function isClosingBodyTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element $element)
2113 {
2114 return $element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag && $element->getTagName() == 'body';
2115 }
2116 }
2117 namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler;
2118
2119 class PhastJavaScriptCompiler
2120 {
2121 /**
2122 * @var Cache
2123 */
2124 private $cache;
2125 /**
2126 * @var string
2127 */
2128 private $serviceUrl;
2129 private $serviceRequestFormat;
2130 /**
2131 * @var \stdClass
2132 */
2133 private $lastCompiledConfig;
2134 /**
2135 * PhastJavaScriptCompiler constructor.
2136 * @param Cache $cache
2137 * @param string $serviceUrl
2138 */
2139 public function __construct(\Kibo\Phast\Cache\Cache $cache, $serviceUrl, $serviceRequestFormat)
2140 {
2141 $this->cache = $cache;
2142 $this->serviceUrl = (new \Kibo\Phast\Services\ServiceRequest())->withUrl(\Kibo\Phast\ValueObjects\URL::fromString((string) $serviceUrl))->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY);
2143 $this->serviceRequestFormat = $serviceRequestFormat;
2144 }
2145 /**
2146 * @return \stdClass|null
2147 */
2148 public function getLastCompiledConfig()
2149 {
2150 return $this->lastCompiledConfig;
2151 }
2152 /**
2153 * @param PhastJavaScript[] $scripts
2154 * @return string
2155 */
2156 public function compileScripts(array $scripts)
2157 {
2158 return $this->cache->get($this->getCacheKey($scripts), function () use($scripts) {
2159 return $this->performCompilation($scripts);
2160 });
2161 }
2162 /**
2163 * @param PhastJavaScript[] $scripts
2164 * @return string
2165 */
2166 public function compileScriptsWithConfig(array $scripts)
2167 {
2168 $bundlerMappings = \Kibo\Phast\Services\Bundler\ShortBundlerParamsParser::getParamsMappings();
2169 $jsMappings = array_combine(array_values($bundlerMappings), array_keys($bundlerMappings));
2170 $resourcesLoader = \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/resources-loader.js', "var Promise=phast.ES6Promise.Promise;phast.ResourceLoader=function(a,b){this.get=function(c){return b.get(c).then(function(d){if(typeof d!==\"string\"){throw new Error(\"response should be string\")}return d}).catch(function(){var e=a.get(c);e.then(function(f){b.set(c,f)});return e})}};phast.ResourceLoader.RequestParams={};phast.ResourceLoader.RequestParams.FaultyParams={};phast.ResourceLoader.RequestParams.fromString=function(g){try{return JSON.parse(g)}catch(h){return phast.ResourceLoader.RequestParams.FaultyParams}};phast.ResourceLoader.BundlerServiceClient=function(i,j,k){var l=phast.ResourceLoader.BundlerServiceClient.RequestsPack;var m=l.PackItem;var n;this.get=function(q){if(q===phast.ResourceLoader.RequestParams.FaultyParams){return Promise.reject(new Error(\"Parameters did not parse as JSON\"))}return new Promise(function(r,s){if(n===undefined){n=new l(j)}n.add(new m({success:r,error:s},q));setTimeout(o);if(n.toQuery().length>4500){console.log(\"[Phast] Resource loader: Pack got too big; flushing early...\");o()}})};function o(){if(n===undefined){return}var t=n;n=undefined;p(t)}function p(u){var v=phast.buildServiceUrl({serviceUrl:i,pathInfo:k},\"service=bundler&\"+u.toQuery());var w=function(){console.error(\"[Phast] Request to bundler failed with status\",y.status);console.log(\"URL:\",v);u.handleError()};var x=function(){if(y.status>=200&&y.status<300){u.handleResponse(y.responseText)}else{u.handleError()}};var y=new XMLHttpRequest;y.open(\"GET\",v);y.addEventListener(\"error\",w);y.addEventListener(\"abort\",w);y.addEventListener(\"load\",x);y.send()}};phast.ResourceLoader.BundlerServiceClient.RequestsPack=function(z){var A={};this.getLength=function(){var F=0;for(var G in A){F++}return F};this.add=function(H){var I;if(H.params.token){I=\"token=\"+H.params.token}else if(H.params.ref){I=\"ref=\"+H.params.ref}else{I=\"\"}if(!A[I]){A[I]={params:H.params,requests:[H.request]}}else{A[I].requests.push(H.request)}};this.toQuery=function(){var J=[],K=[],L=\"\";B().forEach(function(M){var N,O;for(var P in A[M].params){if(P===\"cacheMarker\"){K.push(A[M].params.cacheMarker);continue}N=z[P]?z[P]:P;if(P===\"strip-imports\"){O=encodeURIComponent(N)}else if(P===\"src\"){O=encodeURIComponent(N)+\"=\"+encodeURIComponent(C(A[M].params.src,L));L=A[M].params.src}else{O=encodeURIComponent(N)+\"=\"+encodeURIComponent(A[M].params[P])}J.push(O)}});if(K.length>0){J.unshift(\"c=\"+phast.hash(K.join(\"|\"),23045))}return E(J.join(\"&\"))};function B(){return Object.keys(A).sort(function(R,S){return Q(R,S)?1:Q(S,R)?-1:0});function Q(T,U){if(typeof A[T].params.src!==\"undefined\"&&typeof A[U].params.src!==\"undefined\"){return A[T].params.src>A[U].params.src}return T>U}}function C(V,W){var X=0,Y=Math.pow(36,2)-1;while(X<W.length&&V[X]===W[X]){X++}X=Math.min(X,Y);return D(X)+\"\"+V.substr(X)}function D(Z){var \$=[\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\",\"h\",\"i\",\"j\",\"k\",\"l\",\"m\",\"n\",\"o\",\"p\",\"q\",\"r\",\"s\",\"t\",\"u\",\"v\",\"w\",\"x\",\"y\",\"z\"];var _=Z%36;var aa=Math.floor((Z-_)/36);return \$[aa]+\$[_]}function E(ba){if(!/(^|&)s=/.test(ba)){return ba}return ba.replace(/(%..)|([A-M])|([N-Z])/gi,function(ca,da,ea,fa){if(da){return ca}return String.fromCharCode(ca.charCodeAt(0)+(ea?13:-13))})}this.handleResponse=function(ga){try{var ha=JSON.parse(ga)}catch(ja){this.handleError();return}var ia=B();if(ha.length!==ia.length){console.error(\"[Phast] Requested\",ia.length,\"items from bundler, but got\",ha.length,\"response(s)\");this.handleError();return}ha.forEach(function(ka,la){if(ka.status===200){A[ia[la]].requests.forEach(function(ma){ma.success(ka.content)})}else{A[ia[la]].requests.forEach(function(na){na.error(new Error(\"Got from bundler: \"+JSON.stringify(ka)))})}})}.bind(this);this.handleError=function(){for(var oa in A){A[oa].requests.forEach(function(pa){pa.error()})}}};phast.ResourceLoader.BundlerServiceClient.RequestsPack.PackItem=function(qa,ra){this.request=qa;this.params=ra};phast.ResourceLoader.IndexedDBStorage=function(sa){var ta=phast.ResourceLoader.IndexedDBStorage;var ua=ta.logPrefix;var va=ta.requestToPromise;var wa;Ba();this.get=function(Ca){return xa(\"readonly\").then(function(Da){return va(Da.get(Ca)).catch(ya(\"reading from store\"))})};this.store=function(Ea){return xa(\"readwrite\").then(function(Fa){return va(Fa.put(Ea)).catch(ya(\"writing to store\"))})};this.clear=function(){return xa(\"readwrite\").then(function(Ga){return va(Ga.clear())})};this.iterateOnAll=function(Ha){return xa(\"readonly\").then(function(Ia){return za(Ha,Ia.openCursor()).catch(ya(\"iterating on all\"))})};function xa(Ja){return wa.get().then(function(Ka){try{return Ka.transaction(sa.storeName,Ja).objectStore(sa.storeName)}catch(La){console.error(ua,\"Could not open store; recreating database:\",La);Aa();throw La}})}function ya(Ma){return function(Na){console.error(ua,\"Error \"+Ma+\":\",Na);Aa();throw Na}}function za(Oa,Pa){return new Promise(function(Qa,Ra){Pa.onsuccess=function(Sa){var Ta=Sa.target.result;if(Ta){Oa(Ta.value);Ta.continue()}else{Qa()}};Pa.onerror=Ra})}function Aa(){var Ua=wa.dropDB().then(Ba);wa={get:function(){return Promise.reject(new Error(\"Database is being dropped and recreated\"))},dropDB:function(){return Ua}}}function Ba(){wa=new phast.ResourceLoader.IndexedDBStorage.Connection(sa)}};phast.ResourceLoader.IndexedDBStorage.logPrefix=\"[Phast] Resource loader:\";phast.ResourceLoader.IndexedDBStorage.requestToPromise=function(Va){return new Promise(function(Wa,Xa){Va.onsuccess=function(){Wa(Va.result)};Va.onerror=function(){Xa(Va.error)}})};phast.ResourceLoader.IndexedDBStorage.ConnectionParams=function(){this.dbName=\"phastResourcesCache\";this.dbVersion=1;this.storeName=\"resources\"};phast.ResourceLoader.IndexedDBStorage.StoredResource=function(Ya,Za){this.token=Ya;this.content=Za};phast.ResourceLoader.IndexedDBStorage.Connection=function(\$a){var _a=phast.ResourceLoader.IndexedDBStorage.logPrefix;var ab=phast.ResourceLoader.IndexedDBStorage.requestToPromise;var bb;this.get=cb;this.dropDB=db;function cb(){if(!bb){bb=eb(\$a)}return bb}function db(){return cb().then(function(gb){console.error(_a,\"Dropping DB\");gb.close();bb=null;return ab(window.indexedDB.deleteDatabase(\$a.dbName))})}function eb(hb){if(typeof window.indexedDB===\"undefined\"){return Promise.reject(new Error(\"IndexedDB is not available\"))}var ib=window.indexedDB.open(hb.dbName,hb.dbVersion);ib.onupgradeneeded=function(){fb(ib.result,hb)};return ab(ib).then(function(jb){jb.onversionchange=function(){console.debug(_a,\"Closing DB\");jb.close();if(bb){bb=null}};return jb}).catch(function(kb){console.log(_a,\"IndexedDB cache is not available. This is usually due to using private browsing mode.\");throw kb})}function fb(lb,mb){lb.createObjectStore(mb.storeName,{keyPath:\"token\"})}};phast.ResourceLoader.StorageCache=function(nb,ob){var pb=phast.ResourceLoader.IndexedDBStorage.StoredResource;this.get=function(xb){return sb(rb(xb))};this.set=function(yb,zb){return tb(rb(yb),zb,false)};var qb=null;function rb(Ab){return JSON.stringify(Ab)}function sb(Bb){return ob.get(Bb).then(function(Cb){if(Cb){return Promise.resolve(Cb.content)}return Promise.resolve()})}function tb(Db,Eb,Fb){return wb().then(function(Gb){var Hb=Eb.length+Gb;if(Hb>nb.maxStorageSize){return Fb||Eb.length>nb.maxStorageSize?Promise.reject(new Error(\"Storage quota will be exceeded\")):ub(Db,Eb)}qb=Hb;var Ib=new pb(Db,Eb);return ob.store(Ib)})}function ub(Jb,Kb){return vb().then(function(){return tb(Jb,Kb,true)})}function vb(){return ob.clear().then(function(){qb=0})}function wb(){if(qb!==null){return Promise.resolve(qb)}var Lb=0;return ob.iterateOnAll(function(Mb){Lb+=Mb.content.length}).then(function(){qb=Lb;return Promise.resolve(qb)})}};phast.ResourceLoader.StorageCache.StorageCacheParams=function(){this.maxStorageSize=4.5*1024*1024};phast.ResourceLoader.BlackholeCache=function(){this.get=function(){return Promise.reject()};this.set=function(){return Promise.reject()}};phast.ResourceLoader.make=function(Nb,Ob,Pb){var Qb=Sb();var Rb=new phast.ResourceLoader.BundlerServiceClient(Nb,Ob,Pb);return new phast.ResourceLoader(Rb,Qb);function Sb(){var Tb=window.navigator.userAgent;if(/safari/i.test(Tb)&&!/chrome|android/i.test(Tb)){console.log(\"[Phast] Not using IndexedDB cache on Safari\");return new phast.ResourceLoader.BlackholeCache}else{var Ub=new phast.ResourceLoader.IndexedDBStorage.ConnectionParams;var Vb=new phast.ResourceLoader.IndexedDBStorage(Ub);var Wb=new phast.ResourceLoader.StorageCache.StorageCacheParams;return new phast.ResourceLoader.StorageCache(Wb,Vb)}}};\n");
2171 $resourcesLoader->setConfig('resourcesLoader', ['serviceUrl' => (string) $this->serviceUrl, 'shortParamsMappings' => $jsMappings, 'pathInfo' => $this->serviceRequestFormat === \Kibo\Phast\Services\ServiceRequest::FORMAT_PATH]);
2172 $scripts = array_merge([\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/runner.js', "phast.config=JSON.parse(atob(phast.config));while(phast.scripts.length){phast.scripts.shift()()}\n"), \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/es6-promise.js', "(function(a,b){typeof exports===\"object\"&&typeof module!==\"undefined\"?module.exports=b():typeof define===\"function\"&&define.amd?define(b):a.ES6Promise=b()})(phast,function(){\"use strict\";function c(ia){var ja=typeof ia;return ia!==null&&(ja===\"object\"||ja===\"function\")}function d(ka){return typeof ka===\"function\"}var e=void 0;if(Array.isArray){e=Array.isArray}else{e=function(la){return Object.prototype.toString.call(la)===\"[object Array]\"}}var f=e;var g=0;var h=void 0;var i=void 0;var j=function ma(na,oa){w[g]=na;w[g+1]=oa;g+=2;if(g===2){if(i){i(x)}else{z()}}};function k(pa){i=pa}function l(qa){j=qa}var m=typeof window!==\"undefined\"?window:undefined;var n=m||{};var o=n.MutationObserver||n.WebKitMutationObserver;var p=typeof self===\"undefined\"&&typeof process!==\"undefined\"&&{}.toString.call(process)===\"[object process]\";var q=typeof Uint8ClampedArray!==\"undefined\"&&typeof importScripts!==\"undefined\"&&typeof MessageChannel!==\"undefined\";function r(){return function(){return process.nextTick(x)}}function s(){if(typeof h!==\"undefined\"){return function(){h(x)}}return v()}function t(){var ra=0;var sa=new o(x);var ta=document.createTextNode(\"\");sa.observe(ta,{characterData:true});return function(){ta.data=ra=++ra%2}}function u(){var ua=new MessageChannel;ua.port1.onmessage=x;return function(){return ua.port2.postMessage(0)}}function v(){var va=setTimeout;return function(){return va(x,1)}}var w=new Array(1e3);function x(){for(var wa=0;wa<g;wa+=2){var xa=w[wa];var ya=w[wa+1];xa(ya);w[wa]=undefined;w[wa+1]=undefined}g=0}function y(){try{var za=Function(\"return this\")().require(\"vertx\");h=za.runOnLoop||za.runOnContext;return s()}catch(Aa){return v()}}var z=void 0;if(p){z=r()}else if(o){z=t()}else if(q){z=u()}else if(m===undefined&&typeof require===\"function\"){z=y()}else{z=v()}function A(Ba,Ca){var Da=this;var Ea=new this.constructor(D);if(Ea[C]===undefined){\$(Ea)}var Fa=Da._state;if(Fa){var Ga=arguments[Fa-1];j(function(){return W(Fa,Ea,Ga,Da._result)})}else{T(Da,Ea,Ba,Ca)}return Ea}function B(Ha){var Ia=this;if(Ha&&typeof Ha===\"object\"&&Ha.constructor===Ia){return Ha}var Ja=new Ia(D);P(Ja,Ha);return Ja}var C=Math.random().toString(36).substring(2);function D(){}var E=void 0;var F=1;var G=2;var H={error:null};function I(){return new TypeError(\"You cannot resolve a promise with itself\")}function J(){return new TypeError(\"A promises callback cannot return that same promise.\")}function K(Ka){try{return Ka.then}catch(La){H.error=La;return H}}function L(Ma,Na,Oa,Pa){try{Ma.call(Na,Oa,Pa)}catch(Qa){return Qa}}function M(Ra,Sa,Ta){j(function(Ua){var Va=false;var Wa=L(Ta,Sa,function(Xa){if(Va){return}Va=true;if(Sa!==Xa){P(Ua,Xa)}else{R(Ua,Xa)}},function(Ya){if(Va){return}Va=true;S(Ua,Ya)},\"Settle: \"+(Ua._label||\" unknown promise\"));if(!Va&&Wa){Va=true;S(Ua,Wa)}},Ra)}function N(Za,\$a){if(\$a._state===F){R(Za,\$a._result)}else if(\$a._state===G){S(Za,\$a._result)}else{T(\$a,undefined,function(_a){return P(Za,_a)},function(ab){return S(Za,ab)})}}function O(bb,cb,db){if(cb.constructor===bb.constructor&&db===A&&cb.constructor.resolve===B){N(bb,cb)}else{if(db===H){S(bb,H.error);H.error=null}else if(db===undefined){R(bb,cb)}else if(d(db)){M(bb,cb,db)}else{R(bb,cb)}}}function P(eb,fb){if(eb===fb){S(eb,I())}else if(c(fb)){O(eb,fb,K(fb))}else{R(eb,fb)}}function Q(gb){if(gb._onerror){gb._onerror(gb._result)}U(gb)}function R(hb,ib){if(hb._state!==E){return}hb._result=ib;hb._state=F;if(hb._subscribers.length!==0){j(U,hb)}}function S(jb,kb){if(jb._state!==E){return}jb._state=G;jb._result=kb;j(Q,jb)}function T(lb,mb,nb,ob){var pb=lb._subscribers;var qb=pb.length;lb._onerror=null;pb[qb]=mb;pb[qb+F]=nb;pb[qb+G]=ob;if(qb===0&&lb._state){j(U,lb)}}function U(rb){var sb=rb._subscribers;var tb=rb._state;if(sb.length===0){return}var ub=void 0,vb=void 0,wb=rb._result;for(var xb=0;xb<sb.length;xb+=3){ub=sb[xb];vb=sb[xb+tb];if(ub){W(tb,ub,vb,wb)}else{vb(wb)}}rb._subscribers.length=0}function V(yb,zb){try{return yb(zb)}catch(Ab){H.error=Ab;return H}}function W(Bb,Cb,Db,Eb){var Fb=d(Db),Gb=void 0,Hb=void 0,Ib=void 0,Jb=void 0;if(Fb){Gb=V(Db,Eb);if(Gb===H){Jb=true;Hb=Gb.error;Gb.error=null}else{Ib=true}if(Cb===Gb){S(Cb,J());return}}else{Gb=Eb;Ib=true}if(Cb._state!==E){}else if(Fb&&Ib){P(Cb,Gb)}else if(Jb){S(Cb,Hb)}else if(Bb===F){R(Cb,Gb)}else if(Bb===G){S(Cb,Gb)}}function X(Kb,Lb){try{Lb(function Mb(Nb){P(Kb,Nb)},function Ob(Pb){S(Kb,Pb)})}catch(Qb){S(Kb,Qb)}}var Y=0;function Z(){return Y++}function \$(Rb){Rb[C]=Y++;Rb._state=undefined;Rb._result=undefined;Rb._subscribers=[]}function _(){return new Error(\"Array Methods must be provided an Array\")}var aa=function(){function Sb(Tb,Ub){this._instanceConstructor=Tb;this.promise=new Tb(D);if(!this.promise[C]){\$(this.promise)}if(f(Ub)){this.length=Ub.length;this._remaining=Ub.length;this._result=new Array(this.length);if(this.length===0){R(this.promise,this._result)}else{this.length=this.length||0;this._enumerate(Ub);if(this._remaining===0){R(this.promise,this._result)}}}else{S(this.promise,_())}}Sb.prototype._enumerate=function Vb(Wb){for(var Xb=0;this._state===E&&Xb<Wb.length;Xb++){this._eachEntry(Wb[Xb],Xb)}};Sb.prototype._eachEntry=function Yb(Zb,\$b){var _b=this._instanceConstructor;var ac=_b.resolve;if(ac===B){var bc=K(Zb);if(bc===A&&Zb._state!==E){this._settledAt(Zb._state,\$b,Zb._result)}else if(typeof bc!==\"function\"){this._remaining--;this._result[\$b]=Zb}else if(_b===ga){var cc=new _b(D);O(cc,Zb,bc);this._willSettleAt(cc,\$b)}else{this._willSettleAt(new _b(function(dc){return dc(Zb)}),\$b)}}else{this._willSettleAt(ac(Zb),\$b)}};Sb.prototype._settledAt=function ec(fc,gc,hc){var ic=this.promise;if(ic._state===E){this._remaining--;if(fc===G){S(ic,hc)}else{this._result[gc]=hc}}if(this._remaining===0){R(ic,this._result)}};Sb.prototype._willSettleAt=function jc(kc,lc){var mc=this;T(kc,undefined,function(nc){return mc._settledAt(F,lc,nc)},function(oc){return mc._settledAt(G,lc,oc)})};return Sb}();function ba(pc){return new aa(this,pc).promise}function ca(qc){var rc=this;if(!f(qc)){return new rc(function(sc,tc){return tc(new TypeError(\"You must pass an array to race.\"))})}else{return new rc(function(uc,vc){var wc=qc.length;for(var xc=0;xc<wc;xc++){rc.resolve(qc[xc]).then(uc,vc)}})}}function da(yc){var zc=this;var Ac=new zc(D);S(Ac,yc);return Ac}function ea(){throw new TypeError(\"You must pass a resolver function as the first argument to the promise constructor\")}function fa(){throw new TypeError(\"Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.\")}var ga=function(){function Bc(Cc){this[C]=Z();this._result=this._state=undefined;this._subscribers=[];if(D!==Cc){typeof Cc!==\"function\"&&ea();this instanceof Bc?X(this,Cc):fa()}}Bc.prototype.catch=function Dc(Ec){return this.then(null,Ec)};Bc.prototype.finally=function Fc(Gc){var Hc=this;var Ic=Hc.constructor;return Hc.then(function(Jc){return Ic.resolve(Gc()).then(function(){return Jc})},function(Kc){return Ic.resolve(Gc()).then(function(){throw Kc})})};return Bc}();ga.prototype.then=A;ga.all=ba;ga.race=ca;ga.resolve=B;ga.reject=da;ga._setScheduler=k;ga._setAsap=l;ga._asap=j;function ha(){var Lc=void 0;if(typeof global!==\"undefined\"){Lc=global}else if(typeof self!==\"undefined\"){Lc=self}else{try{Lc=Function(\"return this\")()}catch(Oc){throw new Error(\"polyfill failed because global object is unavailable in this environment\")}}var Mc=Lc.Promise;if(Mc){var Nc=null;try{Nc=Object.prototype.toString.call(Mc.resolve())}catch(Pc){}if(Nc===\"[object Promise]\"&&!Mc.cast){return}}Lc.Promise=ga}ga.polyfill=ha;ga.Promise=ga;return ga});\n"), \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/hash.js', "function murmurhash3_32_gc(a,b){var c,d,e,f,g,h,i,j,k,l;c=a.length&3;d=a.length-c;e=b;g=3432918353;i=461845907;l=0;while(l<d){k=a.charCodeAt(l)&255|(a.charCodeAt(++l)&255)<<8|(a.charCodeAt(++l)&255)<<16|(a.charCodeAt(++l)&255)<<24;++l;k=(k&65535)*g+(((k>>>16)*g&65535)<<16)&4294967295;k=k<<15|k>>>17;k=(k&65535)*i+(((k>>>16)*i&65535)<<16)&4294967295;e^=k;e=e<<13|e>>>19;f=(e&65535)*5+(((e>>>16)*5&65535)<<16)&4294967295;e=(f&65535)+27492+(((f>>>16)+58964&65535)<<16)}k=0;switch(c){case 3:k^=(a.charCodeAt(l+2)&255)<<16;case 2:k^=(a.charCodeAt(l+1)&255)<<8;case 1:k^=a.charCodeAt(l)&255;k=(k&65535)*g+(((k>>>16)*g&65535)<<16)&4294967295;k=k<<15|k>>>17;k=(k&65535)*i+(((k>>>16)*i&65535)<<16)&4294967295;e^=k}e^=a.length;e^=e>>>16;e=(e&65535)*2246822507+(((e>>>16)*2246822507&65535)<<16)&4294967295;e^=e>>>13;e=(e&65535)*3266489909+(((e>>>16)*3266489909&65535)<<16)&4294967295;e^=e>>>16;return e>>>0}phast.hash=murmurhash3_32_gc;\n"), \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/service-url.js', "phast.buildServiceUrl=function(a,b){if(a.pathInfo){return appendPathInfo(a.serviceUrl,buildQuery(b))}else{return appendQueryString(a.serviceUrl,buildQuery(b))}};function buildQuery(c){if(typeof c===\"string\"){return c}var d=[];for(var e in c){if(c.hasOwnProperty(e)){d.push(encodeURIComponent(e)+\"=\"+encodeURIComponent(c[e]))}}return d.join(\"&\")}function appendPathInfo(f,g){var h=btoa(g).replace(/=/g,\"\").replace(/\\//g,\"_\").replace(/\\+/g,\"-\");return f.replace(/\\?.*\$/,\"\").replace(/\\/__p__\\.js\$/,\"\")+\"/\"+h+\".q.js\"}function appendQueryString(i,j){var k=i.indexOf(\"?\")>-1?\"&\":\"?\";return i+k+j}\n"), $resourcesLoader, \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/PhastScriptsCompiler/phast.js', "var Promise=phast.ES6Promise;phast.ResourceLoader.instance=phast.ResourceLoader.make(phast.config.resourcesLoader.serviceUrl,phast.config.resourcesLoader.shortParamsMappings,phast.config.resourcesLoader.pathInfo);phast.forEachSelectedElement=function(a,b){Array.prototype.forEach.call(window.document.querySelectorAll(a),b)};phast.once=function(c){var d=false;return function(){if(!d){d=true;c.apply(this,Array.prototype.slice(arguments))}}};phast.on=function(e,f){return new Promise(function(g){e.addEventListener(f,g)})};phast.wait=function(h){return new Promise(function(i){setTimeout(i,h)})};phast.on(document,\"DOMContentLoaded\").then(function(){var j,k;function l(n){return n&&n.nodeType===8&&/^\\s*\\[Phast\\]/.test(n.textContent)}function m(o){while(o){if(l(o)){return o}o=o.nextSibling}return false}k=m(document.documentElement.nextSibling);if(k===false){k=m(document.body.firstChild)}if(k){j=k.textContent.replace(/^\\s+|\\s+\$/g,\"\").split(\"\\n\");console.groupCollapsed(j.shift());console.log(j.join(\"\\n\"));console.groupEnd()}});phast.on(document,\"DOMContentLoaded\").then(function(){var p=performance.timing;var q=[];q.push([\"Downloading phases:\"]);q.push([\" Look up hostname in DNS + %s ms\",t(p.domainLookupEnd-p.fetchStart)]);q.push([\" Establish connection + %s ms\",t(p.connectEnd-p.domainLookupEnd)]);q.push([\" Send request + %s ms\",t(p.requestStart-p.connectEnd)]);q.push([\" Receive first byte + %s ms\",t(p.responseStart-p.requestStart)]);q.push([\" Download page + %s ms\",t(p.responseEnd-p.responseStart)]);q.push([\"\"]);q.push([\"Totals:\"]);q.push([\" Time to first byte %s ms\",t(p.responseStart-p.fetchStart)]);q.push([\" (since request start) %s ms\",t(p.responseStart-p.requestStart)]);q.push([\" Total request time %s ms\",t(p.responseEnd-p.fetchStart)]);q.push([\" (since request start) %s ms\",t(p.responseEnd-p.requestStart)]);q.push([\" \"]);var r=[];var s=[];q.forEach(function(u){r.push(u.shift());s=s.concat(u)});console.groupCollapsed(\"[Phast] Client-side performance metrics\");console.log.apply(console,[r.join(\"\\n\")].concat(s));console.groupEnd();function t(v){v=\"\"+v;while(v.length<4){v=\" \"+v}return v}});\n")], $scripts);
2173 $compiled = $this->compileScripts($scripts);
2174 return '(' . $compiled . ')(' . $this->compileConfig($scripts) . ');';
2175 }
2176 /**
2177 * @param PhastJavaScript[] $scripts
2178 * @return string
2179 */
2180 private function performCompilation(array $scripts)
2181 {
2182 $compiled = implode(',', array_map(function (\Kibo\Phast\ValueObjects\PhastJavaScript $script) {
2183 return $this->interpolate($script->getContents());
2184 }, $scripts));
2185 return 'function phastScripts(phast){phast.scripts=[' . $compiled . '];(phast.scripts.shift())();}';
2186 }
2187 /**
2188 * @param PhastJavaScript[] $scripts
2189 * @return string
2190 */
2191 private function compileConfig(array $scripts)
2192 {
2193 $config = new \stdClass();
2194 foreach ($scripts as $script) {
2195 if ($script->hasConfig()) {
2196 $config->{$script->getConfigKey()} = $script->getConfig();
2197 }
2198 }
2199 $this->lastCompiledConfig = $config;
2200 return \Kibo\Phast\Common\JSON::encode(['config' => base64_encode(\Kibo\Phast\Common\JSON::encode($config))]);
2201 }
2202 /**
2203 * @param string $script
2204 * @return string
2205 */
2206 private function interpolate($script)
2207 {
2208 return sprintf('(function(){%s})', $script);
2209 }
2210 /**
2211 * @param PhastJavaScript[] $scripts
2212 * @return string
2213 */
2214 private function getCacheKey(array $scripts)
2215 {
2216 return array_reduce($scripts, function ($carry, \Kibo\Phast\ValueObjects\PhastJavaScript $script) {
2217 $carry .= $script->getFilename() . '-' . $script->getCacheSalt() . "\n";
2218 return $carry;
2219 }, '');
2220 }
2221 }
2222 namespace Kibo\Phast\Filters\HTML\LazyImageLoading;
2223
2224 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2225 {
2226 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2227 {
2228 if (!$tag->hasAttribute('loading')) {
2229 $tag->setAttribute('loading', 'lazy');
2230 }
2231 (yield $tag);
2232 }
2233 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2234 {
2235 return $tag->getTagName() == 'img';
2236 }
2237 }
2238 namespace Kibo\Phast\Filters\HTML\ScriptsProxyService;
2239
2240 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2241 {
2242 use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait, \Kibo\Phast\Logging\LoggingTrait;
2243 /**
2244 * @var array
2245 */
2246 private $config;
2247 /**
2248 * @var ServiceSignature
2249 */
2250 private $signature;
2251 /**
2252 * @var LocalRetriever
2253 */
2254 private $retriever;
2255 private $tokenRefMaker;
2256 /**
2257 * @var ObjectifiedFunctions
2258 */
2259 private $functions;
2260 /**
2261 * @var bool
2262 */
2263 private $didInject = false;
2264 public function __construct(array $config, \Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\Retrievers\LocalRetriever $retriever, \Kibo\Phast\Services\Bundler\TokenRefMaker $tokenRefMaker, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
2265 {
2266 $this->config = $config;
2267 $this->signature = $signature;
2268 $this->retriever = $retriever;
2269 $this->tokenRefMaker = $tokenRefMaker;
2270 $this->functions = is_null($functions) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $functions;
2271 }
2272 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2273 {
2274 return $tag->getTagName() == 'script' && $this->isJSElement($tag);
2275 }
2276 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
2277 {
2278 $this->rewriteScriptSource($script);
2279 if (!$this->didInject) {
2280 $this->addScript();
2281 $this->didInject = true;
2282 }
2283 (yield $script);
2284 }
2285 private function rewriteScriptSource(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $element)
2286 {
2287 if (!$element->hasAttribute('src')) {
2288 return;
2289 }
2290 $src = trim($element->getAttribute('src'));
2291 $url = $this->getAbsoluteURL($src);
2292 $cacheMarker = $this->retriever->getCacheSalt($url);
2293 if (!$cacheMarker) {
2294 return;
2295 }
2296 $cacheMarker .= '-' . \Kibo\Phast\Filters\JavaScript\Minification\JSMinifierFilter::VERSION;
2297 $element->setAttribute('src', $this->makeProxiedURL($url, $cacheMarker));
2298 $element->setAttribute('data-phast-original-src', (string) $url);
2299 $element->setAttribute('data-phast-params', $this->makeServiceParams($url, $cacheMarker));
2300 }
2301 private function makeProxiedURL(\Kibo\Phast\ValueObjects\URL $url, $cacheMarker)
2302 {
2303 $params = ['service' => 'scripts', 'src' => (string) $url->withoutQuery(), 'cacheMarker' => $cacheMarker];
2304 return (new \Kibo\Phast\Services\ServiceRequest())->withUrl(\Kibo\Phast\ValueObjects\URL::fromString($this->config['serviceUrl']))->withParams($params)->serialize();
2305 }
2306 private function makeServiceParams(\Kibo\Phast\ValueObjects\URL $url, $cacheMarker)
2307 {
2308 return \Kibo\Phast\Services\Bundler\ServiceParams::fromArray(['src' => (string) $url->withoutQuery(), 'cacheMarker' => $cacheMarker, 'isScript' => '1'])->sign($this->signature)->replaceByTokenRef($this->tokenRefMaker)->serialize();
2309 }
2310 private function addScript()
2311 {
2312 $config = ['serviceUrl' => $this->config['serviceUrl'], 'pathInfo' => \Kibo\Phast\Services\ServiceRequest::getDefaultSerializationMode() === \Kibo\Phast\Services\ServiceRequest::FORMAT_PATH, 'urlRefreshTime' => $this->config['urlRefreshTime'], 'whitelist' => $this->config['match']];
2313 $script = \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/ScriptsProxyService/rewrite-function.js', "var config=phast.config[\"script-proxy-service\"];var urlPattern=/^(https?:)?\\/\\//;var cacheMarker=Math.floor((new Date).getTime()/1e3/config.urlRefreshTime);var whitelist=compileWhitelistPatterns(config.whitelist);phast.scripts.push(function(){overrideDOMMethod(\"appendChild\");overrideDOMMethod(\"insertBefore\")});function compileWhitelistPatterns(a){var b=/^(.)(.*)\\1([a-z]*)\$/i;var c=[];a.forEach(function(d){var e=b.exec(d);if(!e){window.console&&window.console.log(\"Phast: Not a pattern:\",d);return}try{c.push(new RegExp(e[2],e[3]))}catch(f){window.console&&window.console.log(\"Phast: Failed to compile pattern:\",d)}});return c}function checkWhitelist(g){for(var h=0;h<whitelist.length;h++){if(whitelist[h].exec(g)){return true}}return false}function overrideDOMMethod(i){var j=Element.prototype[i];var k=function(){var l=processNode(arguments[0]);var m=j.apply(this,arguments);l();return m};Element.prototype[i]=k;window.addEventListener(\"load\",function(){if(Element.prototype[i]===k){delete Element.prototype[i]}})}function processNode(n){if(!n||n.nodeType!==Node.ELEMENT_NODE||n.tagName!==\"SCRIPT\"||!urlPattern.test(n.src)||n.src.substr(0,config.serviceUrl.length)===config.serviceUrl||!checkWhitelist(n.src)){return function(){}}var o=n.src;n.src=phast.buildServiceUrl(config,{service:\"scripts\",src:o,cacheMarker:cacheMarker});return function(){n.src=o}}\n");
2314 $script->setConfig('script-proxy-service', $config);
2315 $this->context->addPhastJavaScript($script);
2316 }
2317 private function getAbsoluteURL($url)
2318 {
2319 return \Kibo\Phast\ValueObjects\URL::fromString($url)->withBase($this->context->getBaseUrl());
2320 }
2321 }
2322 namespace Kibo\Phast\Filters\HTML\BaseURLSetter;
2323
2324 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2325 {
2326 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2327 {
2328 return $tag->getTagName() == 'base' && $tag->hasAttribute('href');
2329 }
2330 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2331 {
2332 $base = \Kibo\Phast\ValueObjects\URL::fromString($tag->getAttribute('href'));
2333 $current = $this->context->getBaseUrl();
2334 $this->context->setBaseUrl($base->withBase($current));
2335 (yield $tag);
2336 }
2337 }
2338 namespace Kibo\Phast\Filters\HTML\CSSInlining;
2339
2340 class OptimizerFactory
2341 {
2342 /**
2343 * @var Cache
2344 */
2345 private $cache;
2346 public function __construct(array $config)
2347 {
2348 $this->cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'css-optimizitor');
2349 }
2350 /**
2351 * @param \Traversable $elements
2352 * @return Optimizer
2353 */
2354 public function makeForElements(\Traversable $elements)
2355 {
2356 return new \Kibo\Phast\Filters\HTML\CSSInlining\Optimizer($elements, $this->cache);
2357 }
2358 }
2359 namespace Kibo\Phast\Filters\HTML\CSSInlining;
2360
2361 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2362 {
2363 use \Kibo\Phast\Logging\LoggingTrait;
2364 const CSS_IMPORTS_REGEXP = '~
2365 @import \\s++
2366 ( url \\( )?+ # url() is optional
2367 ( (?(1) ["\']?+ | ["\'] ) ) # without url() a quote is necessary
2368 \\s*+ (?<url>[A-Za-z0-9_/.:?&=+%,-]++) \\s*+
2369 \\2 # match ending quote
2370 (?(1)\\)) # match closing paren if url( was used
2371 \\s*+ ;
2372 ~xi';
2373 /**
2374 * @var ServiceSignature
2375 */
2376 private $signature;
2377 /**
2378 * @var int
2379 */
2380 private $maxInlineDepth = 2;
2381 /**
2382 * @var URL
2383 */
2384 private $baseURL;
2385 /**
2386 * @var string[]
2387 */
2388 private $whitelist = array();
2389 /**
2390 * @var string
2391 */
2392 private $serviceUrl;
2393 /**
2394 * @var int
2395 */
2396 private $optimizerSizeDiffThreshold;
2397 /**
2398 * @var Retriever
2399 */
2400 private $localRetriever;
2401 /**
2402 * @var Retriever
2403 */
2404 private $retriever;
2405 /**
2406 * @var OptimizerFactory
2407 */
2408 private $optimizerFactory;
2409 /**
2410 * @var ServiceFilter
2411 */
2412 private $cssFilter;
2413 /**
2414 * @var Optimizer
2415 */
2416 private $optimizer;
2417 /**
2418 * @var TokenRefMaker
2419 */
2420 private $tokenRefMaker;
2421 /**
2422 * @var string[]
2423 */
2424 private $cacheMarkers = array();
2425 public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\ValueObjects\URL $baseURL, array $config, \Kibo\Phast\Retrievers\Retriever $localRetriever, \Kibo\Phast\Retrievers\Retriever $retriever, \Kibo\Phast\Filters\HTML\CSSInlining\OptimizerFactory $optimizerFactory, \Kibo\Phast\Services\ServiceFilter $cssFilter, \Kibo\Phast\Services\Bundler\TokenRefMaker $tokenRefMaker)
2426 {
2427 $this->signature = $signature;
2428 $this->baseURL = $baseURL;
2429 $this->serviceUrl = \Kibo\Phast\ValueObjects\URL::fromString((string) $config['serviceUrl']);
2430 $this->optimizerSizeDiffThreshold = (int) $config['optimizerSizeDiffThreshold'];
2431 $this->localRetriever = $localRetriever;
2432 $this->retriever = $retriever;
2433 $this->optimizerFactory = $optimizerFactory;
2434 $this->cssFilter = $cssFilter;
2435 $this->tokenRefMaker = $tokenRefMaker;
2436 foreach ($config['whitelist'] as $key => $value) {
2437 if (!is_array($value)) {
2438 $this->whitelist[$value] = ['ieCompatible' => true];
2439 $key = $value;
2440 } else {
2441 $this->whitelist[$key] = $value;
2442 }
2443 }
2444 }
2445 protected function beforeLoop()
2446 {
2447 $this->elements = iterator_to_array($this->elements);
2448 $this->optimizer = $this->optimizerFactory->makeForElements(new \ArrayIterator($this->elements));
2449 }
2450 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2451 {
2452 return $tag->getTagName() == 'style' || $tag->getTagName() == 'link' && $tag->getAttribute('rel') == 'stylesheet' && $tag->hasAttribute('href');
2453 }
2454 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2455 {
2456 if ($tag->getTagName() == 'link') {
2457 return $this->inlineLink($tag, $this->context->getBaseUrl());
2458 }
2459 return $this->inlineStyle($tag);
2460 }
2461 protected function afterLoop()
2462 {
2463 $this->addIEFallbackScript();
2464 $this->addInlinedRetrieverScript();
2465 }
2466 private function inlineLink(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $link, \Kibo\Phast\ValueObjects\URL $baseUrl)
2467 {
2468 $href = trim($link->getAttribute('href'));
2469 if (trim($href, '/') == '') {
2470 return [$link];
2471 }
2472 $location = \Kibo\Phast\ValueObjects\URL::fromString($href)->withBase($baseUrl);
2473 if (!$this->findInWhitelist($location) && !$this->localRetriever->getCacheSalt($location)) {
2474 return [$link];
2475 }
2476 $media = $link->getAttribute('media');
2477 if (preg_match('~^\\s*(this\\.)?media\\s*=\\s*(?<q>[\'"])(?<m>((?!\\k<q>).)+?)\\k<q>\\s*(;|$)~', $link->getAttribute('onload'), $match)) {
2478 $media = $match['m'];
2479 }
2480 $elements = $this->inlineURL($location, $media);
2481 return is_null($elements) ? [$link] : $elements;
2482 }
2483 private function inlineStyle(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $style)
2484 {
2485 $processed = $this->cssFilter->apply(\Kibo\Phast\ValueObjects\Resource::makeWithContent($this->baseURL, $style->textContent), [])->getContent();
2486 $elements = $this->inlineCSS($this->baseURL, $processed, $style->getAttribute('media'), false);
2487 if (($id = $style->getAttribute('id')) != '') {
2488 if (sizeof($elements) == 1) {
2489 $elements[0]->setAttribute('id', $id);
2490 } else {
2491 foreach ($elements as $element) {
2492 $element->setAttribute('data-phast-original-id', $id);
2493 }
2494 }
2495 }
2496 return $elements;
2497 }
2498 private function findInWhitelist(\Kibo\Phast\ValueObjects\URL $url)
2499 {
2500 $stringUrl = (string) $url;
2501 foreach ($this->whitelist as $pattern => $settings) {
2502 if (preg_match($pattern, $stringUrl)) {
2503 return $settings;
2504 }
2505 }
2506 return false;
2507 }
2508 /**
2509 * @param URL $url
2510 * @param string $media
2511 * @param boolean $ieCompatible
2512 * @param int $currentLevel
2513 * @param string[] $seen
2514 * @return Tag[]|null
2515 * @throws \Kibo\Phast\Exceptions\ItemNotFoundException
2516 */
2517 private function inlineURL(\Kibo\Phast\ValueObjects\URL $url, $media, $ieCompatible = true, $currentLevel = 0, $seen = array())
2518 {
2519 $whitelistEntry = $this->findInWhitelist($url);
2520 if (!$whitelistEntry) {
2521 $whitelistEntry = !!$this->localRetriever->getCacheSalt($url);
2522 }
2523 if (!$whitelistEntry) {
2524 $this->logger()->info('Not inlining {url}. Not in whitelist', ['url' => $url]);
2525 return [$this->makeLink($url, $media)];
2526 }
2527 if (isset($whitelistEntry['ieCompatible']) && !$whitelistEntry['ieCompatible']) {
2528 $ieFallbackUrl = $ieCompatible ? $url : null;
2529 $ieCompatible = false;
2530 } else {
2531 $ieFallbackUrl = null;
2532 }
2533 if (in_array($url, $seen)) {
2534 return [];
2535 }
2536 if ($currentLevel > $this->maxInlineDepth) {
2537 return $this->addIEFallback($ieFallbackUrl, [$this->makeLink($url, $media)]);
2538 }
2539 $seen[] = $url;
2540 $this->logger()->info('Inlining {url}.', ['url' => (string) $url]);
2541 $content = $this->retriever->retrieve($url);
2542 if ($content === false) {
2543 return $this->addIEFallback($ieFallbackUrl, [$this->makeServiceLink($url, $media)]);
2544 }
2545 $content = $this->cssFilter->apply(\Kibo\Phast\ValueObjects\Resource::makeWithContent($url, $content), [])->getContent();
2546 $this->cacheMarkers[$url->toString()] = \Kibo\Phast\Common\Base64url::shortHash(implode("\0", [$this->retriever->getCacheSalt($url), $content]));
2547 $optimized = $this->optimizer->optimizeCSS($content);
2548 if ($optimized === null) {
2549 $this->logger()->error('CSS optimizer failed for {url}', ['url' => (string) $url]);
2550 return null;
2551 }
2552 $isOptimized = false;
2553 if (strlen($content) - strlen($optimized) > $this->optimizerSizeDiffThreshold) {
2554 $content = $optimized;
2555 $isOptimized = true;
2556 }
2557 $elements = $this->inlineCSS($url, $content, $media, $isOptimized, $ieCompatible, $currentLevel, $seen);
2558 $this->addIEFallback($ieFallbackUrl, $elements);
2559 return $elements;
2560 }
2561 private function inlineCSS(\Kibo\Phast\ValueObjects\URL $url, $content, $media, $optimized, $ieCompatible = true, $currentLevel = 0, $seen = array())
2562 {
2563 $urlMatches = $this->getImportedURLs($content);
2564 $elements = [];
2565 foreach ($urlMatches as $match) {
2566 $matchedUrl = \Kibo\Phast\ValueObjects\URL::fromString($match['url'])->withBase($url);
2567 $replacement = $this->inlineURL($matchedUrl, $media, $ieCompatible, $currentLevel + 1, $seen);
2568 if ($replacement !== null) {
2569 $content = str_replace($match[0], '', $content);
2570 $elements = array_merge($elements, $replacement);
2571 }
2572 }
2573 $elements[] = $this->makeStyle($url, $content, $media, $optimized);
2574 return $elements;
2575 }
2576 private function addIEFallback(\Kibo\Phast\ValueObjects\URL $fallbackUrl = null, array $elements = null)
2577 {
2578 if ($fallbackUrl === null || !$elements) {
2579 return $elements;
2580 }
2581 foreach ($elements as $element) {
2582 $element->setAttribute('data-phast-nested-inlined', '');
2583 }
2584 $element->setAttribute('data-phast-ie-fallback-url', (string) $fallbackUrl);
2585 $element->removeAttribute('data-phast-nested-inlined');
2586 $this->logger()->info('Set {url} as IE fallback URL', ['url' => (string) $fallbackUrl]);
2587 return $elements;
2588 }
2589 private function addIEFallbackScript()
2590 {
2591 $this->logger()->info('Adding IE fallback script');
2592 $this->context->addPhastJavaScript(\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/CSSInlining/ie-fallback.js', "(function(){var a=window.navigator.userAgent;if(a.indexOf(\"MSIE \")===-1&&a.indexOf(\"Trident/\")===-1){return}Array.prototype.forEach.call(document.querySelectorAll(\"style[data-phast-ie-fallback-url]\"),function(b){var c=document.createElement(\"link\");if(b.hasAttribute(\"media\")){c.setAttribute(\"media\",b.getAttribute(\"media\"))}c.setAttribute(\"rel\",\"stylesheet\");c.setAttribute(\"href\",b.getAttribute(\"data-phast-ie-fallback-url\"));b.parentNode.insertBefore(c,b);b.parentNode.removeChild(b)});Array.prototype.forEach.call(document.querySelectorAll(\"style[data-phast-nested-inlined]\"),function(d){d.parentNode.removeChild(d)})})();\n"));
2593 }
2594 private function addInlinedRetrieverScript()
2595 {
2596 $this->logger()->info('Adding inlined retriever script');
2597 $this->context->addPhastJavaScript(\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/CSSInlining/inlined-css-retriever.js', "phast.stylesLoading=0;var resourceLoader=phast.ResourceLoader.instance;phast.forEachSelectedElement(\"style[data-phast-params]\",function(a){var b=a.getAttribute(\"data-phast-params\");var c=phast.ResourceLoader.RequestParams.fromString(b);phast.stylesLoading++;resourceLoader.get(c).then(function(d){a.textContent=d;a.removeAttribute(\"data-phast-params\")}).catch(function(e){console.warn(\"[Phast] Failed to load CSS\",c,e);var f=a.getAttribute(\"data-phast-original-src\");if(!f){console.error(\"[Phast] No data-phast-original-src on <style>!\",a);return}console.info(\"[Phast] Falling back to <link> element for\",f);var g=document.createElement(\"link\");g.href=f;g.media=a.media;g.rel=\"stylesheet\";g.addEventListener(\"load\",function(){if(a.parentNode){a.parentNode.removeChild(a)}});a.parentNode.insertBefore(g,a.nextSibling)}).finally(function(){phast.stylesLoading--;if(phast.stylesLoading===0&&phast.onStylesLoaded){phast.onStylesLoaded()}})});(function(){var h=[];phast.forEachSelectedElement(\"style[data-phast-original-id]\",function(i){var j=i.getAttribute(\"data-phast-original-id\");if(h[j]){return}h[j]=true;console.warn(\"[Phast] The style element with id\",j,\"has been split into multiple style tags due to @import statements and the id attribute has been removed. Normally, this does not cause any issues.\")})})();\n"));
2598 }
2599 private function getImportedURLs($cssContent)
2600 {
2601 preg_match_all(self::CSS_IMPORTS_REGEXP, $cssContent, $matches, PREG_SET_ORDER);
2602 return $matches;
2603 }
2604 private function makeStyle(\Kibo\Phast\ValueObjects\URL $url, $content, $media, $optimized, $stripImports = true)
2605 {
2606 $style = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag('style');
2607 if ($media !== '' && $media !== 'all') {
2608 $style->setAttribute('media', $media);
2609 }
2610 if ($optimized) {
2611 $style->setAttribute('data-phast-original-src', $url->toString());
2612 $style->setAttribute('data-phast-params', $this->makeServiceParams($url, $stripImports));
2613 }
2614 $content = preg_replace('~(</)(style)~i', '$1 $2', $content);
2615 $style->setTextContent($content);
2616 return $style;
2617 }
2618 private function makeLink(\Kibo\Phast\ValueObjects\URL $url, $media)
2619 {
2620 $link = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag('link', ['rel' => 'stylesheet', 'href' => (string) $url]);
2621 if ($media !== '') {
2622 $link->setAttribute('media', $media);
2623 }
2624 return $link;
2625 }
2626 private function makeServiceLink(\Kibo\Phast\ValueObjects\URL $location, $media)
2627 {
2628 $url = $this->makeServiceURL($location);
2629 return $this->makeLink(\Kibo\Phast\ValueObjects\URL::fromString($url), $media);
2630 }
2631 protected function makeServiceParams(\Kibo\Phast\ValueObjects\URL $originalLocation, $stripImports = false)
2632 {
2633 if (isset($this->cacheMarkers[$originalLocation->toString()])) {
2634 $cacheMarker = $this->cacheMarkers[$originalLocation->toString()];
2635 } else {
2636 $cacheMarker = $this->retriever->getCacheSalt($originalLocation);
2637 }
2638 $src = $originalLocation;
2639 if ($this->localRetriever->getCacheSalt($src)) {
2640 $src = $originalLocation->withoutQuery();
2641 }
2642 $params = ['src' => (string) $src, 'cacheMarker' => $cacheMarker];
2643 if ($stripImports) {
2644 $params['strip-imports'] = 1;
2645 }
2646 return \Kibo\Phast\Services\Bundler\ServiceParams::fromArray($params)->sign($this->signature)->replaceByTokenRef($this->tokenRefMaker)->serialize();
2647 }
2648 protected function makeServiceURL(\Kibo\Phast\ValueObjects\URL $originalLocation)
2649 {
2650 $params = ['service' => 'css', 'src' => (string) $originalLocation, 'cacheMarker' => $this->retriever->getCacheSalt($originalLocation)];
2651 return (new \Kibo\Phast\Services\ServiceRequest())->withUrl($this->serviceUrl)->withParams($params)->sign($this->signature)->serialize();
2652 }
2653 }
2654 namespace Kibo\Phast\Filters\HTML\CSSInlining;
2655
2656 class Optimizer
2657 {
2658 private $classNamePattern = '-?[_a-zA-Z]++[_a-zA-Z0-9-]*+';
2659 /**
2660 * @var array
2661 */
2662 private $usedClasses;
2663 /**
2664 * @var Cache
2665 */
2666 private $cache;
2667 public function __construct(\Traversable $elements, \Kibo\Phast\Cache\Cache $cache)
2668 {
2669 $this->usedClasses = $this->getUsedClasses($elements);
2670 $this->cache = $cache;
2671 }
2672 public function optimizeCSS($css)
2673 {
2674 $stylesheet = $this->cache->get(md5($css), function () use($css) {
2675 return $this->parseCSS($css);
2676 });
2677 if ($stylesheet === null) {
2678 return;
2679 }
2680 $output = '';
2681 $selectors = null;
2682 foreach ($stylesheet as $element) {
2683 if (is_array($element)) {
2684 if ($selectors === null) {
2685 $selectors = [];
2686 }
2687 foreach ($element as $i => $class) {
2688 if ($i !== 0 && !isset($this->usedClasses[$class])) {
2689 continue 2;
2690 }
2691 }
2692 $selectors[] = $element[0];
2693 } elseif ($selectors !== null) {
2694 if (isset($selectors[0])) {
2695 $output .= implode(',', $selectors) . $element;
2696 }
2697 $selectors = null;
2698 } else {
2699 $output .= $element;
2700 }
2701 }
2702 $output = $this->removeEmptyMediaQueries($output);
2703 return trim($output);
2704 }
2705 /**
2706 * Parse a stylesheet into an array of segments
2707 *
2708 * Each string segment is preceded by zero or more arrays encoding selectors
2709 * parsed by parseSelector (see below).
2710 *
2711 * @param $css
2712 * @return array|void
2713 */
2714 private function parseCSS($css)
2715 {
2716 $re_simple_selector_chars = "[A-Z0-9_.#*:>+\\~\\s-]";
2717 $re_selector = "(?: {$re_simple_selector_chars} | \\[[a-z]++\\] )++";
2718 $re_rule = "~\n (?<= ^ | [;{}] ) \\s*+\n ( (?: {$re_selector} , )*+ {$re_selector} )\n ( { [^}]*+ } )\n ~xi";
2719 if (preg_match_all($re_rule, $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE) === false) {
2720 // This is an error condition
2721 return;
2722 }
2723 $offset = 0;
2724 $stylesheet = [];
2725 foreach ($matches as $match) {
2726 $selectors = $this->parseSelectors($match[1][0]);
2727 if ($selectors === null) {
2728 continue;
2729 }
2730 if ($match[0][1] > $offset) {
2731 $stylesheet[] = substr($css, $offset, $match[0][1] - $offset);
2732 }
2733 foreach ($selectors as $selector) {
2734 $stylesheet[] = $selector;
2735 }
2736 $stylesheet[] = $match[2][0];
2737 $offset = $match[0][1] + strlen($match[0][0]);
2738 }
2739 if ($offset < strlen($css)) {
2740 $stylesheet[] = substr($css, $offset);
2741 }
2742 return $stylesheet;
2743 }
2744 /**
2745 * Parse the selector part of a CSS rule into an array of selectors.
2746 *
2747 * Each selector will be an array with at offset 0, the string contents of
2748 * the selector. The rest of the array will be the class names (if any) that
2749 * must be present in the document for this selector to match.
2750 *
2751 * Null is returned if none of the selectors use classes, and can therefore
2752 * not be optimized.
2753 *
2754 * @param string $selectors
2755 * @return array|void
2756 */
2757 private function parseSelectors($selectors)
2758 {
2759 $newSelectors = [];
2760 $anyClasses = false;
2761 foreach (explode(',', $selectors) as $selector) {
2762 $classes = [$selector];
2763 if (preg_match_all("~\\.({$this->classNamePattern})~", $selector, $matches)) {
2764 foreach ($matches[1] as $class) {
2765 $classes[] = $class;
2766 $anyClasses = true;
2767 }
2768 }
2769 $newSelectors[] = $classes;
2770 }
2771 if (!$anyClasses) {
2772 return;
2773 }
2774 return $newSelectors;
2775 }
2776 private function getUsedClasses(\Traversable $elements)
2777 {
2778 $classes = [];
2779 /** @var Tag $tag */
2780 foreach ($elements as $tag) {
2781 if (!$tag instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag) {
2782 continue;
2783 }
2784 foreach (preg_split('/\\s+/', $tag->getAttribute('class')) as $cls) {
2785 if ($cls != '' && !isset($classes[$cls]) && preg_match("/^{$this->classNamePattern}\$/", $cls)) {
2786 $classes[$cls] = true;
2787 }
2788 }
2789 }
2790 return $classes;
2791 }
2792 private function removeEmptyMediaQueries($css)
2793 {
2794 return preg_replace('~@media\\s++[A-Z0-9():,\\s-]++\\s*+{}~i', '', $css);
2795 }
2796 }
2797 namespace Kibo\Phast\Filters\HTML\Minify;
2798
2799 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
2800 {
2801 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
2802 {
2803 $inTags = ['pre' => 0, 'textarea' => 0];
2804 foreach ($elements as $element) {
2805 if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag && isset($inTags[$element->getTagName()])) {
2806 $inTags[$element->getTagName()]++;
2807 } elseif ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag && !empty($inTags[$element->getTagName()])) {
2808 $inTags[$element->getTagName()]--;
2809 } elseif ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Junk && !array_sum($inTags)) {
2810 $element->originalString = preg_replace_callback('~\\s++~', function ($match) {
2811 return strpos($match[0], "\n") === false ? ' ' : "\n";
2812 }, $element->originalString);
2813 }
2814 (yield $element);
2815 }
2816 }
2817 }
2818 namespace Kibo\Phast\Filters\HTML\DelayedIFrameLoading;
2819
2820 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2821 {
2822 use \Kibo\Phast\Logging\LoggingTrait;
2823 protected $addScript = false;
2824 private $ignoredUrlPattern = '~
2825 ^about: |
2826 ^data:
2827 ~ix';
2828 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2829 {
2830 return $tag->getTagName() == 'iframe' && $tag->hasAttribute('src');
2831 }
2832 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $iframe)
2833 {
2834 $src = trim($iframe->getAttribute('src'));
2835 if (preg_match($this->ignoredUrlPattern, $src)) {
2836 (yield $iframe);
2837 return;
2838 }
2839 $this->logger()->info('Delaying iframe {src}', ['src' => $src]);
2840 $iframe->setAttribute('data-phast-src', $src);
2841 $iframe->setAttribute('src', 'about:blank');
2842 $this->addScript = true;
2843 (yield $iframe);
2844 }
2845 protected function afterLoop()
2846 {
2847 if ($this->addScript) {
2848 $this->context->addPhastJavaScript(\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/DelayedIFrameLoading/iframe-loader.js', "window.addEventListener(\"load\",function(){window.setTimeout(loadIframes,30)});function loadIframes(){phast.forEachSelectedElement(\"iframe[data-phast-src]\",function(a){var b=a.getAttribute(\"data-phast-src\");a.removeAttribute(\"data-phast-src\");if(a.getAttribute(\"src\")===\"about:blank\"){a.setAttribute(\"src\",b)}})}\n"));
2849 }
2850 }
2851 }
2852 namespace Kibo\Phast\Filters\HTML\Diagnostics;
2853
2854 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
2855 {
2856 private $serviceUrl;
2857 public function __construct($serviceUrl)
2858 {
2859 $this->serviceUrl = $serviceUrl;
2860 }
2861 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
2862 {
2863 $url = (new \Kibo\Phast\Services\ServiceRequest())->withUrl(\Kibo\Phast\ValueObjects\URL::fromString($this->serviceUrl))->serialize();
2864 $script = \Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/Diagnostics/diagnostics.js', "window.addEventListener(\"load\",function(){var a=phast.config.diagnostics.serviceUrl;var b=new XMLHttpRequest;b.open(\"GET\",a);b.responseType=\"json\";b.onload=function(){var c=b.response;var d={};var e=[];c.forEach(function(g){var h=g.context.requestId;if(!d[h]){d[h]={title:g.context.service,timestamp:g.context.timestamp,errorsCnt:0,warningsCnt:0,longestPrefixLength:0,entries:[]};e.push(d[h])}if(g.level>8){d[h].errorsCnt++}else if(g.level===8){d[h].warningsCnt++}var i=(g.context.timestamp-d[h].timestamp).toFixed(3);if(g.context.class){i+=\" \"+g.context.class}if(g.context.class&&g.context.method){i+=\"::\"}if(g.context.method){i+=g.context.method+\"()\"}if(g.context.line){i+=\" Line: \"+g.context.line}var j=g.message.replace(/\\{([a-z0-9_.]*)\\}/gi,function(l,m){return g.context[m]});var k;if(g.level>8){k=console.error}else if(g.level===8){k=console.warn}else if(g.level>1){k=console.info}else{k=console.log}d[h].entries.push({prefix:i,message:j,cb:k});if(i.length>d[h].longestPrefixLength){d[h].longestPrefixLength=i.length}});if(e.length===0){return}e.sort(function(n,o){return n.timestamp<o.timestamp?-1:1});var f=e[0].timestamp;console.group(\"Phast diagnostics log\");e.forEach(function(p){var q=(p.timestamp-f).toFixed(3);var r=q+\" - \"+p.title+\" (entries: \"+p.entries.length;if(p.errorsCnt>0){r+=\", errors: \"+p.errorsCnt}if(p.warningsCnt>0){r+=\", warnings: \"+p.warningsCnt}r+=\")\";console.groupCollapsed(r);p.entries.forEach(function(s){var t=s.prefix;var u=p.longestPrefixLength-t.length;for(var v=0;v<u;v++){t+=\" \"}s.cb(t+\" \"+s.message)});console.groupEnd()});console.groupEnd()};b.send()});\n");
2865 $script->setConfig('diagnostics', ['serviceUrl' => $url]);
2866 $context->addPhastJavaScript($script);
2867 foreach ($elements as $element) {
2868 (yield $element);
2869 }
2870 }
2871 }
2872 namespace Kibo\Phast\Filters\HTML;
2873
2874 interface HTMLFilterFactory
2875 {
2876 /**
2877 * @param array $config
2878 * @return HTMLStreamFilter
2879 */
2880 public function make(array $config);
2881 }
2882 namespace Kibo\Phast\Filters\Image;
2883
2884 interface ImageFilter
2885 {
2886 /**
2887 * @param array $request
2888 * @return string
2889 */
2890 public function getCacheSalt(array $request);
2891 /**
2892 * @param Image $image
2893 * @param array $request
2894 * @return Image
2895 */
2896 public function transformImage(\Kibo\Phast\Filters\Image\Image $image, array $request);
2897 }
2898 namespace Kibo\Phast\Filters\Image;
2899
2900 interface ImageFilterFactory
2901 {
2902 /**
2903 * @param array $config
2904 * @return ImageFilter
2905 */
2906 public function make(array $config);
2907 }
2908 namespace Kibo\Phast\Filters\Image\Composite;
2909
2910 class Factory
2911 {
2912 /**
2913 * @var array
2914 */
2915 private $config;
2916 /**
2917 * CompositeImageFilterFactory constructor.
2918 *
2919 * @param array $config
2920 */
2921 public function __construct(array $config)
2922 {
2923 $this->config = $config;
2924 }
2925 public function make()
2926 {
2927 $imageFactoryClass = $this->config['images']['factory'];
2928 if (!class_exists($imageFactoryClass)) {
2929 throw new \Kibo\Phast\Exceptions\LogicException("No such class: {$imageFactoryClass}");
2930 }
2931 $composite = new \Kibo\Phast\Filters\Image\Composite\Filter(new $imageFactoryClass($this->config), (new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManagerFactory())->make($this->config));
2932 foreach ($this->config['images']['filters'] as $class => $config) {
2933 if ($config === null) {
2934 continue;
2935 }
2936 $package = \Kibo\Phast\Environment\Package::fromPackageClass($class);
2937 $filter = $package->getFactory()->make($this->config);
2938 $composite->addImageFilter($filter);
2939 }
2940 if ($this->config['images']['enable-cache']) {
2941 return new \Kibo\Phast\Filters\Service\CachingServiceFilter(new \Kibo\Phast\Cache\File\Cache($this->config['cache'], 'images-1'), $composite, new \Kibo\Phast\Retrievers\LocalRetriever($this->config['retrieverMap']));
2942 }
2943 return $composite;
2944 }
2945 }
2946 namespace Kibo\Phast\Filters\Image;
2947
2948 class ImageFactory
2949 {
2950 private $config;
2951 public function __construct(array $config)
2952 {
2953 $this->config = $config;
2954 }
2955 /**
2956 * @param URL $url
2957 * @return Image
2958 */
2959 public function getForURL(\Kibo\Phast\ValueObjects\URL $url)
2960 {
2961 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
2962 $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($this->config['retrieverMap']));
2963 $retriever->addRetriever((new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($this->config));
2964 return new \Kibo\Phast\Filters\Image\ImageImplementations\DefaultImage($url, $retriever);
2965 }
2966 /**
2967 * @param Resource $resource
2968 * @return Image
2969 */
2970 public function getForResource(\Kibo\Phast\ValueObjects\Resource $resource)
2971 {
2972 return $this->getForURL($resource->getUrl());
2973 }
2974 }
2975 namespace Kibo\Phast\Filters\Image\CommonDiagnostics;
2976
2977 class DiagnosticsRetriever implements \Kibo\Phast\Retrievers\Retriever
2978 {
2979 /**
2980 * @var string
2981 */
2982 private $file;
2983 /**
2984 * DiagnosticsRetriever constructor.
2985 * @param string $file
2986 */
2987 public function __construct($file)
2988 {
2989 $this->file = $file;
2990 }
2991 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
2992 {
2993 return file_get_contents($this->file);
2994 }
2995 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
2996 {
2997 return '';
2998 }
2999 }
3000 namespace Kibo\Phast\Filters\Image;
3001
3002 interface Image
3003 {
3004 const TYPE_JPEG = 'image/jpeg';
3005 const TYPE_PNG = 'image/png';
3006 const TYPE_WEBP = 'image/webp';
3007 /**
3008 * @return integer
3009 */
3010 public function getWidth();
3011 /**
3012 * @return integer
3013 */
3014 public function getHeight();
3015 /**
3016 * @return string
3017 */
3018 public function getType();
3019 /**
3020 * @return string
3021 */
3022 public function getAsString();
3023 /**
3024 * @return integer
3025 */
3026 public function getSizeAsString();
3027 /**
3028 * @param integer $width
3029 * @param integer $height
3030 * @return Image
3031 */
3032 public function resize($width, $height);
3033 /**
3034 * @param integer $compression
3035 * @return Image
3036 */
3037 public function compress($compression);
3038 /**
3039 * @param string $type - One of Image::TYPE_JPEG, Image::TYPE_PNG or Image::TYPE_WEBP
3040 * @return Image
3041 */
3042 public function encodeTo($type);
3043 }
3044 namespace Kibo\Phast\Filters\Image\ImageAPIClient;
3045
3046 class Diagnostics implements \Kibo\Phast\Diagnostics\Diagnostics
3047 {
3048 public function diagnose(array $config)
3049 {
3050 $package = \Kibo\Phast\Environment\Package::fromPackageClass(get_class($this));
3051 /** @var ImageFilter $filter */
3052 $filter = $package->getFactory()->make($config);
3053 $imageData = @"\211PNG\r\n\32\n\0\0\0\rIHDR\0\0\1h\0\0\1h\10\2\0\0\0\365\207\366\202\0\0\0\31tEXtSoftware\0Adobe ImageReadyq\311e<\0\0\3\$iTXtXML:com.adobe.xmp\0\0\0\0\0<?xpacket begin=\"\357\273\277\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?> <x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 \"> <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"> <rdf:Description rdf:about=\"\" xmlns:xmp=\"http://ns.adobe.com/xap/1.0/\" xmlns:xmpMM=\"http://ns.adobe.com/xap/1.0/mm/\" xmlns:stRef=\"http://ns.adobe.com/xap/1.0/sType/ResourceRef#\" xmp:CreatorTool=\"Adobe Photoshop CS6 (Macintosh)\" xmpMM:InstanceID=\"xmp.iid:0E913E46F5A911E5B20EF2CD3E8D574E\" xmpMM:DocumentID=\"xmp.did:0E913E47F5A911E5B20EF2CD3E8D574E\"> <xmpMM:DerivedFrom stRef:instanceID=\"xmp.iid:CCC4537FF57711E5B20EF2CD3E8D574E\" stRef:documentID=\"xmp.did:CCC45380F57711E5B20EF2CD3E8D574E\"/> </rdf:Description> </rdf:RDF> </x:xmpmeta> <?xpacket end=\"r\"?>\10\f\316\"\0\0!^IDATx\332\354\235{lT\327\235\307g\346\316\353\216I\370\3\\\330\215\f!\17\r\201\$\305\20\300\255\275y\264\252]\247\213\332\30\323f\225\332\33\233\0006*\304\260\305P\333\332\222\312\266\10T@q\4\256\223\340\10oTR\273\356\37\220&f\265\$\$8\261\351\202+S\10\243<xX\221\332uF-\221=\343y\334\231=\327\3.\1\0033\366=\257;\337\217PB\22\342\271s\317\275\237\363\373\235s~\347X//^d\1\0\200T\260\341\26\0\0 \16\0\0\304\1\0\2008\0\0\20\7\0\0\342\0\0\0\210\3\0\0q\0\0 \16\0\0\304\1\0\2008\0\0\0\342\0\0@\34\0\0^\330q\v@2(Ks\334?Y\307\370CG^j\322z{p\363\345\26GF\333\353\214/n\270\344\31\223\335nGY\271\363[\337\26\347z\202\365\277\210\371|\311\374I\353\324\251\212\327\313\370\362\310\207\342\25\225^\34\354\237\33\363YC\255\\+\2205\366\355M\322\32\0 U\2015,\361`0P\275\tY\0\2300\30\34\2055\0\2008`\rX\3@\34\260\206\201h\3\3\260\0060\4\214q\244\2235*V\307\7\7\321.\0\21\7\254\1k\0\210\3\326\2005\0\304\1kp'\322}|x\371S\260\6\2008`\215\24\254\21\334P\205F\1\20\7\254\1k\0\376`V\305\234\326\30i;\20n\332\203F\1\20\207\330\367\261x\2058\326\10\356\333\33i\335\217F\1HU\204FY\232\243\256[\17k\0D\34 \5kx\266\357\260\252*\367+\211\7\203#\257\265\302\32\0\342\2005R\260\6\226\223\3\244*\260\6\254\1 \16X\3\326\0\20\7\254!\2105\264\201\201\341\325\317\301\32\2001\30\343\220\333\32(B\1\2108`\rX\3@\34\260\0065\242}}\260\6@\252\2k\244\0\212P\0\"\16X\3\326\0\20\7\254\1k\0\244*@\34k\240\10\5 \342\2005`\r\0q\300\32\260\6@\252\2\4\261F<\30\f6\324G\217t\241E\0\304\1k\$k\r\24\241\0\244*\222\334\21\257\27\326\0\0\342H\1kf\246\332\270\215\2735b~?\254\1\220\252Hc\rOs\213\222\225\305\3672P\204\2\20q\300\32\260\6\2008`\rX\3\0\210C\34kD\272\217\303\32@\26\322}\214C\34k\240\10\5 \342\2005`\r\0q\300\32\324\10uv\302\32\0\251\n\254\221\2(B\1\2108`\rX\3@\34\260\6\254\1\0R\25A\254\201\"\24\200\210\3\326\2005\0\304\1k\300\32\0@\34\342XC_N\16k\0\263`\3761\16Q\254\201\345\344\0\21\7\254\1k\0\210\303\264\360\267\206\317\7k\0\244*2\241\356\332\315\327\32(B\1\2108\344\263\206#7\17\326\0\0\342\2005\0@\252bRk\214\264\35\0107\355\301\263\5 \16X#YP\204\2\220\252\300\32\260\6\0\246\2168\370ZC?\253\261iO\264\243\35\217\24\2008`\215d\255\201\345\344\0\251\212d8\312\312a\r\0 \216\324\254\241V\256\2055\0@\252\"\2075P\204\2\20q\310\207\2624\7\326\0\0\342H\315\32\236\355;`\r\0 \216\324\254aUU.\237\36\355\353\2035@\232#\337\30\20753\323]\275\231\2275P\204\2\200|\21\7\337\215y`\r\0\244\24\207g'\267-6\302G\272`\r\0\344KU\364\215y\274^.\37\35\352\354\fmk\304\343\2\200d\21\207kK\r\307\345\241\316\302B\222%\341q\1@&q8\312\312]EE\34/\300\252\252\236\346\26\270\3\0i\304\301w\241\327?.#+K\255G\266\2\200\f\342\260y\275\34\27z]\207=;\333]\337\200\207\6\0\241\305AR\3\265q\33\257%\33\343\342\314/ y\23\236\33\220\346\10=\253r\307\233o\txU\$o\212\376y\364H\27\236\36\200\210\3\244\342\216\332:ei\16\356\3\2008@*9\224\252\352\313\3361\311\2 \16\220\22JV\226\247\271\5\367\1@\34 ew\250\273v\343>\0\210\3\244\206#7\317\265\245\6\367\1@\34 5\\EE\230\240\5\20\7H\31\367\263e\230d\1\20\7H\r\275\222e\373\16\33\247\312]\0 \16\211\335\241/r\305\4-\2008@J\240\n\16@\34`\"\330\263\2631A\v \16\2202\216\334<L\262\0\210C2\342\301`\250\263\223\374\225\3435\250\225k\355\371\5x\266\0\304!\2155\2\325\233B\333\32\203\r\365|\257\4Up\0\342\220\311\32\211\363\237\243G\272F\332\16p\274\30}\222e\353\v\230d\1\20\207\320h\3\3C\305E\327\236\32\37n\332\23\351>\316\363\316N\233\206mJ\1\304!\2645\306=\2231\270\241\212\374'\216\27\246de\271kj\361\220\1\210C8n}\222\253\376\237\270\16\224\242\n\16@\34\302A\222\221\300\232U\2678\377\231\374\247@\365&\276\356@\25\34\2008\4\"\3113\31\265\336\236\221\327Z\371^\252Z\271\26\223,\0\342\340Op\337\336\221\272d\207\17\"\255\373\303\274\367\26F\25\34\2008\370[\203\270 \245\377\205X&\332\327\307\361\232Q\5\7 \16n\304\203\301\341u?I\325\32WtSW\303}\222\305\263\23\225,\0\342`n\215\261%^\23\371\337\7\7\2035[\370\16\224*^/\252\340\0\304\301\16}\261\306\$\254\221 \346\363q_\215\216*8\0q0\264F\305\352IZ#A\364HWp\337^\276_G\257\202+^\201\207\17@\0344\255\341\363\335b\211\327\4\210\264\356\347\273\32]w\307\272\365\230\240\5\20\7-\310\33>\\\362\214\201\326H\300}5:\252\340\0\304A\321\32\311,\361\232\30\$\212\211\371\375<o=\252\340\0\304A\3z\326\260\$&Y\266\376\234\363\$\v\252\340\0\304!\35ZoO\260i\17\337kp\344\346\271\353\33\360 \2\210C&\242\35\355\241\316N\276\327\340\314/\300\4-\2008\$#\264\255\221\377\$\v\252\340\0\304!\35#\215\r|'Y,\243Upp\7\2008dB\204\325\350VUuWo\306\$\v\2008d\"\346\363\5\2527\361\275\6T\301\1\210C>\364I\26\336\253\321Q\5\7 \16\371\20a5\272#7\317\271n=\332\2@\0342\241\257F\367\371\370^\203\273\244\24Up\0\342\220\214\300\306*\356\223,\250\202\3\20\207d\304\7\7G\266\277\310}\222\305\263}\7&Y\0\304!\23\"\254F\327\335\201*8\0q\310E\264\243\235\357\1\264\26T\301\1\210CF\270\37@kA\25\34\2008dD\204\325\350\250\202\3\20\207d\350\207H\362>\200\326\222\330\2464\277\0\315\1 \16\251\334\301{5\272\356\216\332:L\320\2\210C&DX\215\216*8\0q\310\207\10\7\320\352Up\315-h\v\0q\310\4\367\3h\23\356@\25\34\2008\$\203\373\1\264\26T\301\1\210C:DX\215n\31\255\202\303\4-\2008dB\37(\345}\0\255\356\216g\3130\311\2 \16\231\210\36\351\342\276\32\35Up\0\342\220\17\21V\243\243\n\16@\34\362\301\375\0ZKb\222\245\276\21m\1 \16\231\340~\0-\301\236\235\215*8\0q\310\204\10\7\320ZP\5\7 \16\351\320z{F^k\345~\31\250\202\3\20\207dDZ\367s?\200\326\202*8\0qHGh[#\367\325\350VUU\267\276\200I\26\0q\310\204\10\253\321m\323\246\241\n\16@\0342!\302\1\264\26T\301\1\210C:b>\237\10\253\321\35\271y\256-5h\16\0qHC\364H\27\367-\10\256\242\"L\320\2\210C&D8\200\326\202*8\0qH\207\10\7\320&\252\340l^/\232\3@\34\322\20\330X\305}5\272>A\333\270\r\23\264\0\342\220\6AV\243\243\n\16@\34\222!\302\1\264\226\321*8L\320\2\210C&\242\35\355\"\254Fw\344\346a\222\5@\0342\21\332\326(\302\$\v\252\340\0\304!\31\"\34@kA\25\34\2008\344B\220\3hQ\5\7 \16\t\335!\300\1\264\211*8\270\3@\34\322 \302\1\264\226\321\tZwM-\232\3@\34\322 \302\1\264\26T\301\1\210C:F\352j\271\257F\267\240\n\16@\34\322\21\330X%\304\$K\345ZL\262\0\210C\32\49\200\226\200*8\0q\310\204 \253\321Q\5\7 \16\311\210v\264s?\200\3262:\311\342\331\211J\26\0q\310\203\10\7\320\352\356\360zQ\5\7R\302\216[\300\227\340\206*\333\357~O\272}\276\227\341\310\315\213\226\225GZ\367\337\354\17\304.^`_\255G>\24O\210\364\342\20\241\312\323\224\4*V;W\256\342\37|\316\230y\253w\330\347\vm\303\276\36\340\n\326\313\213\27\341.\0\0R\353fp\v\0\0\20\7\0\0\342\0\0@\34\0\0\210\3\0\0q\0\0\0\304\1\0\2008\0\0\20\7\0\0\342\0\0@\34\0\0000>\334\252c\257=U\314z\327]\327UX)s\346X=\236\361U7k\226UU\223\377\240x0\30\273t\351\306\37=sf\354\367\332'\37[\276\374R\377M\337\251\370\340 \36\v\211\260ff*\331\vo\361\7b\27/\304\4\330\344\325l\267\335\360\"\267+F\270\363N\345\276\373\365\17\230\222\241\314\276{b\357<G\22\373\t'\344\242\235:\31\277|Y\353\355\301\343\302-0\366zm\263\357&\265fL\261\317\237?\261gi\254\v\321.^\210\17\r\243Y9\210\343\332\206\264\315\370\232mz\246\305\343\341\276\251\4mb~?y\362\264\363\347I\204\22;\335\217~\214\252)\224o\346*\367\336Kz\35\205\362\256\250\332\300@\354\322E\355\263\317\264\23'\340\21*\342P\226\346\270\2537\233^\20)y\$\372\347\323x\340\f\224\205#\347\33\312\334\271\34#S\22l\222H3\372\316Q\264\251a\342 i\210\247\276\1wm\33408z\352d\264\277?z\370\20FIR\202\364F\216e\313\224\7\346\211\326!%\3324\322\335\35\355hG3A\34\324\211\366\365Ez>\204An\33_8KJ\355\213\36\261M\233&E\257\20y\353\255\250\0\247\360A\34i\21\203\340i\273\21GY\271\363[\337V\$<\317\205\$\247\221c\307\302\257\276\214.\1\342\240\2373\17\f\204\17\37\272\305>\300\351\362\250ef:W\256r\26\26\3122\263v\v\"\335\307C\315\373\322yt\34\342@g\305\"+qUT:r\363\314\326%\370|\241\266\3\351\31QB\34\254\363\227PG{X\2003\334\240\f\3\3651\362RS\272M\301(?\273\353\237Sx\16\356\275\317\361\255o\343\375\237\270\247\35\16\373\327\277\356(Z\36w:c\3523wb\342Z_\245n\374\17\345\236{M.\307\351\323\235O>i\2337/v\341|\334\357\2078 \16j/\225\307\343X\274\230\350#\26\n\305\316\2365\337\27t\224\225\253u\377\351X\264\210\2102]^\244Y\263\34\205OZg\376\223v\374}\210\3\342\240\254\217\334<\373\243\217ig\317\230\246\247\"\271\211g\367\36\347w\voVjd\362p\362\201\7\354\337-\214\377\375\357\261O?\2058 \16\272\201.\351\251,w\334\241\235\350\225\375\2738\327\255W7\377\3146sfZ7\350\324\251\372;2m\272\271C\17\224\325\v\320S\251\252\273\2444\243\355ukf\246\274\201\6\271~\362-L0\325j\10\256\242\242\214\337\375^Y\232\3q\0\312\261\37y\367\16\374\227\214\217\232\275xEF\313+2.\350\242\333\240YY\236\355;\34e\345HU\220\252P\16=<\36\307\23O\304\206\206\$\0321u\3277\270K\377=}\6ASkP\207\303\261x\261m\336\274h\327\333\2108\0\335\264\305S\275Y\212n\212\$V\$=q^\263!\23\30\27Gn\36I[\344\315C!\16iP+\327\n\356\16\222R\351\211\25\322\223\344\323\226\346\0263\ry@\34\342\272C\330\347\214\\\30\311\336\305\257j\25\316\35\333w\230\306\35\20\207\270\210\371\234\221P\210\\\30fO&\230\207\232\305\35v\271.7\346\367\307\277\370b\354\37\343\201\200v\376\374\355\355\230\330\3340\361{y\366=\325\247i\2537\7*V\213S\27\247/\t\255\\\v\5L\322\35\201\352M\262\327\266\210+\216\304\16n\211\375\307\r\337\250:\261gjbwu\373\374\371\302\332\204\304\267\256\347\253F\352ja\r\270\3\342H\n\252\5\313DCc&\n]U\211\362\315\\\373\303\17+s\37\20*{w\346\27D\337{\217{\3556\254\1wH\234\252PL\202FU\22I\364\363\243\273`\212\263\253\235kM\5_q\330\363\vD\263F\"i\275r~\305\325cqn\214+\307v\341\27m\2Hvw@\34\343eI\275=\211\346\264\27\257p}\377\7\334\2379\222\260\220\16\237\327\36bD\243jm\235@\331\353\251\223\311j\364\253\214|\21e\311\22\373\203\17\361\335E\375Zw\2106\206\225\302\305\v\273\221O\240\256V\220\275\225\310\267v\225\224\362\325\7\351`\207\n9,\265\322\213PZ^\341\373\232\321\330\374\231t\t\216\334\\\373\302E\334\rBl8\\\362\214t\342\20w\311y\344\350QAj\223\311eD~\337\251\375\355o\372s\306im\265\325\343\211E\243\214\367\376\261ffzv\375\212W\276F\\\31~\373\355\340O7F\3368\250\361@\300\310\37~\366l\264\353\355\310\233\207c_~\251\334s\17\307M\0l\323\247[g\317\216\36=*\2278\260\216#\351~\257\243}\250\270H\343\267?\255\223y\225\220\273\246\226\313Y'D\31\301}{I\204\25\332\326H5\214'?\234\$\200\344\203\2\333_\324\6\6\270\265l~\201t\265p\20Gj\317\31\211*#\335\307\371\4\207^\257\215a\272\344\\\267\236\375^\241\372\236\254\235\235\344Mf<\240Cz\205\341\345O\21[\221\v\340\322\270\356g\313lR\255\337\2078R&\270\241\212\227;\354\254*\312\364\263>KJ\331\217e\220\230\216D\31\274ZV\217>\212\213\302<F\326\254\252\2526n\2038\340\16*8\226,e3\264\241n}\201q\240A\222\205\300\232U\334\347\27\310\5\214\324\325\6\352j\331\207\36\372b\277-5\20\207\371\335\301>+f3\263\343\256\251e9 Jn\343\360\352\347\204:\2375z\244\213\313x\226\253\250H\226J\26\210c\342\214l\321|\331\312\350<%\273\241\r\22\270\5*V\vx\$Zb<\213}\332\342\256\336\fq\230\34\255\267\207}\302Bu\10MOR\326\255g\366]\310kI\0027\221\227?\221\264%\270o/\22\26\210\303`B\315\373\30\2425c\n\305P\371\371*f\v\242\310\v)H\361\336mb\242\326\375\214\335\341,,\24\206\5\342\230\24\$\306\216\3661]\224e\237?\237^\22\304l\37@\362*Jt\n7cw\20w\273**!\16\223\23\351\371\320\34_\304\305j\376\225d(\22Y\203\213;\364\223\272\304\336\314\25\342\230,\332\7\335&\370\26\216\262r6S6\221\356\343Rd(\343\272\203\345X\251\213\371:\32\210\203u\266\22\223\377\0G\327\17\304B\262\3\3\301\rU\362\336%\242<f\231)\361\270\310A\7\304a\0\327\356f(\2455\266\3240X\270\21\17\6\2035[do\353`]\r\263\365;\"\7\35\20\207\21\257\204\241\205\233\214\261ff:\v\vY\274rM{\4\\\257\221r[\17\0162[\277#r\320\1q\30\21\201'\261a\262\2608W\256b0\5\33>\322%\324\332\320I5wo\317H\333\2014\17: \16\331\236\332\213\27\f\26\7\375p#\346\367\207~\265\333L\255\20n\332\303fA:\t:\304\\\204\16q\30\321\272s\346\260\v\225\207\206\r\374i\216\262r\6\341\306\310\256\2352\356\216w\233/\365R\23\243\220\360\351\247!\16s\302r\377(\355\324I##a\372\223)\321\276>A\266\2004<a\tuv2\370 Gn\236\200\347\316B\34F\334\304Y\263\330=\257}\247\214\372Q\366\374\2\6\223)#;i\326v\17\277\3722\233\352{\307\323\377\6q\230.OY\232\303\254\276\203\344\325\6\306\374\316\345\305\324_\255#]&\230I\271i\332888\362Z+\vq<\376\4\304a:q,Y\302\354\263\"'z\rkx\257\327\236\235M\367\275\n\6M6&:N\213\264\356g\260\374O\311\312\22m\210\24\342\230t\277\375\344\367\230}\226\201\203\5\f\26\10D\336\317|c\2427\22\372\355\33,\202\216e\313 \16\363\300f\230`,O10\354g\340\2730\253\305\16\351\20t\330\27=\2q\230\7\226\353sB\306\275\207\$\356\245\355\273H\367q\23\217n\\\257\310?\274I\375E\2356M\250l\5\342\230D\247\275n=\263\343\335\264\201\1\3\363\24\6qo\370\340\301\364y\22\"\7\303`zE\250l\5\342\230x\247\355*^\301.\334\370u\263Dq/\321\234\274\347\260O\200\370\340`\324\320\3655\342g+\20\307D\260ff\272\2537\263\234\20550\334`\221\247\274\373N\272=\22\f\",\322j\342l)\10qL\304\32\236\346\26fg#\352\325\350\365\27702V\242?LB\367t{*H\204\305\240\334^\234bY\210Chk\350IJG\273\261\243\214\264W\23E\373\372\322a\26\226K\234e\360!\210C>H\220\317\330\32\344%\f7\3551V|\264\257\3374\233\260\246\334X\364Kr\224\271s!\16\311p\256[\357\331\276\203\2455\364\215\366\352\f>bCy\354q\352\357\317\341C\351\371\204\220\300\220v\266bUUA&e!\216\244\2\215\214\266\327\335%\245\314FC-W7\3323<\346\267/X@[v\351\231\247\\\221\346\37\377H\375\215\2357O\204oj\207\27n\325H^\257\253\242\222\345\221\210c\326\10To\242\261\200Jy`\236\354o\216\320\342x\347\250\253\250\210\356\33\373\360\303\21\210CX\354\371\5\216\302B\366\312\30\263\6\245u\20\264S-\215\376r\6\221!\255F\232\217jdj\233.\304\336\34\20\307\365!\206~\240\331\223\337cy\\;3k0\230\3143\345\236=\251\271\343\3349\252e\307\212\30K9 \216+o\224\375\321GI\30\317r\354s\334\1\202`\315\26z%\36\312\302Et\257?m\212Sn\245\316?\237\246\275_\201\2624\207\373\302\334t\24\207>%\231\275\220\4\27\266\0313\270\313b\f\375\210\263\306\6\252#\213\266\31_\243\373\316\2349\3q0(\355\263\222'\26\342\270U\326`\304\17I\34\357n\235\222\241\314\276[\234H\357\272\364\$\324\321n\354z\215\361\357\306\254\331t#\216O>\2068\364d\255\276\201n\304q\337\375Q\244*7\303-\366\331\231\6\246'#\333_d\23y\322\216\255b\247\373!\216D\233R\275\325\264#G\244*B\303,\320\30K\214i\235\30\3068\22\2\275t\221\2568\4\230X\2018\370\300`D\343\372\304x\352T\312o\313%4\353\225[\361\327\377\243\234r\316\342\376\35\261r\224y\34\353\363\r\225<\23\334P\305x\205%\365)\25\243\217\230\223\270\211)/fa\271\202\31\21\207\20QF\370\340A^\23i\326)\31tS\25C\217\230\223;\t\275|\231z\207\357\365\362M\f!\16\372\217Q0\30y\377\275p\333\1\276-\235\230T\222\267\233\225)\342\240\3377\330f\337\rq\230\226h__\244\347\303H\353~\334\2124\354-DH( \16\371|\21=|H\250:Q\332#j\6\236Mi\2b\227.Q]1d\275\353.\210\303TD\272\217G\373\373\265\17\272E\253.\247\335\1\246s5=\207n`\306L\210\303T8r\363\364\232\332\312\2651\277_;\367\21\221\210h\241\7`\200v\361\202\200k\224!\16\31\372\204i\323lW%\242\371|\221\23\275\372\351\33\234\fB{\365\27\312\333\256\217\277\314>\307\4q0yo\275^\362\313]R\312k\270\224\366\352/\220v\375\"n\1SOgg\253\225k\3578\366\276kK\215\315\324\241,\2008\200\321\375\277\252\272\212\212\246\264\275\256\356\332\r}\0\210\3\244\206#7\17\3720%\246\337a\0\342\20E\37\356\372\6kf\246\244_!\366\5\246\215\276\312\227_B\34\200\5\316\374\202)\35\235\216\262r)\305A\271\36\24@\34\340\246XUU\255\\\233\321\366:2\27\351\233\222\367\312N\210#\355P\274\336\214\226W\354\305+dz\214\4\330\223J\260\0332\323\344_\20m,f\350\341\251\336\354\246\274u\245\221\217\321\364L\264\32R\25 \4\316\374\2\222\266\310;b\nL\214\270+G\3u\265\206\234\3563v\n\21I;\23\1\244}\376|\213\220\333\235\217\233\266x\232[\250\36\266\2\200\251\304a\0247\332'4\226\21\\=`E\271\347\36\333\254\331\202\34\260r\275;\262\2622Z^\241w\274\33\220\221\330_\377\2qp#>8\250k\345\252Yt\217<\366\270}\301\2\373\242Gx\35\19.\372\220\307\366\35B\273\303\343\301\313\374\225\367j4\252\245\370\350~\3769\304!\222G:\332\311/\313\325Cd\35\217?!H\30B\334\341\256\336\34\250X=\261\372Z\332\273`\212\31\254\1z`p\364&\241\240\317\27n\3323\274\374\251\341u?\211t\37\217\7\203\"\344,\236\346\226\211\215\225\"\3151Y\10\306`?d\210cR\220W.\270\241j\250\270(\324\331\311]\37\304\35\356\232ZA\273 ,Zc\30\202q\357\t \216d\263\230\320\266F\21\364\241\35706\241e\351\264/\333Fy\27u\211H\207\31t\210#e}\f\257~.\332\327\307\3612\334\317\226M\240{\247}\322\232\351\27Y\247\20nd/\244\33n\01007\17q\244L\314\347\v\254Y\25\334\267\227W\350\241\227\264\324\375\247p]\220\331\27Y\303\241\20\207\1DZ\367\7\2527\305\374~>}\232\327\233j1K\364\314\31\272\2274g\16\236\n6\16\245\335\224\20\7]\264\336\236\341\322\37k\3\3|\22\226\225\317\211\325\315N\237\216G\"\1\365E\34\303C\20\207\334\304\7\7\3\25\253\271\270\3036mZJ\243\244\264\217h\304R\16f\16\215a\214\3034\356\3402\336\341\372\341\217R\270N\3723\377ceAim\215\314L\332\313\216E84\17\3420\306\35\301\206z.AG\362#\35,NB\306R\16\372S*\244\213\22\341|/\210\303\30\242G\272B\235\235\354?\327\371\235\374\24z*\312)\225\375\301\207\360\$(\v\27\321\315S(O\253C\34\254\t\277\3722\373I\26{vv\362\313\215b\227.\322}\230(\237k-\5\264GFE\230R\2018\fNXB\277}\203\303\223\372\257\313\222\2158>\373\214v\352\204l\205\366>/\202\34\274\0q\30I\244u?\373QRG\3167\222\2158\350\217\306\247\371\370(\203\235bc\247\373!\0163\272\343\375\367Xwqs\347&\33\345\32\261\243\332m\336\234\364\36\346\260/X@\327\32~\277 {\301A\34F\213\343\320!\306\237hU\325\344\23\4\352\343\243\251\214\271\230P\34\213\36\241\233\247\234\373H\220o\nq\30\335\264\275=\354\263\25\333C\17'{y\37\235\245\376\362\$=\346b2\224\2459\264WpD\373\373\5\371\262\20\7\205x\222\371\204\231r\337\375\311>y\372\23\355\213I~\314\305d8\226Q7\246\366A7\304a\336\240\343\342\5\326\255\230\364yH\332\261w\251G\34\351\232\255\320\316S\304\31\340\2008\250\20\37\32f\335\212I\237\207\24\37\34d\260\233C\32f+\366\374\2\352y\312\311\377\25\347\373B\34\24z\6\336[\327\337\232\310\211^\332\37\341L?q8\n\vi\4\2034\23\342\340\32q\360\336\272\3766\317\37\375IY%++\255\26t\220\324\314\221\233G\367\241\n\6\23\373\357C\34\200S@\344\3631\330\7\200A\17,\16\316\225\253\250\353\236\362\256\10\20\7h\2279\31\220\255\274\373\16uq\344\346\245\317\362s\307c\217Qo\262\356n\241\2762\304\221\2160\310V\10\256\212\312\264\260FY9\355a\321\230\337/T\236\2q\320\271\247IO\216\232<[I\217\240#\245\355\224&(z\221\346S \16j\367t\326l\306\237\30\17\4R\375_\302\207Y,\2157}\320\301 \334\320\33\253\355\0\304a~\330\357\276\251\235?\237r'v\370\20\203\245\361\$\3500\361\364\21253\323\375l\31\365\306\365\371\304Y\367\5q\320\202Aa\3658\21G\352\333^\353'l3\31\250w\225\224\232\265\255\235+WYU\225z\270q\364D\f\253\361\252\33\335\307\346\262\377\320\211\365H\341\203\7Y\304_^\257s\335z\23\306\225Ks\\EE\324[\326\357\217\264\356\2078L\16\211]\355<\346b'6K\242\365\366\2609L\320U\274\302|\243\244\356\352\315\f>%\374\2077\305\374\372\20\207|\261\353\365\357\377\$\346GBLF\335\304<\263rR*\334R\303`\$+\36\fF\16\376\6\3420\270\341\344\261\\r2[l\220P\205\315\6\313\$a!/\33\222\224\324\302\215\267\336\22\341\$\4\210\203r/\364|\25\373p\3032\351\332'f\33,\223\227\315\0043,\244{P\267\276\300\340\203H\270\21~\365ea\357\3\304a\f\344\225p\362x+&_\373\24i\335\317\354T\7\265\266N\366\301\16\317\316\335\f\26n\10\36n@\34\306\365B\265u\\>\332\220)\325\221];\31\335(UU\33\267\311\273\315\217\272k\267\302D|D\345\"\207\33\20\2071\326\3604\267pIR,\6M\251F\217ti\254\226\30)YY\372\355\222\320\35\316u\353i\327\316_\233?\212\34n@\34\306X\203\327A\355\344m7\352D\330\221\227\232\230]\266\214\356p\224\225\273Y\255d#\315*\346\332\r\210\303\f\326\260\30\272\246\220\10(\322}\34\356\270\2315\324\312\265\314>\216\245\304!\16\326\330\363\v\246ttr\264\20660`l\2774\322\330\300\362`\7Y\334\341\256o`i\2150I\33\r\212\"!\16\341pm\251\361\3247\360\32\327\270\222\6\377\272\331\330\37H\222\352`\323\36\226_\201\270C\227\357\322\34a#Ju\327n\226\223e1\277?\364\253\335R\274\2\20G\352\201\306[]l\326\377\334\202h_\37\215\315x\242\35\355\32\333BL\"\337\214\246\227\4,f!:#\1\21\263\321\320+A\337\256\235\202\217\211\376\343E\200\v\222W\206\253\244T\21`\31\2I(Fv\376\222\322\17\17l\254\"Q\0\343`\312]Rj\360\241`]\215 \257\r\21\231\253x\5\343\233@\222\0246;\263!\342`\204\243\254<\243\355u\222\233(b,^\nu\264\323\333\240\201}\302r\305\313\331\331DX\334C\17\233\327K\332\232\210\214\2615\264\201\201\221\272Z\231\372Qx\341\26\301\252c\3312\307\277<\312w,\343\306\$%L\371\305&\tK\$7\227q\224\236H[\310\33\353x\374\211\320\257\233\331\367\275\326\314L\327\363U\274\226\377\6k\266H\26\200C\20\327==\312c\217\333\27,\260/z\204\315\312\342\224\210\371\375\$\236g\221l76\3308\3154\353\263-\365\rZIi\250\355\0\33}\350\325\211+W9\v\vy\365\20\$\304\23p\217/\210\343\366\221\205m\336<\345\336{\225\7\346q\234^M\252_\332\372s6\243\0z\302R\263%\243\345\25^\357\22\311\n\211>b\0336\206~\373\206\276\313!\235oM\232\336\371\364\323\354c\253\257\$\236\235\235\242\355`\236\224m//Na\343\31{~\1iN6W\26\351>\256}\366\331\230\211\265\276S\223|zH\372j\233}\267\345\352\271'\366\371\363-\36\217\310\246\270\216@]-\343\0\236es\3376A\213\364|h\224A\22I\250\10A%y\310\203\33\252\244\214\315\205\25G2q{\374\213/n\363\365\246O\0270\343\230H4\273o/\227e\310\214\27M\336\26m`@\373\350\254\366\351\247\261\263g\223_(\245g\240\331\vI\207\241\314\231\243\314\235+\310\240\25\371.\303\313\237\222\364\201\2248U\321\215`\n)\10k\r\313h\321=I\342\234\302\354\243A\"\304k\203\304D\347\21\17\4n\334\347\335:%CI\4\230B\26\362\23k\4*V\313\373Lb\214Ch\364%\33\257\265\362-y\32\251\253\265fd\360\35\10\270m\347a\317\316\226\250Y\23\326\220e\255\327\370w\36/\247\310\326\10To\22\241P\222\344\341,K\340\314\215\t\254\1q\10\375x\r\257~N\234z'\270\3\326\2008D\207\274\242\344\361\22mn\37\356\2005 \16\201\323\223\355/\222WT\314\307\v\356\2300\321\276>\323X\3\342\20\254G\362\371Hz\"\370r \342\216\21\361\316@\226 \204\\\263\3124\326\260`VE\234@#\324\321\36\346Q]6\1\310u\306\207\206\334\317\226\tU\305#\256j\371\315\246C\34&\357\216\364\335\267\244\352\216\364C\25\316\236U\267\276`K\217\2454\23C\257-\332\372s)v\364B\252\"Yn\22\250\253\25vD\3436\27\337\3333\\\372c\222\272\243\35\307\205\334\31rLi\rD\34<\225\301\254\372\223b\20658HRw.\333\336\10\236xr_\266\7q\230\260#\n\377\256Cve\\K\270i\217v\342\204\273z\263D\25\203T\273\204`\375/\244+\223\2078\304\355\205\"\357\277\27n;`\312GJO[\226?\225\346\241G\314\357\37y\365\25\31k\344!\16QC\214\377>\222\16\317\23\t=H\$\345\336\370S\271*G\f!\324\331\31~\365e3M\270B\34\334|a\340\26\22\322\364\272>_`\315*}c\3475\25i\222\271D\272\217\207\232\367\231>7\2018\350\6\253\332\271\217\"\335\335\332\261w\323\312\27\327Kst\303nGY\271\353\207?2\361|mz*\3\3420R\26\321\376~\355\203\356\364|\206n\372^\265\356'\277L\251\217tV\6\3041A\342\301`\354\322\245\350\2313\332'\37\247yd\221\274>\354\305+\\\337\377\201\230{\352\244\324ID\216\35K\253\261\f\210cR\232\320.^\210\17\rk\247NN~\353\3234M^:\332\311/\233\327\353,)\25\355\304\211\244\256?mF\270\223\$\265=G-\243\333\216\352\273\363N\345\276\373\311\337m3\276f\233\256\237\33,\373\356\236c;\230\222P\202\374\2258\"\221\253\343\21\241\322_\25\257p~'_\374\311\227\364\34\341\246\"\216\24\344r\215_\22\214Y\346\37\377f\326,\252\235\317u\33\32'\2\207+\277\37U\3\354\300\363\341\33=\305\306\221\233k_\270H\234\30\204\304\230\321S'\243\375\375\360\5kq\30\205\2624\307:uj2\22\31\204\354\220\266V\226,\261?\370\20\227]\310\211,\264s\347\242>\255\2358a\326\352\2224\22\7H[\211\330\346\315\263\315\230i\237?\237RL\252\r\f\220PT;^\373\344\343\330\351~L\207A\34\300\204\$\222\337\304IZ\312\2349V\217\347\212bn>MC\324`\t\4\256d\243\243\343V\261\277\376%\376\371\347\261\213\27\240\t\210\3\0\300\1\354\307\1\0\2008\0\0\20\7\0\0\342\0\0@\34\0\0\210\3\0\0 \16\0\0\304\1\0\2008\0\0\222\362\377\2\f\0\330R\221^i(\247\250\0\0\0\0IEND\256B`\202";
3054 if ($imageData === false) {
3055 throw new \Kibo\Phast\Exceptions\RuntimeException('Could not read testing image for ' . static::class . ' diagnostics.');
3056 }
3057 $image = new \Kibo\Phast\Filters\Image\ImageImplementations\DummyImage();
3058 $image->setImageString($imageData);
3059 $filter->transformImage($image, []);
3060 }
3061 }
3062 namespace Kibo\Phast\Filters\Image\ImageAPIClient;
3063
3064 class Factory implements \Kibo\Phast\Filters\Image\ImageFilterFactory
3065 {
3066 public function make(array $config)
3067 {
3068 $signature = new \Kibo\Phast\Security\ServiceSignature(new \Kibo\Phast\Cache\File\Cache($config['cache'], 'api-service-signature'));
3069 return new \Kibo\Phast\Filters\Image\ImageAPIClient\Filter($config['images']['filters'][\Kibo\Phast\Filters\Image\ImageAPIClient\Filter::class], $signature, (new \Kibo\Phast\HTTP\ClientFactory())->make($config));
3070 }
3071 }
3072 namespace Kibo\Phast\Filters\Image\ImageAPIClient;
3073
3074 class Filter implements \Kibo\Phast\Filters\Image\ImageFilter
3075 {
3076 /**
3077 * @var array
3078 */
3079 private $config;
3080 /**
3081 * @var ServiceSignature
3082 */
3083 private $signature;
3084 /**
3085 * @var Client
3086 */
3087 private $client;
3088 /**
3089 * Filter constructor.
3090 * @param array $config
3091 * @param ServiceSignature $signature
3092 * @param Client $client
3093 */
3094 public function __construct(array $config, \Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\HTTP\Client $client)
3095 {
3096 $this->config = $config;
3097 $this->signature = $signature;
3098 $this->client = $client;
3099 $this->signature->setIdentities('');
3100 }
3101 public function getCacheSalt(array $request)
3102 {
3103 $result = 'api-call';
3104 foreach (['width', 'height', 'preferredType'] as $key) {
3105 if (isset($request[$key])) {
3106 $result .= "-{$key}-{$request[$key]}";
3107 }
3108 }
3109 return $result;
3110 }
3111 public function transformImage(\Kibo\Phast\Filters\Image\Image $image, array $request)
3112 {
3113 $url = $this->getRequestURL($request);
3114 $headers = $this->getRequestHeaders($image, $request);
3115 $data = $image->getAsString();
3116 try {
3117 $response = $this->client->post(\Kibo\Phast\ValueObjects\URL::fromString($url), $data, $headers);
3118 } catch (\Exception $e) {
3119 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Request exception: ' . get_class($e) . ' MSG: ' . $e->getMessage() . ' Code: ' . $e->getCode());
3120 }
3121 if (strlen($response->getContent()) === 0) {
3122 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Image API response is empty');
3123 }
3124 $newImage = new \Kibo\Phast\Filters\Image\ImageImplementations\DummyImage();
3125 $newImage->setImageString($response->getContent());
3126 $headers = [];
3127 foreach ($response->getHeaders() as $name => $value) {
3128 $headers[strtolower($name)] = $value;
3129 }
3130 $newImage->setType($headers['content-type']);
3131 return $newImage;
3132 }
3133 private function getRequestURL(array $request)
3134 {
3135 $params = [];
3136 foreach (['width', 'height'] as $key) {
3137 if (isset($request[$key])) {
3138 $params[$key] = $request[$key];
3139 }
3140 }
3141 return (new \Kibo\Phast\Services\ServiceRequest())->withUrl(\Kibo\Phast\ValueObjects\URL::fromString($this->config['api-url']))->withParams($params)->sign($this->signature)->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY);
3142 }
3143 private function getRequestHeaders(\Kibo\Phast\Filters\Image\Image $image, array $request)
3144 {
3145 $headers = ['X-Phast-Image-API-Client' => $this->getRequestToken(), 'Content-Type' => 'application/octet-stream'];
3146 if (isset($request['preferredType']) && $request['preferredType'] == \Kibo\Phast\Filters\Image\Image::TYPE_WEBP) {
3147 $headers['Accept'] = 'image/webp';
3148 }
3149 return $headers;
3150 }
3151 private function getRequestToken()
3152 {
3153 $token_parts = [];
3154 foreach (['host-name', 'request-uri', 'plugin-version'] as $key) {
3155 $token_parts[$key] = $this->config[$key];
3156 }
3157 $token_parts['php'] = PHP_VERSION;
3158 return json_encode($token_parts);
3159 }
3160 }
3161 namespace Kibo\Phast\Filters\Image\ImageImplementations;
3162
3163 abstract class BaseImage
3164 {
3165 /**
3166 * @var integer
3167 */
3168 protected $width;
3169 /**
3170 * @var integer
3171 */
3172 protected $height;
3173 /**
3174 * @var integer
3175 */
3176 protected $compression;
3177 /**
3178 * @var string
3179 */
3180 protected $type;
3181 /**
3182 * @return string
3183 */
3184 public abstract function getAsString();
3185 /**
3186 * @return integer
3187 */
3188 public function getSizeAsString()
3189 {
3190 return strlen($this->getAsString());
3191 }
3192 /**
3193 * @param $width
3194 * @param $height
3195 * @return static
3196 */
3197 public function resize($width, $height)
3198 {
3199 $im = clone $this;
3200 $im->width = $width;
3201 $im->height = $height;
3202 return $im;
3203 }
3204 /**
3205 * @param $compression
3206 * @return static
3207 */
3208 public function compress($compression)
3209 {
3210 $im = clone $this;
3211 $im->compression = $compression;
3212 return $im;
3213 }
3214 /**
3215 * @param $type
3216 * @return static
3217 */
3218 public function encodeTo($type)
3219 {
3220 $im = clone $this;
3221 $im->type = $type;
3222 return $im;
3223 }
3224 }
3225 namespace Kibo\Phast\Filters\Image\ImageImplementations;
3226
3227 class DefaultImage extends \Kibo\Phast\Filters\Image\ImageImplementations\BaseImage implements \Kibo\Phast\Filters\Image\Image
3228 {
3229 /**
3230 * @var URL
3231 */
3232 private $imageURL;
3233 /**
3234 * @var Retriever
3235 */
3236 private $retriever;
3237 /**
3238 * @var string
3239 */
3240 private $imageString;
3241 /**
3242 * @var array
3243 */
3244 private $imageInfo;
3245 /**
3246 * @var ObjectifiedFunctions
3247 */
3248 private $funcs;
3249 public function __construct(\Kibo\Phast\ValueObjects\URL $imageURL, \Kibo\Phast\Retrievers\Retriever $retriever, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
3250 {
3251 $this->imageURL = $imageURL;
3252 $this->retriever = $retriever;
3253 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
3254 }
3255 public function getWidth()
3256 {
3257 return isset($this->width) ? $this->width : $this->getImageInfo()[0];
3258 }
3259 public function getHeight()
3260 {
3261 return isset($this->height) ? $this->height : $this->getImageInfo()[1];
3262 }
3263 public function getType()
3264 {
3265 if (isset($this->type)) {
3266 return $this->type;
3267 }
3268 $type = @image_type_to_mime_type($this->getImageInfo()[2]);
3269 if (!$type) {
3270 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Could not determine image type');
3271 }
3272 return $type;
3273 }
3274 public function getAsString()
3275 {
3276 return $this->getImageString();
3277 }
3278 private function getImageString()
3279 {
3280 if (!isset($this->imageString)) {
3281 $imageString = $this->retriever->retrieve($this->imageURL);
3282 if ($imageString === false) {
3283 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Could not find image: ' . $this->imageURL, 0, null, $this->imageURL);
3284 }
3285 $this->imageString = $imageString;
3286 }
3287 return $this->imageString;
3288 }
3289 private function getImageInfo()
3290 {
3291 if (!isset($this->imageInfo)) {
3292 if ($this->getImageString() === '') {
3293 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Image is empty');
3294 }
3295 $imageInfo = @getimagesizefromstring($this->getImageString());
3296 if ($imageInfo === false) {
3297 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Could not read GD image info');
3298 }
3299 $this->imageInfo = $imageInfo;
3300 }
3301 return $this->imageInfo;
3302 }
3303 protected function __clone()
3304 {
3305 throw new \Kibo\Phast\Exceptions\LogicException('No operations may be performed on DefaultImage');
3306 }
3307 }
3308 namespace Kibo\Phast\Filters\CSS\CSSMinifier;
3309
3310 class Factory
3311 {
3312 public function make()
3313 {
3314 return new \Kibo\Phast\Filters\CSS\CSSMinifier\Filter();
3315 }
3316 }
3317 namespace Kibo\Phast\Filters\CSS\CSSURLRewriter;
3318
3319 class Factory
3320 {
3321 public function make()
3322 {
3323 return new \Kibo\Phast\Filters\CSS\CSSURLRewriter\Filter();
3324 }
3325 }
3326 namespace Kibo\Phast\Filters\CSS\ImageURLRewriter;
3327
3328 class Factory
3329 {
3330 public function make(array $config)
3331 {
3332 return new \Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter((new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriterFactory())->make($config, \Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter::class));
3333 }
3334 }
3335 namespace Kibo\Phast\Filters\CSS\Composite;
3336
3337 class Factory
3338 {
3339 /**
3340 * @param array $config
3341 * @return Filter
3342 */
3343 public function make(array $config)
3344 {
3345 $class = \Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter::class;
3346 if (isset($config['documents']['filters'][$class]['serviceUrl'])) {
3347 $serviceUrl = $config['documents']['filters'][$class]['serviceUrl'];
3348 } else {
3349 $serviceUrl = $config['servicesUrl'] . '?service=images';
3350 }
3351 $filter = new \Kibo\Phast\Filters\CSS\Composite\Filter($serviceUrl);
3352 foreach (array_keys($config['styles']['filters']) as $filterClass) {
3353 $filter->addFilter(\Kibo\Phast\Environment\Package::fromPackageClass($filterClass)->getFactory()->make($config));
3354 }
3355 return $filter;
3356 }
3357 }
3358 namespace Kibo\Phast\Filters\CSS\FontSwap;
3359
3360 class Factory
3361 {
3362 public function make()
3363 {
3364 return new \Kibo\Phast\Filters\CSS\FontSwap\Filter();
3365 }
3366 }
3367 namespace Kibo\Phast\Filters\CSS\ImportsStripper;
3368
3369 class Factory
3370 {
3371 public function make()
3372 {
3373 return new \Kibo\Phast\Filters\CSS\ImportsStripper\Filter();
3374 }
3375 }
3376 namespace Kibo\Phast\Filters\Text\Decode;
3377
3378 class Factory
3379 {
3380 public function make()
3381 {
3382 return new \Kibo\Phast\Filters\Text\Decode\Filter();
3383 }
3384 }
3385 namespace Kibo\Phast\Parsing\HTML;
3386
3387 class PCRETokenizer
3388 {
3389 private $mainPattern = '~
3390 # Allow duplicate names for subpatterns
3391 (?J)
3392
3393 (
3394 @@COMMENT |
3395 @@SCRIPT |
3396 @@STYLE |
3397 @@CLOSING_TAG |
3398 @@TAG
3399 )
3400 ~Xxsi';
3401 private $attributePattern = '~
3402 @attr
3403 ~Xxsi';
3404 private $subroutines = array('COMMENT' => '
3405 <!--.*?-->
3406 ', 'SCRIPT' => "\n (?= <script[\\s>]) @@TAG\n (?'body' .*? )\n (?'closing_tag' </script/?+(?:\\s[^a-z>]*+)?+> )\n ", 'STYLE' => "\n (?= <style[\\s>]) @@TAG\n (?'body' .*? )\n (?'closing_tag' </style/?+(?:\\s[^a-z>]*+)?+> )\n ", 'TAG' => "\n < @@tag_name \\s*+ @@attrs? @tag_end\n ", 'tag_name' => "\n [^\\s>]++\n ", 'attrs' => '
3407 (?: @attr )*+
3408 ', 'attr' => "\n \\s*+\n @@attr_name\n (?: \\s*+ = \\s*+ @attr_value )?\n ", 'attr_name' => "\n [^\\s>][^\\s>=]*+\n ", 'attr_value' => "\n (?|\n \"(?'attr_value'[^\"]*+)\" |\n ' (?'attr_value' [^']*+) ' |\n (?'attr_value' [^\\s>]*+)\n )\n ", 'tag_end' => "\n \\s*+ >\n ", 'CLOSING_TAG' => '
3409 </ @@tag_name [^>]*+ >
3410 ');
3411 public function __construct()
3412 {
3413 $this->mainPattern = $this->compilePattern($this->mainPattern, $this->subroutines);
3414 $this->attributePattern = $this->compilePattern($this->attributePattern, $this->subroutines);
3415 }
3416 public function tokenize($data)
3417 {
3418 $offset = 0;
3419 while (preg_match($this->mainPattern, $data, $match, PREG_OFFSET_CAPTURE, $offset)) {
3420 if ($match[0][1] > $offset) {
3421 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Junk();
3422 $element->originalString = substr($data, $offset, $match[0][1] - $offset);
3423 (yield $element);
3424 }
3425 if (!empty($match['COMMENT'][0])) {
3426 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Comment();
3427 $element->originalString = $match[0][0];
3428 } elseif (!empty($match['TAG'][0]) || !empty($match['SCRIPT'][0]) || !empty($match['STYLE'][0])) {
3429 $attributes = $match['attrs'][0] === '' ? [] : $this->parseAttributes($match['attrs'][0]);
3430 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag($match['tag_name'][0], $attributes);
3431 $element->originalString = $match['TAG'][0];
3432 if (isset($match['body'][1]) && $match['body'][1] != -1) {
3433 $element->setTextContent($match['body'][0]);
3434 $element = $element->withClosingTag($match['closing_tag'][0]);
3435 }
3436 } elseif (!empty($match['CLOSING_TAG'][0])) {
3437 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag($match['tag_name'][0]);
3438 $element->originalString = $match[0][0];
3439 } else {
3440 throw new \Kibo\Phast\Exceptions\RuntimeException("Unhandled match:\n" . \Kibo\Phast\Common\JSON::prettyEncode($match));
3441 }
3442 (yield $element);
3443 $offset = $match[0][1] + strlen($match[0][0]);
3444 }
3445 if ($offset < strlen($data) - 1) {
3446 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Junk();
3447 $element->originalString = substr($data, $offset);
3448 (yield $element);
3449 }
3450 }
3451 private function parseAttributes($str)
3452 {
3453 $matches = $this->repeatMatch($this->attributePattern, $str);
3454 foreach ($matches as $match) {
3455 (yield $match['attr_name'][0] => isset($match['attr_value'][0]) ? html_entity_decode($match['attr_value'][0], ENT_QUOTES, 'UTF-8') : '');
3456 }
3457 }
3458 private function repeatMatch($pattern, $subject)
3459 {
3460 $offset = 0;
3461 while (preg_match($pattern, $subject, $match, PREG_OFFSET_CAPTURE, $offset)) {
3462 (yield $match);
3463 $offset = $match[0][1] + strlen($match[0][0]);
3464 }
3465 if ($offset < strlen($subject) - 1) {
3466 throw new \Kibo\Phast\Exceptions\RuntimeException('Unmatched part of subject: ' . substr($subject, $offset));
3467 }
3468 }
3469 /**
3470 * Replace subroutines in patterns
3471 */
3472 private function compilePattern($pattern, array $subroutines)
3473 {
3474 return preg_replace_callback('/@(@?)(\\w+)/', function ($match) use($subroutines) {
3475 $capture = !empty($match[1]);
3476 $ref = $match[2];
3477 if (!isset($subroutines[$ref])) {
3478 throw new \Kibo\Phast\Exceptions\RuntimeException("Unknown pattern '{$ref}' used, or circular reference");
3479 }
3480 $subroutine = $subroutines[$ref];
3481 unset($subroutines[$ref]);
3482 $replace = $this->compilePattern($subroutine, $subroutines);
3483 if ($capture) {
3484 $replace = "(?'{$ref}'{$replace})";
3485 } else {
3486 $replace = "(?:{$replace})";
3487 }
3488 return $replace;
3489 }, $pattern);
3490 }
3491 }
3492 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
3493
3494 class Element
3495 {
3496 /**
3497 * @var string
3498 */
3499 public $originalString;
3500 /**
3501 * @param string $originalString
3502 */
3503 public function setOriginalString($originalString)
3504 {
3505 $this->originalString = $originalString;
3506 }
3507 public function __get($name)
3508 {
3509 $method = 'get' . ucfirst($name);
3510 if (method_exists($this, $method)) {
3511 return call_user_func([$this, $method]);
3512 }
3513 }
3514 public function __set($name, $value)
3515 {
3516 $method = 'set' . ucfirst($name);
3517 if (method_exists($this, $method)) {
3518 return call_user_func([$this, $method], $value);
3519 }
3520 }
3521 public function toString()
3522 {
3523 return $this->__toString();
3524 }
3525 public function __toString()
3526 {
3527 return isset($this->originalString) ? $this->originalString : '';
3528 }
3529 public function dump()
3530 {
3531 return '<' . preg_replace('~^.*\\\\~', '', get_class($this)) . ' ' . $this->dumpValue() . '>';
3532 }
3533 public function dumpValue()
3534 {
3535 return \Kibo\Phast\Common\JSON::encode($this->originalString);
3536 }
3537 }
3538 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
3539
3540 class Junk extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
3541 {
3542 }
3543 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
3544
3545 class Comment extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
3546 {
3547 public function isIEConditional()
3548 {
3549 return (bool) preg_match('/^<!--\\[if\\s/', $this->originalString);
3550 }
3551 }
3552 /**
3553 * Provide general element functions.
3554 */
3555 namespace Kibo\Phast\Parsing\HTML;
3556
3557 /**
3558 * This class provides general information about HTML5 elements,
3559 * including syntactic and semantic issues.
3560 * Parsers and serializers can
3561 * use this class as a reference point for information about the rules
3562 * of various HTML5 elements.
3563 *
3564 * @todo consider using a bitmask table lookup. There is enough overlap in
3565 * naming that this could significantly shrink the size and maybe make it
3566 * faster. See the Go teams implementation at https://code.google.com/p/go/source/browse/html/atom.
3567 */
3568 class HTMLInfo
3569 {
3570 /**
3571 * Indicates an element is described in the specification.
3572 */
3573 const KNOWN_ELEMENT = 1;
3574 // From section 8.1.2: "script", "style"
3575 // From 8.2.5.4.7 ("in body" insertion mode): "noembed"
3576 // From 8.4 "style", "xmp", "iframe", "noembed", "noframes"
3577 /**
3578 * Indicates the contained text should be processed as raw text.
3579 */
3580 const TEXT_RAW = 2;
3581 // From section 8.1.2: "textarea", "title"
3582 /**
3583 * Indicates the contained text should be processed as RCDATA.
3584 */
3585 const TEXT_RCDATA = 4;
3586 /**
3587 * Indicates the tag cannot have content.
3588 */
3589 const VOID_TAG = 8;
3590 // "address", "article", "aside", "blockquote", "center", "details", "dialog", "dir", "div", "dl",
3591 // "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "menu",
3592 // "nav", "ol", "p", "section", "summary", "ul"
3593 // "h1", "h2", "h3", "h4", "h5", "h6"
3594 // "pre", "listing"
3595 // "form"
3596 // "plaintext"
3597 /**
3598 * Indicates that if a previous event is for a P tag, that element
3599 * should be considered closed.
3600 */
3601 const AUTOCLOSE_P = 16;
3602 /**
3603 * Indicates that the text inside is plaintext (pre).
3604 */
3605 const TEXT_PLAINTEXT = 32;
3606 // See https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements
3607 /**
3608 * Indicates that the tag is a block.
3609 */
3610 const BLOCK_TAG = 64;
3611 /**
3612 * Indicates that the tag allows only inline elements as child nodes.
3613 */
3614 const BLOCK_ONLY_INLINE = 128;
3615 /**
3616 * The HTML5 elements as defined in http://dev.w3.org/html5/markup/elements.html.
3617 *
3618 * @var array
3619 */
3620 public static $html5 = array(
3621 'a' => 1,
3622 'abbr' => 1,
3623 'address' => 65,
3624 // NORMAL | BLOCK_TAG
3625 'area' => 9,
3626 // NORMAL | VOID_TAG
3627 'article' => 81,
3628 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3629 'aside' => 81,
3630 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3631 'audio' => 65,
3632 // NORMAL | BLOCK_TAG
3633 'b' => 1,
3634 'base' => 9,
3635 // NORMAL | VOID_TAG
3636 'bdi' => 1,
3637 'bdo' => 1,
3638 'blockquote' => 81,
3639 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3640 'body' => 1,
3641 'br' => 9,
3642 // NORMAL | VOID_TAG
3643 'button' => 1,
3644 'canvas' => 65,
3645 // NORMAL | BLOCK_TAG
3646 'caption' => 1,
3647 'cite' => 1,
3648 'code' => 1,
3649 'col' => 9,
3650 // NORMAL | VOID_TAG
3651 'colgroup' => 1,
3652 'command' => 9,
3653 // NORMAL | VOID_TAG
3654 // "data" => 1, // This is highly experimental and only part of the whatwg spec (not w3c). See https://developer.mozilla.org/en-US/docs/HTML/Element/data
3655 'datalist' => 1,
3656 'dd' => 65,
3657 // NORMAL | BLOCK_TAG
3658 'del' => 1,
3659 'details' => 17,
3660 // NORMAL | AUTOCLOSE_P,
3661 'dfn' => 1,
3662 'dialog' => 17,
3663 // NORMAL | AUTOCLOSE_P,
3664 'div' => 81,
3665 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3666 'dl' => 81,
3667 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3668 'dt' => 1,
3669 'em' => 1,
3670 'embed' => 9,
3671 // NORMAL | VOID_TAG
3672 'fieldset' => 81,
3673 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3674 'figcaption' => 81,
3675 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3676 'figure' => 81,
3677 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3678 'footer' => 81,
3679 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3680 'form' => 81,
3681 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3682 'h1' => 81,
3683 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3684 'h2' => 81,
3685 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3686 'h3' => 81,
3687 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3688 'h4' => 81,
3689 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3690 'h5' => 81,
3691 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3692 'h6' => 81,
3693 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3694 'head' => 1,
3695 'header' => 81,
3696 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3697 'hgroup' => 81,
3698 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3699 'hr' => 73,
3700 // NORMAL | VOID_TAG
3701 'html' => 1,
3702 'i' => 1,
3703 'iframe' => 3,
3704 // NORMAL | TEXT_RAW
3705 'img' => 9,
3706 // NORMAL | VOID_TAG
3707 'input' => 9,
3708 // NORMAL | VOID_TAG
3709 'kbd' => 1,
3710 'ins' => 1,
3711 'keygen' => 9,
3712 // NORMAL | VOID_TAG
3713 'label' => 1,
3714 'legend' => 1,
3715 'li' => 1,
3716 'link' => 9,
3717 // NORMAL | VOID_TAG
3718 'map' => 1,
3719 'mark' => 1,
3720 'menu' => 17,
3721 // NORMAL | AUTOCLOSE_P,
3722 'meta' => 9,
3723 // NORMAL | VOID_TAG
3724 'meter' => 1,
3725 'nav' => 17,
3726 // NORMAL | AUTOCLOSE_P,
3727 'noscript' => 65,
3728 // NORMAL | BLOCK_TAG
3729 'object' => 1,
3730 'ol' => 81,
3731 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3732 'optgroup' => 1,
3733 'option' => 1,
3734 'output' => 65,
3735 // NORMAL | BLOCK_TAG
3736 'p' => 209,
3737 // NORMAL | AUTOCLOSE_P | BLOCK_TAG | BLOCK_ONLY_INLINE
3738 'param' => 9,
3739 // NORMAL | VOID_TAG
3740 'pre' => 81,
3741 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3742 'progress' => 1,
3743 'q' => 1,
3744 'rp' => 1,
3745 'rt' => 1,
3746 'ruby' => 1,
3747 's' => 1,
3748 'samp' => 1,
3749 'script' => 3,
3750 // NORMAL | TEXT_RAW
3751 'section' => 81,
3752 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3753 'select' => 1,
3754 'small' => 1,
3755 'source' => 9,
3756 // NORMAL | VOID_TAG
3757 'span' => 1,
3758 'strong' => 1,
3759 'style' => 3,
3760 // NORMAL | TEXT_RAW
3761 'sub' => 1,
3762 'summary' => 17,
3763 // NORMAL | AUTOCLOSE_P,
3764 'sup' => 1,
3765 'table' => 65,
3766 // NORMAL | BLOCK_TAG
3767 'tbody' => 1,
3768 'td' => 1,
3769 'textarea' => 5,
3770 // NORMAL | TEXT_RCDATA
3771 'tfoot' => 65,
3772 // NORMAL | BLOCK_TAG
3773 'th' => 1,
3774 'thead' => 1,
3775 'time' => 1,
3776 'title' => 5,
3777 // NORMAL | TEXT_RCDATA
3778 'tr' => 1,
3779 'track' => 9,
3780 // NORMAL | VOID_TAG
3781 'u' => 1,
3782 'ul' => 81,
3783 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
3784 'var' => 1,
3785 'video' => 65,
3786 // NORMAL | BLOCK_TAG
3787 'wbr' => 9,
3788 // NORMAL | VOID_TAG
3789 // Legacy?
3790 'basefont' => 8,
3791 // VOID_TAG
3792 'bgsound' => 8,
3793 // VOID_TAG
3794 'noframes' => 2,
3795 // RAW_TEXT
3796 'frame' => 9,
3797 // NORMAL | VOID_TAG
3798 'frameset' => 1,
3799 'center' => 16,
3800 'dir' => 16,
3801 'listing' => 16,
3802 // AUTOCLOSE_P
3803 'plaintext' => 48,
3804 // AUTOCLOSE_P | TEXT_PLAINTEXT
3805 'applet' => 0,
3806 'marquee' => 0,
3807 'isindex' => 8,
3808 // VOID_TAG
3809 'xmp' => 20,
3810 // AUTOCLOSE_P | VOID_TAG | RAW_TEXT
3811 'noembed' => 2,
3812 );
3813 /**
3814 * The MathML elements.
3815 * See http://www.w3.org/wiki/MathML/Elements.
3816 *
3817 * In our case we are only concerned with presentation MathML and not content
3818 * MathML. There is a nice list of this subset at https://developer.mozilla.org/en-US/docs/MathML/Element.
3819 *
3820 * @var array
3821 */
3822 public static $mathml = array('maction' => 1, 'maligngroup' => 1, 'malignmark' => 1, 'math' => 1, 'menclose' => 1, 'merror' => 1, 'mfenced' => 1, 'mfrac' => 1, 'mglyph' => 1, 'mi' => 1, 'mlabeledtr' => 1, 'mlongdiv' => 1, 'mmultiscripts' => 1, 'mn' => 1, 'mo' => 1, 'mover' => 1, 'mpadded' => 1, 'mphantom' => 1, 'mroot' => 1, 'mrow' => 1, 'ms' => 1, 'mscarries' => 1, 'mscarry' => 1, 'msgroup' => 1, 'msline' => 1, 'mspace' => 1, 'msqrt' => 1, 'msrow' => 1, 'mstack' => 1, 'mstyle' => 1, 'msub' => 1, 'msup' => 1, 'msubsup' => 1, 'mtable' => 1, 'mtd' => 1, 'mtext' => 1, 'mtr' => 1, 'munder' => 1, 'munderover' => 1);
3823 /**
3824 * The svg elements.
3825 *
3826 * The Mozilla documentation has a good list at https://developer.mozilla.org/en-US/docs/SVG/Element.
3827 * The w3c list appears to be lacking in some areas like filter effect elements.
3828 * That list can be found at http://www.w3.org/wiki/SVG/Elements.
3829 *
3830 * Note, FireFox appears to do a better job rendering filter effects than chrome.
3831 * While they are in the spec I'm not sure how widely implemented they are.
3832 *
3833 * @var array
3834 */
3835 public static $svg = array(
3836 'a' => 1,
3837 'altGlyph' => 1,
3838 'altGlyphDef' => 1,
3839 'altGlyphItem' => 1,
3840 'animate' => 1,
3841 'animateColor' => 1,
3842 'animateMotion' => 1,
3843 'animateTransform' => 1,
3844 'circle' => 1,
3845 'clipPath' => 1,
3846 'color-profile' => 1,
3847 'cursor' => 1,
3848 'defs' => 1,
3849 'desc' => 1,
3850 'ellipse' => 1,
3851 'feBlend' => 1,
3852 'feColorMatrix' => 1,
3853 'feComponentTransfer' => 1,
3854 'feComposite' => 1,
3855 'feConvolveMatrix' => 1,
3856 'feDiffuseLighting' => 1,
3857 'feDisplacementMap' => 1,
3858 'feDistantLight' => 1,
3859 'feFlood' => 1,
3860 'feFuncA' => 1,
3861 'feFuncB' => 1,
3862 'feFuncG' => 1,
3863 'feFuncR' => 1,
3864 'feGaussianBlur' => 1,
3865 'feImage' => 1,
3866 'feMerge' => 1,
3867 'feMergeNode' => 1,
3868 'feMorphology' => 1,
3869 'feOffset' => 1,
3870 'fePointLight' => 1,
3871 'feSpecularLighting' => 1,
3872 'feSpotLight' => 1,
3873 'feTile' => 1,
3874 'feTurbulence' => 1,
3875 'filter' => 1,
3876 'font' => 1,
3877 'font-face' => 1,
3878 'font-face-format' => 1,
3879 'font-face-name' => 1,
3880 'font-face-src' => 1,
3881 'font-face-uri' => 1,
3882 'foreignObject' => 1,
3883 'g' => 1,
3884 'glyph' => 1,
3885 'glyphRef' => 1,
3886 'hkern' => 1,
3887 'image' => 1,
3888 'line' => 1,
3889 'linearGradient' => 1,
3890 'marker' => 1,
3891 'mask' => 1,
3892 'metadata' => 1,
3893 'missing-glyph' => 1,
3894 'mpath' => 1,
3895 'path' => 1,
3896 'pattern' => 1,
3897 'polygon' => 1,
3898 'polyline' => 1,
3899 'radialGradient' => 1,
3900 'rect' => 1,
3901 'script' => 3,
3902 // NORMAL | RAW_TEXT
3903 'set' => 1,
3904 'stop' => 1,
3905 'style' => 3,
3906 // NORMAL | RAW_TEXT
3907 'svg' => 1,
3908 'switch' => 1,
3909 'symbol' => 1,
3910 'text' => 1,
3911 'textPath' => 1,
3912 'title' => 1,
3913 'tref' => 1,
3914 'tspan' => 1,
3915 'use' => 1,
3916 'view' => 1,
3917 'vkern' => 1,
3918 );
3919 /**
3920 * Some attributes in SVG are case sensetitive.
3921 *
3922 * This map contains key/value pairs with the key as the lowercase attribute
3923 * name and the value with the correct casing.
3924 */
3925 public static $svgCaseSensitiveAttributeMap = array('attributename' => 'attributeName', 'attributetype' => 'attributeType', 'basefrequency' => 'baseFrequency', 'baseprofile' => 'baseProfile', 'calcmode' => 'calcMode', 'clippathunits' => 'clipPathUnits', 'contentscripttype' => 'contentScriptType', 'contentstyletype' => 'contentStyleType', 'diffuseconstant' => 'diffuseConstant', 'edgemode' => 'edgeMode', 'externalresourcesrequired' => 'externalResourcesRequired', 'filterres' => 'filterRes', 'filterunits' => 'filterUnits', 'glyphref' => 'glyphRef', 'gradienttransform' => 'gradientTransform', 'gradientunits' => 'gradientUnits', 'kernelmatrix' => 'kernelMatrix', 'kernelunitlength' => 'kernelUnitLength', 'keypoints' => 'keyPoints', 'keysplines' => 'keySplines', 'keytimes' => 'keyTimes', 'lengthadjust' => 'lengthAdjust', 'limitingconeangle' => 'limitingConeAngle', 'markerheight' => 'markerHeight', 'markerunits' => 'markerUnits', 'markerwidth' => 'markerWidth', 'maskcontentunits' => 'maskContentUnits', 'maskunits' => 'maskUnits', 'numoctaves' => 'numOctaves', 'pathlength' => 'pathLength', 'patterncontentunits' => 'patternContentUnits', 'patterntransform' => 'patternTransform', 'patternunits' => 'patternUnits', 'pointsatx' => 'pointsAtX', 'pointsaty' => 'pointsAtY', 'pointsatz' => 'pointsAtZ', 'preservealpha' => 'preserveAlpha', 'preserveaspectratio' => 'preserveAspectRatio', 'primitiveunits' => 'primitiveUnits', 'refx' => 'refX', 'refy' => 'refY', 'repeatcount' => 'repeatCount', 'repeatdur' => 'repeatDur', 'requiredextensions' => 'requiredExtensions', 'requiredfeatures' => 'requiredFeatures', 'specularconstant' => 'specularConstant', 'specularexponent' => 'specularExponent', 'spreadmethod' => 'spreadMethod', 'startoffset' => 'startOffset', 'stddeviation' => 'stdDeviation', 'stitchtiles' => 'stitchTiles', 'surfacescale' => 'surfaceScale', 'systemlanguage' => 'systemLanguage', 'tablevalues' => 'tableValues', 'targetx' => 'targetX', 'targety' => 'targetY', 'textlength' => 'textLength', 'viewbox' => 'viewBox', 'viewtarget' => 'viewTarget', 'xchannelselector' => 'xChannelSelector', 'ychannelselector' => 'yChannelSelector', 'zoomandpan' => 'zoomAndPan');
3926 /**
3927 * Some SVG elements are case sensetitive.
3928 * This map contains these.
3929 *
3930 * The map contains key/value store of the name is lowercase as the keys and
3931 * the correct casing as the value.
3932 */
3933 public static $svgCaseSensitiveElementMap = array('altglyph' => 'altGlyph', 'altglyphdef' => 'altGlyphDef', 'altglyphitem' => 'altGlyphItem', 'animatecolor' => 'animateColor', 'animatemotion' => 'animateMotion', 'animatetransform' => 'animateTransform', 'clippath' => 'clipPath', 'feblend' => 'feBlend', 'fecolormatrix' => 'feColorMatrix', 'fecomponenttransfer' => 'feComponentTransfer', 'fecomposite' => 'feComposite', 'feconvolvematrix' => 'feConvolveMatrix', 'fediffuselighting' => 'feDiffuseLighting', 'fedisplacementmap' => 'feDisplacementMap', 'fedistantlight' => 'feDistantLight', 'feflood' => 'feFlood', 'fefunca' => 'feFuncA', 'fefuncb' => 'feFuncB', 'fefuncg' => 'feFuncG', 'fefuncr' => 'feFuncR', 'fegaussianblur' => 'feGaussianBlur', 'feimage' => 'feImage', 'femerge' => 'feMerge', 'femergenode' => 'feMergeNode', 'femorphology' => 'feMorphology', 'feoffset' => 'feOffset', 'fepointlight' => 'fePointLight', 'fespecularlighting' => 'feSpecularLighting', 'fespotlight' => 'feSpotLight', 'fetile' => 'feTile', 'feturbulence' => 'feTurbulence', 'foreignobject' => 'foreignObject', 'glyphref' => 'glyphRef', 'lineargradient' => 'linearGradient', 'radialgradient' => 'radialGradient', 'textpath' => 'textPath');
3934 /**
3935 * Check whether the given element meets the given criterion.
3936 *
3937 * Example:
3938 *
3939 * Elements::isA('script', Elements::TEXT_RAW); // Returns true.
3940 *
3941 * Elements::isA('script', Elements::TEXT_RCDATA); // Returns false.
3942 *
3943 * @param string $name
3944 * The element name.
3945 * @param int $mask
3946 * One of the constants on this class.
3947 * @return boolean true if the element matches the mask, false otherwise.
3948 */
3949 public static function isA($name, $mask)
3950 {
3951 if (!static::isElement($name)) {
3952 return false;
3953 }
3954 return (static::element($name) & $mask) == $mask;
3955 }
3956 /**
3957 * Test if an element is a valid html5 element.
3958 *
3959 * @param string $name
3960 * The name of the element.
3961 *
3962 * @return bool True if a html5 element and false otherwise.
3963 */
3964 public static function isHtml5Element($name)
3965 {
3966 // html5 element names are case insensetitive. Forcing lowercase for the check.
3967 // Do we need this check or will all data passed here already be lowercase?
3968 return isset(static::$html5[strtolower($name)]);
3969 }
3970 /**
3971 * Test if an element name is a valid MathML presentation element.
3972 *
3973 * @param string $name
3974 * The name of the element.
3975 *
3976 * @return bool True if a MathML name and false otherwise.
3977 */
3978 public static function isMathMLElement($name)
3979 {
3980 // MathML is case-sensetitive unlike html5 elements.
3981 return isset(static::$mathml[$name]);
3982 }
3983 /**
3984 * Test if an element is a valid SVG element.
3985 *
3986 * @param string $name
3987 * The name of the element.
3988 *
3989 * @return boolean True if a SVG element and false otherise.
3990 */
3991 public static function isSvgElement($name)
3992 {
3993 // SVG is case-sensetitive unlike html5 elements.
3994 return isset(static::$svg[$name]);
3995 }
3996 /**
3997 * Is an element name valid in an html5 document.
3998 *
3999 * This includes html5 elements along with other allowed embedded content
4000 * such as svg and mathml.
4001 *
4002 * @param string $name
4003 * The name of the element.
4004 *
4005 * @return bool True if valid and false otherwise.
4006 */
4007 public static function isElement($name)
4008 {
4009 return static::isHtml5Element($name) || static::isMathMLElement($name) || static::isSvgElement($name);
4010 }
4011 /**
4012 * Get the element mask for the given element name.
4013 *
4014 * @param string $name
4015 * The name of the element.
4016 *
4017 * @return int|bool The element mask or false if element does not exist.
4018 */
4019 public static function element($name)
4020 {
4021 if (isset(static::$html5[$name])) {
4022 return static::$html5[$name];
4023 }
4024 if (isset(static::$svg[$name])) {
4025 return static::$svg[$name];
4026 }
4027 if (isset(static::$mathml[$name])) {
4028 return static::$mathml[$name];
4029 }
4030 return false;
4031 }
4032 /**
4033 * Normalize a SVG element name to its proper case and form.
4034 *
4035 * @param string $name
4036 * The name of the element.
4037 *
4038 * @return string The normalized form of the element name.
4039 */
4040 public static function normalizeSvgElement($name)
4041 {
4042 $name = strtolower($name);
4043 if (isset(static::$svgCaseSensitiveElementMap[$name])) {
4044 $name = static::$svgCaseSensitiveElementMap[$name];
4045 }
4046 return $name;
4047 }
4048 /**
4049 * Normalize a SVG attribute name to its proper case and form.
4050 *
4051 * @param string $name
4052 * The name of the attribute.
4053 *
4054 * @return string The normalized form of the attribute name.
4055 */
4056 public static function normalizeSvgAttribute($name)
4057 {
4058 $name = strtolower($name);
4059 if (isset(static::$svgCaseSensitiveAttributeMap[$name])) {
4060 $name = static::$svgCaseSensitiveAttributeMap[$name];
4061 }
4062 return $name;
4063 }
4064 /**
4065 * Normalize a MathML attribute name to its proper case and form.
4066 *
4067 * Note, all MathML element names are lowercase.
4068 *
4069 * @param string $name
4070 * The name of the attribute.
4071 *
4072 * @return string The normalized form of the attribute name.
4073 */
4074 public static function normalizeMathMlAttribute($name)
4075 {
4076 $name = strtolower($name);
4077 // Only one attribute has a mixed case form for MathML.
4078 if ($name == 'definitionurl') {
4079 $name = 'definitionURL';
4080 }
4081 return $name;
4082 }
4083 }
4084 namespace Kibo\Phast;
4085
4086 class PhastServices
4087 {
4088 /**
4089 * @param callable|null $getConfig
4090 */
4091 public static function serve(callable $getConfig = null)
4092 {
4093 $httpRequest = \Kibo\Phast\HTTP\Request::fromGlobals();
4094 $serviceRequest = \Kibo\Phast\Services\ServiceRequest::fromHTTPRequest($httpRequest);
4095 $serviceParams = $serviceRequest->getParams();
4096 if (defined('PHAST_SERVICE')) {
4097 $service = PHAST_SERVICE;
4098 } elseif (!isset($serviceParams['service'])) {
4099 http_response_code(404);
4100 exit;
4101 } else {
4102 $service = $serviceParams['service'];
4103 }
4104 if (isset($serviceParams['src']) && !headers_sent()) {
4105 http_response_code(301);
4106 header('Location: ' . $serviceParams['src']);
4107 header('Cache-Control: max-age=86400');
4108 }
4109 if ($getConfig === null) {
4110 $config = [];
4111 } else {
4112 $config = $getConfig();
4113 }
4114 $userConfig = new \Kibo\Phast\Environment\Configuration($config);
4115 $runtimeConfig = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration($userConfig)->withServiceRequest($serviceRequest)->getRuntimeConfig()->toArray();
4116 \Kibo\Phast\Logging\Log::init($runtimeConfig['logging'], $serviceRequest, $service);
4117 try {
4118 \Kibo\Phast\Services\ServiceRequest::setDefaultSerializationMode($runtimeConfig['serviceRequestFormat']);
4119 \Kibo\Phast\Logging\Log::info('Starting service');
4120 $response = (new \Kibo\Phast\Services\Factory())->make($service, $runtimeConfig)->serve($serviceRequest);
4121 \Kibo\Phast\Logging\Log::info('Service completed');
4122 } catch (\Kibo\Phast\Exceptions\UnauthorizedException $e) {
4123 \Kibo\Phast\Logging\Log::error('Unauthorized exception: {message}', ['message' => $e->getMessage()]);
4124 exit;
4125 } catch (\Kibo\Phast\Exceptions\ItemNotFoundException $e) {
4126 \Kibo\Phast\Logging\Log::error('Item not found: {message}', ['message' => $e->getMessage()]);
4127 exit;
4128 } catch (\Exception $e) {
4129 \Kibo\Phast\Logging\Log::critical('Unhandled exception: {type} Message: {message} File: {file} Line: {line}', ['type' => get_class($e), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
4130 exit;
4131 }
4132 header_remove('Location');
4133 header_remove('Cache-Control');
4134 self::output($httpRequest, $response, $runtimeConfig);
4135 }
4136 public static function output(\Kibo\Phast\HTTP\Request $request, \Kibo\Phast\HTTP\Response $response, array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
4137 {
4138 if (is_null($funcs)) {
4139 $funcs = new \Kibo\Phast\Common\ObjectifiedFunctions();
4140 }
4141 $headers = $response->getHeaders();
4142 $content = $response->getContent();
4143 if (!self::isIterable($content)) {
4144 $content = [$content];
4145 }
4146 $fp = fopen('php://output', 'wb');
4147 $zipping = false;
4148 if ($response->isCompressible() && self::shouldZip($request) && !empty($config['compressServiceResponse'])) {
4149 $zipping = @$funcs->stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE, ['level' => 9, 'window' => 31]);
4150 if ($zipping) {
4151 $headers['Content-Encoding'] = 'gzip';
4152 }
4153 }
4154 $maxAge = 86400 * 365;
4155 $headers += ['Vary' => 'Accept-Encoding', 'Cache-Control' => 'max-age=' . $maxAge, 'Expires' => self::formatHeaderDate(time() + $maxAge), 'X-Accel-Expires' => $maxAge, 'Access-Control-Allow-Origin' => '*', 'ETag' => self::generateETag($headers, $content), 'Last-Modified' => self::formatHeaderDate(time()), 'X-Content-Type-Options' => 'nosniff', 'Content-Security-Policy' => "default-src 'none'"];
4156 if (is_array($content) && !$zipping) {
4157 $headers['Content-Length'] = (string) array_sum(array_map('strlen', $content));
4158 }
4159 $funcs->http_response_code($response->getCode());
4160 foreach ($headers as $name => $value) {
4161 $funcs->header($name . ': ' . $value);
4162 }
4163 foreach ($content as $part) {
4164 fwrite($fp, $part);
4165 }
4166 fclose($fp);
4167 }
4168 private static function formatHeaderDate($time)
4169 {
4170 return gmdate('D, d M Y H:i:s', $time) . ' GMT';
4171 }
4172 private static function shouldZip(\Kibo\Phast\HTTP\Request $request)
4173 {
4174 return !$request->isCloudflare() && strpos($request->getHeader('Accept-Encoding'), 'gzip') !== false;
4175 }
4176 private static function generateETag(array $headers, $content)
4177 {
4178 $headersPart = http_build_query($headers);
4179 $contentPart = self::isIterable($content) ? uniqid() : $content;
4180 return '"' . md5($headersPart . "\0" . $contentPart) . '"';
4181 }
4182 private static function isIterable($thing)
4183 {
4184 return is_array($thing) || $thing instanceof \Iterator || $thing instanceof \Generator;
4185 }
4186 }
4187 namespace Kibo\Phast\Common;
4188
4189 class JSON
4190 {
4191 public static function encode($value)
4192 {
4193 return self::_encode($value, 0);
4194 }
4195 public static function prettyEncode($value)
4196 {
4197 return self::_encode($value, JSON_PRETTY_PRINT);
4198 }
4199 private static function _encode($value, $flags)
4200 {
4201 $flags |= JSON_UNESCAPED_SLASHES;
4202 if (version_compare(PHP_VERSION, '7.2.0', '<')) {
4203 return self::legacyEncode($value, $flags);
4204 }
4205 return json_encode($value, $flags | JSON_INVALID_UTF8_IGNORE | JSON_PARTIAL_OUTPUT_ON_ERROR);
4206 }
4207 private static function legacyEncode($value, $flags)
4208 {
4209 $result = json_encode($value, $flags);
4210 if ($result !== false || json_last_error() !== JSON_ERROR_UTF8) {
4211 return $result;
4212 }
4213 self::cleanUTF8($value);
4214 return json_encode($value, $flags | JSON_PARTIAL_OUTPUT_ON_ERROR);
4215 }
4216 private static function cleanUTF8(&$value)
4217 {
4218 if (is_array($value)) {
4219 array_walk_recursive($value, __METHOD__);
4220 } elseif (is_string($value)) {
4221 $value = preg_replace_callback('~
4222 [\\x00-\\x7F]++ # ASCII
4223 | [\\xC2-\\xDF][\\x80-\\xBF] # non-overlong 2-byte
4224 | \\xE0[\\xA0-\\xBF][\\x80-\\xBF] # excluding overlongs
4225 | [\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2} # straight 3-byte
4226 | \\xED[\\x80-\\x9F][\\x80-\\xBF] # excluding surrogates
4227 | \\xF0[\\x90-\\xBF][\\x80-\\xBF]{2} # planes 1-3
4228 | [\\xF1-\\xF3][\\x80-\\xBF]{3} # planes 4-15
4229 | \\xF4[\\x80-\\x8F][\\x80-\\xBF]{2} # plane 16
4230 | (.)
4231 ~xs', function ($match) {
4232 if (isset($match[1]) && strlen($match[1])) {
4233 return '';
4234 }
4235 return $match[0];
4236 }, $value);
4237 }
4238 }
4239 }
4240 namespace Kibo\Phast\Common;
4241
4242 class Base64url
4243 {
4244 public static function encode($data)
4245 {
4246 return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
4247 }
4248 public static function decode($data)
4249 {
4250 return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
4251 }
4252 public static function shortHash($data)
4253 {
4254 return self::encode(substr(sha1($data, true), 0, 8));
4255 }
4256 }
4257 namespace Kibo\Phast\Common;
4258
4259 class ObjectifiedFunctions
4260 {
4261 /**
4262 * @param string $name
4263 * @param array $arguments
4264 */
4265 public function __call($name, array $arguments)
4266 {
4267 if (isset($this->{$name}) && is_callable($this->{$name})) {
4268 $fn = $this->{$name};
4269 return $fn(...$arguments);
4270 }
4271 if (function_exists($name)) {
4272 return $name(...$arguments);
4273 }
4274 throw new \Kibo\Phast\Exceptions\UndefinedObjectifiedFunction("Undefined objectified function {$name}");
4275 }
4276 }
4277 namespace Kibo\Phast\Common;
4278
4279 class System
4280 {
4281 private $functions;
4282 public function __construct(\Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
4283 {
4284 if ($functions === null) {
4285 $functions = new \Kibo\Phast\Common\ObjectifiedFunctions();
4286 }
4287 $this->functions = $functions;
4288 }
4289 public function getUserId()
4290 {
4291 try {
4292 return (int) $this->functions->posix_geteuid();
4293 } catch (\Kibo\Phast\Exceptions\UndefinedObjectifiedFunction $e) {
4294 return 0;
4295 }
4296 }
4297 }
4298 namespace Kibo\Phast\Common;
4299
4300 class OutputBufferHandler
4301 {
4302 use \Kibo\Phast\Logging\LoggingTrait;
4303 const START_PATTERN = '~
4304 (
4305 \\s*+ <!doctype\\s++html> |
4306 \\s*+ <html> |
4307 \\s*+ <head> |
4308 \\s*+ <!--.*?-->
4309 )++
4310 ~xsiA';
4311 private $filterCb;
4312 /**
4313 * @var ?string
4314 */
4315 private $buffer = '';
4316 private $offset = 0;
4317 /**
4318 * @var integer
4319 */
4320 private $maxBufferSizeToApply;
4321 private $canceled = false;
4322 public function __construct($maxBufferSizeToApply, callable $filterCb)
4323 {
4324 $this->maxBufferSizeToApply = $maxBufferSizeToApply;
4325 $this->filterCb = $filterCb;
4326 }
4327 public function install()
4328 {
4329 $ignoreHandlers = ['default output handler', 'ob_gzhandler'];
4330 if (!array_diff(ob_list_handlers(), $ignoreHandlers)) {
4331 while (@ob_end_clean()) {
4332 }
4333 }
4334 ob_start([$this, 'handleChunk'], 2);
4335 ob_implicit_flush(1);
4336 }
4337 public function handleChunk($chunk, $phase)
4338 {
4339 if ($this->buffer === null) {
4340 return $chunk;
4341 }
4342 $this->buffer .= $chunk;
4343 if ($this->canceled) {
4344 return $this->stop();
4345 }
4346 if (strlen($this->buffer) > $this->maxBufferSizeToApply) {
4347 $this->logger()->info('Buffer exceeds max. size ({buffersize} bytes). Not applying', ['buffersize' => $this->maxBufferSizeToApply]);
4348 return $this->stop();
4349 }
4350 $output = '';
4351 if (preg_match(self::START_PATTERN, $this->buffer, $match, 0, $this->offset)) {
4352 $this->offset += strlen($match[0]);
4353 $output .= $match[0];
4354 }
4355 if ($phase & PHP_OUTPUT_HANDLER_FINAL) {
4356 $output .= $this->finalize();
4357 }
4358 if ($output !== '') {
4359 @header_remove('Content-Length');
4360 }
4361 return $output;
4362 }
4363 private function finalize()
4364 {
4365 $input = substr($this->buffer, $this->offset);
4366 $result = call_user_func($this->filterCb, $input, $this->buffer);
4367 $this->buffer = null;
4368 return $result;
4369 }
4370 private function stop()
4371 {
4372 $output = $this->buffer;
4373 $this->buffer = null;
4374 return $output;
4375 }
4376 public function cancel()
4377 {
4378 $this->canceled = true;
4379 }
4380 }
4381 namespace Kibo\Phast\ValueObjects;
4382
4383 class PhastJavaScript
4384 {
4385 /**
4386 * @var string
4387 */
4388 private $filename;
4389 /**
4390 * @var string
4391 */
4392 private $contents;
4393 /**
4394 * @var string
4395 */
4396 private $configKey;
4397 /**
4398 * @var mixed
4399 */
4400 private $config;
4401 /**
4402 * @var ObjectifiedFunctions
4403 */
4404 private $funcs;
4405 /**
4406 * @param string $filename
4407 * @param string $contents
4408 */
4409 private function __construct($filename, $contents)
4410 {
4411 $this->filename = $filename;
4412 $this->contents = $contents;
4413 }
4414 /**
4415 * @param string $filename
4416 * @param ObjectifiedFunctions|null $funcs
4417 */
4418 public static function fromFile($filename, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
4419 {
4420 $funcs = $funcs ? $funcs : new \Kibo\Phast\Common\ObjectifiedFunctions();
4421 $contents = $funcs->file_get_contents($filename);
4422 if ($contents === false) {
4423 throw new \RuntimeException("Failed to read script: {$filename}");
4424 }
4425 $contents = (new \Kibo\Phast\Common\JSMinifier($contents))->min();
4426 return new self($filename, $contents);
4427 }
4428 /**
4429 * @param string $filename
4430 * @param string $contents
4431 */
4432 public static function fromString($filename, $contents)
4433 {
4434 return new self($filename, $contents);
4435 }
4436 /**
4437 * @return string
4438 */
4439 public function getFilename()
4440 {
4441 return $this->filename;
4442 }
4443 /**
4444 * @return bool|string
4445 */
4446 public function getContents()
4447 {
4448 return $this->contents;
4449 }
4450 /**
4451 * @return string
4452 */
4453 public function getCacheSalt()
4454 {
4455 $hash = md5($this->getContents(), true);
4456 return substr(preg_replace('/^[a-z0-9]/i', '', base64_encode($hash)), 0, 16);
4457 }
4458 /**
4459 * @param string $configKey
4460 * @param mixed $config
4461 */
4462 public function setConfig($configKey, $config)
4463 {
4464 $this->configKey = $configKey;
4465 $this->config = $config;
4466 }
4467 /**
4468 * @return bool
4469 */
4470 public function hasConfig()
4471 {
4472 return isset($this->configKey);
4473 }
4474 /**
4475 * @return string
4476 */
4477 public function getConfigKey()
4478 {
4479 return $this->configKey;
4480 }
4481 /**
4482 * @return mixed
4483 */
4484 public function getConfig()
4485 {
4486 return $this->config;
4487 }
4488 }
4489 namespace Kibo\Phast\ValueObjects;
4490
4491 class Resource
4492 {
4493 const EXTENSION_TO_MIME_TYPE = array('gif' => 'image/gif', 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'bmp' => 'image/bmp', 'webp' => 'image/webp', 'svg' => 'image/svg+xml', 'css' => 'text/css', 'js' => 'application/javascript', 'json' => 'application/json');
4494 /**
4495 * @var URL
4496 */
4497 private $url;
4498 /**
4499 * @var Retriever
4500 */
4501 private $retriever;
4502 /**
4503 * @var string
4504 */
4505 private $content;
4506 /**
4507 * @var string
4508 */
4509 private $mimeType;
4510 /**
4511 * @var Resource[]
4512 */
4513 private $dependencies = array();
4514 private function __construct()
4515 {
4516 }
4517 public static function makeWithContent(\Kibo\Phast\ValueObjects\URL $url, $content, $mimeType = null)
4518 {
4519 $instance = new self();
4520 $instance->url = $url;
4521 $instance->mimeType = $mimeType;
4522 $instance->content = $content;
4523 return $instance;
4524 }
4525 public static function makeWithRetriever(\Kibo\Phast\ValueObjects\URL $url, \Kibo\Phast\Retrievers\Retriever $retriever, $mimeType = null)
4526 {
4527 $instance = new self();
4528 $instance->url = $url;
4529 $instance->mimeType = $mimeType;
4530 $instance->retriever = $retriever;
4531 return $instance;
4532 }
4533 /**
4534 * @return URL
4535 */
4536 public function getUrl()
4537 {
4538 return $this->url;
4539 }
4540 /**
4541 * @return string
4542 * @throws ItemNotFoundException
4543 */
4544 public function getContent()
4545 {
4546 if (!isset($this->content)) {
4547 $this->content = $this->retriever->retrieve($this->url);
4548 if ($this->content === false) {
4549 throw new \Kibo\Phast\Exceptions\ItemNotFoundException("Could not get {$this->url}");
4550 }
4551 }
4552 return $this->content;
4553 }
4554 /**
4555 * @return string|null
4556 */
4557 public function getMimeType()
4558 {
4559 if (!isset($this->mimeType)) {
4560 $ext = strtolower($this->url->getExtension());
4561 $ext2mime = self::EXTENSION_TO_MIME_TYPE;
4562 if (isset($ext2mime[$ext])) {
4563 $this->mimeType = self::EXTENSION_TO_MIME_TYPE[$ext];
4564 }
4565 }
4566 return $this->mimeType;
4567 }
4568 /**
4569 * @return bool|int
4570 */
4571 public function getSize()
4572 {
4573 if (isset($this->retriever) && method_exists($this->retriever, 'getSize')) {
4574 return $this->retriever->getSize($this->url);
4575 }
4576 if (isset($this->content)) {
4577 return strlen($this->content);
4578 }
4579 return false;
4580 }
4581 public function toDataURL()
4582 {
4583 $mime = $this->getMimeType();
4584 $content = $this->getContent();
4585 return "data:{$mime};base64," . base64_encode($content);
4586 }
4587 /**
4588 * @return Resource[]
4589 */
4590 public function getDependencies()
4591 {
4592 return $this->dependencies;
4593 }
4594 /**
4595 * @return bool|int
4596 */
4597 public function getCacheSalt()
4598 {
4599 return isset($this->retriever) ? $this->retriever->getCacheSalt($this->url) : 0;
4600 }
4601 /**
4602 * @param string $content
4603 * @param string|null $mimeType
4604 * @return Resource
4605 */
4606 public function withContent($content, $mimeType = null)
4607 {
4608 $new = clone $this;
4609 $new->content = $content;
4610 if (!is_null($mimeType)) {
4611 $new->mimeType = $mimeType;
4612 }
4613 return $new;
4614 }
4615 /**
4616 * @param Resource[] $dependencies
4617 * @return Resource
4618 */
4619 public function withDependencies(array $dependencies)
4620 {
4621 $new = clone $this;
4622 $new->dependencies = $dependencies;
4623 return $new;
4624 }
4625 }
4626 namespace Kibo\Phast\ValueObjects;
4627
4628 class Query implements \IteratorAggregate
4629 {
4630 private $tuples = array();
4631 /**
4632 * @param array $assoc
4633 * @return Query
4634 */
4635 public static function fromAssoc($assoc)
4636 {
4637 $result = new static();
4638 foreach ($assoc as $k => $v) {
4639 $result->add($k, $v);
4640 }
4641 return $result;
4642 }
4643 /**
4644 * @param string $string
4645 * @return Query
4646 */
4647 public static function fromString($string)
4648 {
4649 $result = new static();
4650 foreach (explode('&', $string) as $piece) {
4651 if ($piece === '') {
4652 continue;
4653 }
4654 $parts = array_map('urldecode', explode('=', $piece, 2));
4655 $result->add($parts[0], isset($parts[1]) ? $parts[1] : '');
4656 }
4657 return $result;
4658 }
4659 public function add($key, $value)
4660 {
4661 $this->tuples[] = [(string) $key, (string) $value];
4662 }
4663 public function get($key, $default = null)
4664 {
4665 foreach ($this->tuples as $tuple) {
4666 if ($tuple[0] === (string) $key) {
4667 return $tuple[1];
4668 }
4669 }
4670 return $default;
4671 }
4672 public function delete($key)
4673 {
4674 $this->tuples = array_filter($this->tuples, function ($tuple) use($key) {
4675 return $tuple[0] !== (string) $key;
4676 });
4677 }
4678 public function set($key, $value)
4679 {
4680 $this->delete($key);
4681 $this->add($key, $value);
4682 }
4683 public function has($key)
4684 {
4685 foreach ($this->tuples as $tuple) {
4686 if ($tuple[0] === (string) $key) {
4687 return true;
4688 }
4689 }
4690 return false;
4691 }
4692 public function update(\Kibo\Phast\ValueObjects\Query $source)
4693 {
4694 foreach ($source as $key => $value) {
4695 $this->delete($key);
4696 }
4697 foreach ($source as $key => $value) {
4698 $this->add($key, $value);
4699 }
4700 }
4701 public function toAssoc()
4702 {
4703 $assoc = [];
4704 foreach ($this->tuples as $tuple) {
4705 if (!array_key_exists($tuple[0], $assoc)) {
4706 $assoc[$tuple[0]] = $tuple[1];
4707 }
4708 }
4709 return $assoc;
4710 }
4711 public function getIterator()
4712 {
4713 foreach ($this->tuples as $tuple) {
4714 (yield $tuple[0] => $tuple[1]);
4715 }
4716 }
4717 public function pop($key)
4718 {
4719 $value = $this->get($key);
4720 $this->delete($key);
4721 return $value;
4722 }
4723 public function getAll($key)
4724 {
4725 $result = [];
4726 foreach ($this->tuples as $tuple) {
4727 if ($tuple[0] === (string) $key) {
4728 $result[] = $tuple[1];
4729 }
4730 }
4731 return $result;
4732 }
4733 }
4734 namespace Kibo\Phast\ValueObjects;
4735
4736 class URL
4737 {
4738 /**
4739 * @var string
4740 */
4741 private $scheme;
4742 /**
4743 * @var string
4744 */
4745 private $host;
4746 /**
4747 * @var string
4748 */
4749 private $port;
4750 /**
4751 * @var string
4752 */
4753 private $user;
4754 /**
4755 * @var string
4756 */
4757 private $pass;
4758 /**
4759 * @var string
4760 */
4761 private $path;
4762 /**
4763 * @var string
4764 */
4765 private $query;
4766 /**
4767 * @var string
4768 */
4769 private $fragment;
4770 /**
4771 * @param $string
4772 * @return URL
4773 */
4774 public static function fromString($string)
4775 {
4776 $components = parse_url($string);
4777 if (!$components) {
4778 return new self();
4779 }
4780 return self::fromArray($components);
4781 }
4782 /**
4783 * @param array $arr Should follow the format produced by parse_url()
4784 * @return URL
4785 * @see parse_url()
4786 */
4787 public static function fromArray(array $arr)
4788 {
4789 $url = new self();
4790 foreach ($arr as $key => $value) {
4791 $url->{$key} = $key == 'path' ? $url->normalizePath($value) : $value;
4792 }
4793 return $url;
4794 }
4795 /**
4796 * If $this can be interpreted as relative to $base,
4797 * will produce URL that is $base/$this.
4798 * Otherwise the returned URL will point to the same place as $this
4799 *
4800 * @param URL $base
4801 * @return URL
4802 *
4803 * @example this: www/htdocs + base: /var -> /var/www/htdocs
4804 * @example this: /var + base: http://example.com -> http://example.com/var
4805 * @example this: /var + base: /www -> /var
4806 */
4807 public function withBase(\Kibo\Phast\ValueObjects\URL $base)
4808 {
4809 $new = clone $this;
4810 foreach (['scheme', 'host', 'port', 'user', 'pass', 'path'] as $key) {
4811 if ($key == 'path') {
4812 $new->path = $this->resolvePath($base->path, $this->path);
4813 } elseif (!isset($this->{$key}) && isset($base->{$key})) {
4814 $new->{$key} = $base->{$key};
4815 } elseif (isset($this->{$key})) {
4816 break;
4817 }
4818 }
4819 return $new;
4820 }
4821 /**
4822 * Tells whether $this can be interpreted as at the same host as $url
4823 *
4824 * @param URL $url
4825 * @return bool
4826 */
4827 public function isLocalTo(\Kibo\Phast\ValueObjects\URL $url)
4828 {
4829 return empty($this->host) || $this->host === $url->host;
4830 }
4831 /**
4832 * @return string
4833 */
4834 public function toString()
4835 {
4836 $scheme = isset($this->scheme) ? $this->scheme . '://' : '';
4837 $host = isset($this->host) ? $this->host : '';
4838 $port = isset($this->port) ? ':' . $this->port : '';
4839 $user = isset($this->user) ? $this->user : '';
4840 $pass = isset($this->pass) ? ':' . $this->pass : '';
4841 $pass = $user || $pass ? "{$pass}@" : '';
4842 $path = isset($this->path) ? $this->getPath() : '';
4843 $query = isset($this->query) ? '?' . $this->query : '';
4844 $fragment = isset($this->fragment) ? '#' . $this->fragment : '';
4845 return "{$scheme}{$user}{$pass}{$host}{$port}{$path}{$query}{$fragment}";
4846 }
4847 private function normalizePath($path)
4848 {
4849 $stack = [];
4850 $head = null;
4851 foreach (explode('/', $path) as $part) {
4852 if ($part == '.' || $part == '') {
4853 continue;
4854 }
4855 if (!is_null($head) && $part == '..' && $head != '..') {
4856 array_pop($stack);
4857 $head = empty($stack) ? null : $stack[count($stack) - 1];
4858 } else {
4859 $stack[] = $head = $part;
4860 }
4861 }
4862 $normalized = substr($path, 0, 1) == '/' ? '/' : '';
4863 if (!empty($stack)) {
4864 $normalized .= join('/', $stack);
4865 $normalized .= substr($path, -1) == '/' ? '/' : '';
4866 }
4867 return $normalized;
4868 }
4869 private function resolvePath($base, $requested)
4870 {
4871 if (!$requested) {
4872 return $base;
4873 }
4874 if ($requested[0] == '/') {
4875 return $requested;
4876 }
4877 if (substr($base, -1, 1) == '/') {
4878 $usedBase = $base;
4879 } else {
4880 $usedBase = dirname($base);
4881 }
4882 return rtrim($usedBase, '/') . '/' . $requested;
4883 }
4884 /**
4885 * @return string
4886 */
4887 public function getScheme()
4888 {
4889 return $this->scheme;
4890 }
4891 /**
4892 * @return string
4893 */
4894 public function getHost()
4895 {
4896 return $this->host;
4897 }
4898 /**
4899 * @return string
4900 */
4901 public function getPort()
4902 {
4903 return $this->port;
4904 }
4905 /**
4906 * @return string
4907 */
4908 public function getUser()
4909 {
4910 return $this->user;
4911 }
4912 /**
4913 * @return string
4914 */
4915 public function getPass()
4916 {
4917 return $this->pass;
4918 }
4919 /**
4920 * @return string
4921 */
4922 public function getPath()
4923 {
4924 return $this->path;
4925 }
4926 /**
4927 * @return string
4928 */
4929 public function getQuery()
4930 {
4931 return $this->query;
4932 }
4933 /**
4934 * @return string
4935 */
4936 public function getExtension()
4937 {
4938 $matches = [];
4939 if (preg_match('/\\.([^.]*)$/', $this->path, $matches)) {
4940 return $matches[1];
4941 }
4942 return '';
4943 }
4944 /**
4945 * @return string
4946 */
4947 public function getFragment()
4948 {
4949 return $this->fragment;
4950 }
4951 /**
4952 * @param string $path
4953 * @return self
4954 */
4955 public function withPath($path)
4956 {
4957 $url = clone $this;
4958 $url->path = (string) $path;
4959 return $url;
4960 }
4961 /**
4962 * @param string|null $query
4963 * @return self
4964 */
4965 public function withQuery($query)
4966 {
4967 $url = clone $this;
4968 if ($query === null) {
4969 $url->query = null;
4970 } else {
4971 $url->query = (string) $query;
4972 }
4973 return $url;
4974 }
4975 /**
4976 * @return self
4977 */
4978 public function withoutQuery()
4979 {
4980 $url = clone $this;
4981 $url->query = null;
4982 return $url;
4983 }
4984 public function __toString()
4985 {
4986 return $this->toString();
4987 }
4988 public function rewrite(\Kibo\Phast\ValueObjects\URL $from, \Kibo\Phast\ValueObjects\URL $to)
4989 {
4990 $str_from = rtrim($from->toString(), '/');
4991 $str_to = rtrim($to->toString(), '/');
4992 return \Kibo\Phast\ValueObjects\URL::fromString(preg_replace('~^' . preg_quote($str_from, '~') . '(?=$|/)~', $str_to, $this->toString()));
4993 }
4994 }
4995 namespace Kibo\Phast\Logging;
4996
4997 class LogLevel
4998 {
4999 const EMERGENCY = 128;
5000 const ALERT = 64;
5001 const CRITICAL = 32;
5002 const ERROR = 16;
5003 const WARNING = 8;
5004 const NOTICE = 4;
5005 const INFO = 2;
5006 const DEBUG = 1;
5007 public static function toString($level)
5008 {
5009 switch ($level) {
5010 case self::EMERGENCY:
5011 return 'EMERGENCY';
5012 case self::ALERT:
5013 return 'ALERT';
5014 case self::CRITICAL:
5015 return 'CRITICAL';
5016 case self::ERROR:
5017 return 'ERROR';
5018 case self::WARNING:
5019 return 'WARNING';
5020 case self::NOTICE:
5021 return 'NOTICE';
5022 case self::INFO:
5023 return 'INFO';
5024 case self::DEBUG:
5025 return 'DEBUG';
5026 default:
5027 return 'UNKNOWN';
5028 }
5029 }
5030 }
5031 namespace Kibo\Phast\Logging;
5032
5033 class Log
5034 {
5035 /**
5036 * @var Logger
5037 */
5038 private static $logger;
5039 public static function setLogger(\Kibo\Phast\Logging\Logger $logger)
5040 {
5041 self::$logger = $logger;
5042 }
5043 public static function initWithDummy()
5044 {
5045 self::$logger = new \Kibo\Phast\Logging\Logger(new \Kibo\Phast\Logging\LogWriters\Dummy\Writer());
5046 }
5047 public static function init(array $config, \Kibo\Phast\Services\ServiceRequest $request, $service)
5048 {
5049 $writer = (new \Kibo\Phast\Logging\LogWriters\Factory())->make($config, $request);
5050 $logger = new \Kibo\Phast\Logging\Logger($writer);
5051 self::$logger = $logger->withContext(['documentRequestId' => $request->getDocumentRequestId(), 'requestId' => mt_rand(0, 99999999), 'service' => $service]);
5052 }
5053 /**
5054 * @return Logger
5055 */
5056 public static function get()
5057 {
5058 if (!isset(self::$logger)) {
5059 self::initWithDummy();
5060 }
5061 return self::$logger;
5062 }
5063 /**
5064 * @param array $context
5065 * @return Logger
5066 */
5067 public static function context(array $context)
5068 {
5069 return self::get()->withContext($context);
5070 }
5071 /**
5072 * System is unusable.
5073 *
5074 * @param string $message
5075 * @param array $context
5076 *
5077 * @return void
5078 */
5079 public static function emergency($message, array $context = array())
5080 {
5081 self::get()->emergency($message, $context);
5082 }
5083 /**
5084 * Action must be taken immediately.
5085 *
5086 * Example: Entire website down, database unavailable, etc. This should
5087 * trigger the SMS alerts and wake you up.
5088 *
5089 * @param string $message
5090 * @param array $context
5091 *
5092 * @return void
5093 */
5094 public static function alert($message, array $context = array())
5095 {
5096 self::get()->alert($message, $context);
5097 }
5098 /**
5099 * Critical conditions.
5100 *
5101 * Example: Application component unavailable, unexpected exception.
5102 *
5103 * @param string $message
5104 * @param array $context
5105 *
5106 * @return void
5107 */
5108 public static function critical($message, array $context = array())
5109 {
5110 self::get()->critical($message, $context);
5111 }
5112 /**
5113 * Runtime errors that do not require immediate action but should typically
5114 * be logged and monitored.
5115 *
5116 * @param string $message
5117 * @param array $context
5118 *
5119 * @return void
5120 */
5121 public static function error($message, array $context = array())
5122 {
5123 self::get()->error($message, $context);
5124 }
5125 /**
5126 * Exceptional occurrences that are not errors.
5127 *
5128 * Example: Use of deprecated APIs, poor use of an API, undesirable things
5129 * that are not necessarily wrong.
5130 *
5131 * @param string $message
5132 * @param array $context
5133 *
5134 * @return void
5135 */
5136 public static function warning($message, array $context = array())
5137 {
5138 self::get()->warning($message, $context);
5139 }
5140 /**
5141 * Normal but significant events.
5142 *
5143 * @param string $message
5144 * @param array $context
5145 *
5146 * @return void
5147 */
5148 public static function notice($message, array $context = array())
5149 {
5150 self::get()->notice($message, $context);
5151 }
5152 /**
5153 * Interesting events.
5154 *
5155 * Example: User logs in, SQL logs.
5156 *
5157 * @param string $message
5158 * @param array $context
5159 *
5160 * @return void
5161 */
5162 public static function info($message, array $context = array())
5163 {
5164 self::get()->info($message, $context);
5165 }
5166 /**
5167 * Detailed debug information.
5168 *
5169 * @param string $message
5170 * @param array $context
5171 *
5172 * @return void
5173 */
5174 public static function debug($message, array $context = array())
5175 {
5176 self::get()->debug($message, $context);
5177 }
5178 }
5179 namespace Kibo\Phast\Logging\LogWriters;
5180
5181 class Factory
5182 {
5183 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5184 {
5185 if (isset($config['logWriters']) && count($config['logWriters']) > 1) {
5186 $class = \Kibo\Phast\Logging\LogWriters\Composite\Writer::class;
5187 } elseif (isset($config['logWriters'])) {
5188 $config = array_pop($config['logWriters']);
5189 $class = $config['class'];
5190 } else {
5191 $class = $config['class'];
5192 }
5193 $package = \Kibo\Phast\Environment\Package::fromPackageClass($class);
5194 $writer = $package->getFactory()->make($config, $request);
5195 if (isset($config['levelMask'])) {
5196 $writer->setLevelMask($config['levelMask']);
5197 }
5198 return $writer;
5199 }
5200 }
5201 namespace Kibo\Phast\Logging\LogWriters\JSONLFile;
5202
5203 class Factory
5204 {
5205 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5206 {
5207 return new \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer($config['logRoot'], $request->getDocumentRequestId());
5208 }
5209 }
5210 namespace Kibo\Phast\Logging\LogWriters\Composite;
5211
5212 class Factory
5213 {
5214 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5215 {
5216 $writer = new \Kibo\Phast\Logging\LogWriters\Composite\Writer();
5217 $factory = new \Kibo\Phast\Logging\LogWriters\Factory();
5218 foreach ($config['logWriters'] as $writerConfig) {
5219 $writer->addWriter($factory->make($writerConfig, $request));
5220 }
5221 return $writer;
5222 }
5223 }
5224 namespace Kibo\Phast\Logging\LogWriters\RotatingTextFile;
5225
5226 class Factory
5227 {
5228 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5229 {
5230 return new \Kibo\Phast\Logging\LogWriters\RotatingTextFile\Writer($config);
5231 }
5232 }
5233 namespace Kibo\Phast\Logging\LogWriters\PHPError;
5234
5235 class Factory
5236 {
5237 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5238 {
5239 return new \Kibo\Phast\Logging\LogWriters\PHPError\Writer($config);
5240 }
5241 }
5242 namespace Kibo\Phast\Logging\Common;
5243
5244 trait JSONLFileLogTrait
5245 {
5246 /**
5247 * @var string
5248 */
5249 private $dir;
5250 /**
5251 * @var string
5252 */
5253 private $filename;
5254 /**
5255 * JSONLFileLogWriter constructor.
5256 * @param string $dir
5257 * @param string $suffix
5258 */
5259 public function __construct($dir, $suffix)
5260 {
5261 $this->dir = $dir;
5262 $suffix = preg_replace('/[^0-9A-Za-z_-]/', '', (string) $suffix);
5263 if (!empty($suffix)) {
5264 $suffix = '-' . $suffix;
5265 }
5266 $this->filename = $this->dir . '/log' . $suffix . '.jsonl';
5267 }
5268 }
5269 namespace Kibo\Phast\Logging;
5270
5271 class LogEntry implements \JsonSerializable
5272 {
5273 /**
5274 * @var int
5275 */
5276 private $level;
5277 /**
5278 * @var string
5279 */
5280 private $message;
5281 /**
5282 * @var array
5283 */
5284 private $context;
5285 /**
5286 * LogEntry constructor.
5287 * @param int $level
5288 * @param string $message
5289 * @param array $context
5290 */
5291 public function __construct($level, $message, array $context)
5292 {
5293 $this->level = (int) $level;
5294 $this->message = $message;
5295 $this->context = $context;
5296 }
5297 /**
5298 * @return int
5299 */
5300 public function getLevel()
5301 {
5302 return $this->level;
5303 }
5304 /**
5305 * @return string
5306 */
5307 public function getMessage()
5308 {
5309 return $this->message;
5310 }
5311 /**
5312 * @return array
5313 */
5314 public function getContext()
5315 {
5316 return $this->context;
5317 }
5318 public function toArray()
5319 {
5320 return ['level' => $this->level, 'message' => $this->message, 'context' => $this->context];
5321 }
5322 public function jsonSerialize()
5323 {
5324 return $this->toArray();
5325 }
5326 }
5327 namespace Kibo\Phast\Logging;
5328
5329 class Logger
5330 {
5331 /**
5332 * @var LogWriter
5333 */
5334 private $writer;
5335 /**
5336 * @var array
5337 */
5338 private $context = array();
5339 /**
5340 * @var ObjectifiedFunctions
5341 */
5342 private $functions;
5343 /**
5344 * Logger constructor.
5345 * @param LogWriter $writer
5346 */
5347 public function __construct(\Kibo\Phast\Logging\LogWriter $writer, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
5348 {
5349 $this->writer = $writer;
5350 $this->functions = is_null($functions) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $functions;
5351 }
5352 /**
5353 * Returns a new logger with default context
5354 * merged from the current logger and the passed array
5355 *
5356 * @param array $context
5357 * @return Logger
5358 */
5359 public function withContext(array $context)
5360 {
5361 $logger = clone $this;
5362 $logger->context = array_merge($this->context, $context);
5363 return $logger;
5364 }
5365 /**
5366 * System is unusable.
5367 *
5368 * @param string $message
5369 * @param array $context
5370 *
5371 * @return void
5372 */
5373 public function emergency($message, array $context = array())
5374 {
5375 $this->log(\Kibo\Phast\Logging\LogLevel::EMERGENCY, $message, $context);
5376 }
5377 /**
5378 * Action must be taken immediately.
5379 *
5380 * Example: Entire website down, database unavailable, etc. This should
5381 * trigger the SMS alerts and wake you up.
5382 *
5383 * @param string $message
5384 * @param array $context
5385 *
5386 * @return void
5387 */
5388 public function alert($message, array $context = array())
5389 {
5390 $this->log(\Kibo\Phast\Logging\LogLevel::ALERT, $message, $context);
5391 }
5392 /**
5393 * Critical conditions.
5394 *
5395 * Example: Application component unavailable, unexpected exception.
5396 *
5397 * @param string $message
5398 * @param array $context
5399 *
5400 * @return void
5401 */
5402 public function critical($message, array $context = array())
5403 {
5404 $this->log(\Kibo\Phast\Logging\LogLevel::CRITICAL, $message, $context);
5405 }
5406 /**
5407 * Runtime errors that do not require immediate action but should typically
5408 * be logged and monitored.
5409 *
5410 * @param string $message
5411 * @param array $context
5412 *
5413 * @return void
5414 */
5415 public function error($message, array $context = array())
5416 {
5417 $this->log(\Kibo\Phast\Logging\LogLevel::ERROR, $message, $context);
5418 }
5419 /**
5420 * Exceptional occurrences that are not errors.
5421 *
5422 * Example: Use of deprecated APIs, poor use of an API, undesirable things
5423 * that are not necessarily wrong.
5424 *
5425 * @param string $message
5426 * @param array $context
5427 *
5428 * @return void
5429 */
5430 public function warning($message, array $context = array())
5431 {
5432 $this->log(\Kibo\Phast\Logging\LogLevel::WARNING, $message, $context);
5433 }
5434 /**
5435 * Normal but significant events.
5436 *
5437 * @param string $message
5438 * @param array $context
5439 *
5440 * @return void
5441 */
5442 public function notice($message, array $context = array())
5443 {
5444 $this->log(\Kibo\Phast\Logging\LogLevel::NOTICE, $message, $context);
5445 }
5446 /**
5447 * Interesting events.
5448 *
5449 * Example: User logs in, SQL logs.
5450 *
5451 * @param string $message
5452 * @param array $context
5453 *
5454 * @return void
5455 */
5456 public function info($message, array $context = array())
5457 {
5458 $this->log(\Kibo\Phast\Logging\LogLevel::INFO, $message, $context);
5459 }
5460 /**
5461 * Detailed debug information.
5462 *
5463 * @param string $message
5464 * @param array $context
5465 *
5466 * @return void
5467 */
5468 public function debug($message, array $context = array())
5469 {
5470 $this->log(\Kibo\Phast\Logging\LogLevel::DEBUG, $message, $context);
5471 }
5472 protected function log($level, $message, array $context = array())
5473 {
5474 $context = array_merge(['timestamp' => $this->functions->microtime(true)], $context);
5475 $this->writer->writeEntry(new \Kibo\Phast\Logging\LogEntry($level, $message, array_merge($this->context, $context)));
5476 }
5477 }
5478 namespace Kibo\Phast\Logging;
5479
5480 interface LogReader
5481 {
5482 /**
5483 * Reads LogMessage objects
5484 *
5485 * @return \Generator
5486 */
5487 public function readEntries();
5488 }
5489 namespace Kibo\Phast\Logging;
5490
5491 trait LoggingTrait
5492 {
5493 protected function logger($method = null, $line = null)
5494 {
5495 $context = ['class' => get_class($this)];
5496 if (!is_null($method)) {
5497 $context['method'] = $method;
5498 }
5499 if (!is_null($line)) {
5500 $context['line'] = $line;
5501 }
5502 return \Kibo\Phast\Logging\Log::context($context);
5503 }
5504 }
5505 namespace Kibo\Phast\Logging\LogReaders\JSONLFile;
5506
5507 class Reader implements \Kibo\Phast\Logging\LogReader
5508 {
5509 use \Kibo\Phast\Logging\Common\JSONLFileLogTrait;
5510 public function readEntries()
5511 {
5512 $fp = @fopen($this->filename, 'r');
5513 while ($fp && ($row = @fgets($fp))) {
5514 $decoded = @json_decode($row, true);
5515 if (!$decoded) {
5516 continue;
5517 }
5518 (yield new \Kibo\Phast\Logging\LogEntry(@$decoded['level'], @$decoded['message'], @$decoded['context']));
5519 }
5520 @fclose($fp);
5521 @unlink($this->filename);
5522 }
5523 public function __destruct()
5524 {
5525 if (!($dir = @opendir($this->dir))) {
5526 return;
5527 }
5528 $tenMinutesAgo = time() - 600;
5529 while ($file = @readdir($dir)) {
5530 $filename = $this->dir . "/{$file}";
5531 if (preg_match('/\\.jsonl$/', $file) && @filectime($filename) < $tenMinutesAgo) {
5532 @unlink($filename);
5533 }
5534 }
5535 }
5536 }
5537 namespace Kibo\Phast\Logging;
5538
5539 interface LogWriter
5540 {
5541 /**
5542 * Set a bit-mask to filter entries that are actually written
5543 *
5544 * @param int $mask
5545 * @return void
5546 */
5547 public function setLevelMask($mask);
5548 /**
5549 * Write an entry to the log
5550 *
5551 * @param LogEntry $entry
5552 * @return void
5553 */
5554 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry);
5555 }
5556 namespace Kibo\Phast\Services;
5557
5558 interface ServiceFilter
5559 {
5560 /**
5561 * @param Resource $resource
5562 * @param array $request
5563 * @return Resource
5564 */
5565 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request);
5566 }
5567 namespace Kibo\Phast\Services;
5568
5569 class Factory
5570 {
5571 /**
5572 * @param string $service
5573 * @param array $config
5574 * @return BaseService
5575 * @throws ItemNotFoundException
5576 */
5577 public function make($service, array $config)
5578 {
5579 if (!preg_match('/^[a-z]+$/', $service)) {
5580 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Bad service');
5581 }
5582 $class = __NAMESPACE__ . '\\' . ucfirst($service) . '\\Factory';
5583 if (class_exists($class)) {
5584 return (new $class())->make($config);
5585 }
5586 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Unknown service');
5587 }
5588 }
5589 namespace Kibo\Phast\Services;
5590
5591 trait ServiceFactoryTrait
5592 {
5593 /**
5594 * @param array $config
5595 * @param $cacheNamespace
5596 * @return UniversalRetriever
5597 */
5598 public function makeUniversalCachingRetriever(array $config, $cacheNamespace)
5599 {
5600 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
5601 $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']));
5602 $retriever->addRetriever(new \Kibo\Phast\Retrievers\CachingRetriever(new \Kibo\Phast\Cache\File\Cache($config['cache'], $cacheNamespace), (new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($config)));
5603 return $retriever;
5604 }
5605 public function makeCachingServiceFilter(array $config, \Kibo\Phast\Filters\Service\CompositeFilter $compositeFilter, $cacheNamespace)
5606 {
5607 return new \Kibo\Phast\Filters\Service\CachingServiceFilter(new \Kibo\Phast\Cache\File\Cache($config['cache'], $cacheNamespace), $compositeFilter, new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']));
5608 }
5609 }
5610 namespace Kibo\Phast\Services\Bundler;
5611
5612 class Service
5613 {
5614 use \Kibo\Phast\Logging\LoggingTrait;
5615 /**
5616 * @var ServiceSignature
5617 */
5618 private $signature;
5619 /**
5620 * @var Retriever
5621 */
5622 private $cssRetriever;
5623 /**
5624 * @var ServiceFilter
5625 */
5626 private $cssFilter;
5627 /**
5628 * @var Retriever
5629 */
5630 private $jsRetriever;
5631 /**
5632 * @var ServiceFilter
5633 */
5634 private $jsFilter;
5635 private $tokenRefMaker;
5636 public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\Retrievers\Retriever $cssRetriever, \Kibo\Phast\Services\ServiceFilter $cssFilter, \Kibo\Phast\Retrievers\Retriever $jsRetriever, \Kibo\Phast\Services\ServiceFilter $jsFilter, \Kibo\Phast\Services\Bundler\TokenRefMaker $tokenRefMaker)
5637 {
5638 $this->signature = $signature;
5639 $this->cssRetriever = $cssRetriever;
5640 $this->cssFilter = $cssFilter;
5641 $this->jsRetriever = $jsRetriever;
5642 $this->jsFilter = $jsFilter;
5643 $this->tokenRefMaker = $tokenRefMaker;
5644 }
5645 /**
5646 * @param ServiceRequest $request
5647 * @return Response
5648 */
5649 public function serve(\Kibo\Phast\Services\ServiceRequest $request)
5650 {
5651 $response = new \Kibo\Phast\HTTP\Response();
5652 $response->setHeader('Content-Type', 'application/json');
5653 $response->setContent($this->streamResponse($request));
5654 return $response;
5655 }
5656 private function streamResponse(\Kibo\Phast\Services\ServiceRequest $request)
5657 {
5658 (yield '[');
5659 $firstRow = true;
5660 foreach ($this->getParams($request) as $key => $params) {
5661 if (isset($params['ref'])) {
5662 $ref = $params['ref'];
5663 $params = $this->tokenRefMaker->getParams($ref);
5664 if (!$params) {
5665 $this->logger()->error('Could not resolve ref {ref}', ['ref' => $ref]);
5666 (yield $this->generateJSONRow(['status' => 404], $firstRow));
5667 continue;
5668 }
5669 }
5670 if (!isset($params['src'])) {
5671 $this->logger()->error('No src found for set {key}', ['key' => $key]);
5672 (yield $this->generateJSONRow(['status' => 404], $firstRow));
5673 continue;
5674 }
5675 if (!$this->verifyParams($params)) {
5676 $this->logger()->error('Params verification failed for set {key}', ['key' => $key]);
5677 (yield $this->generateJSONRow(['status' => 401], $firstRow));
5678 continue;
5679 }
5680 list($retriever, $filter) = $this->getRetrieverAndFilter($params);
5681 $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever(\Kibo\Phast\ValueObjects\URL::fromString($params['src']), $retriever);
5682 try {
5683 $this->logger()->info('Applying for set {key}', ['key' => $key]);
5684 $filtered = $filter->apply($resource, $params);
5685 (yield $this->generateJSONRow(['status' => 200, 'content' => $filtered->getContent()], $firstRow));
5686 } catch (\Kibo\Phast\Exceptions\ItemNotFoundException $e) {
5687 $this->logger()->error('Could not find {url} for set {key}', ['url' => $params['src'], 'key' => $key]);
5688 (yield $this->generateJSONRow(['status' => 404], $firstRow));
5689 } catch (\Exception $e) {
5690 $this->logger()->critical('Unhandled exception for set {key}: {type} Message: {message} File: {file} Line: {line}', ['key' => $key, 'type' => get_class($e), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
5691 (yield $this->generateJSONRow(['status' => 500], $firstRow));
5692 }
5693 }
5694 (yield ']');
5695 }
5696 private function getParams(\Kibo\Phast\Services\ServiceRequest $request)
5697 {
5698 $params = $request->getParams();
5699 if (isset($params['src_0'])) {
5700 return (new \Kibo\Phast\Services\Bundler\BundlerParamsParser())->parse($request);
5701 }
5702 return (new \Kibo\Phast\Services\Bundler\ShortBundlerParamsParser())->parse($request);
5703 }
5704 private function verifyParams(array $params)
5705 {
5706 return \Kibo\Phast\Services\Bundler\ServiceParams::fromArray($params)->verify($this->signature);
5707 }
5708 private function getRetrieverAndFilter(array $params)
5709 {
5710 if (isset($params['isScript'])) {
5711 return [$this->jsRetriever, $this->jsFilter];
5712 }
5713 return [$this->cssRetriever, $this->cssFilter];
5714 }
5715 private function generateJSONRow(array $content, &$firstRow)
5716 {
5717 if (!$firstRow) {
5718 $prepend = ',';
5719 } else {
5720 $prepend = '';
5721 $firstRow = false;
5722 }
5723 return $prepend . \Kibo\Phast\Common\JSON::encode($content);
5724 }
5725 }
5726 namespace Kibo\Phast\Services\Bundler;
5727
5728 class BundlerParamsParser
5729 {
5730 public function parse(\Kibo\Phast\Services\ServiceRequest $request)
5731 {
5732 $result = [];
5733 foreach ($request->getParams() as $name => $value) {
5734 if (strpos($name, '_') !== false) {
5735 list($name, $key) = explode('_', $name, 2);
5736 $result[$key][$name] = $value;
5737 }
5738 }
5739 return $result;
5740 }
5741 }
5742 namespace Kibo\Phast\Services\Bundler;
5743
5744 class ShortBundlerParamsParser
5745 {
5746 public static function getParamsMappings()
5747 {
5748 return ['s' => 'src', 'i' => 'strip-imports', 'c' => 'cacheMarker', 't' => 'token', 'j' => 'isScript', 'r' => 'ref'];
5749 }
5750 public function parse(\Kibo\Phast\Services\ServiceRequest $request)
5751 {
5752 $query_string = $request->getHTTPRequest()->getQueryString();
5753 if (preg_match('/(^|&)f=/', $query_string)) {
5754 $query = \Kibo\Phast\ValueObjects\Query::fromString($this->unobfuscateQuery($query_string));
5755 } else {
5756 $query = $request->getQuery();
5757 }
5758 $query = $this->unshortenParams($query->getIterator());
5759 $query = $this->uncompressSrcs($query);
5760 $result = [];
5761 $current = null;
5762 foreach ($query as $key => $value) {
5763 if (in_array($key, ['src', 'ref'])) {
5764 if ($current) {
5765 $result[] = $current;
5766 }
5767 $current = [];
5768 }
5769 if ($current !== null) {
5770 $current[$key] = $value;
5771 }
5772 }
5773 if ($current) {
5774 $result[] = $current;
5775 }
5776 return $result;
5777 }
5778 private function unobfuscateQuery($query)
5779 {
5780 $query = str_rot13($query);
5781 if (strpos($query, '%2S') !== false) {
5782 $query = preg_replace_callback('/%../', function ($match) {
5783 return str_rot13($match[0]);
5784 }, $query);
5785 }
5786 return $query;
5787 }
5788 private function unshortenParams(\Generator $query)
5789 {
5790 $mappings = self::getParamsMappings();
5791 foreach ($query as $key => $value) {
5792 if (isset($mappings[$key])) {
5793 (yield $mappings[$key] => $value === '' ? '1' : $value);
5794 } else {
5795 (yield $key => $value);
5796 }
5797 }
5798 }
5799 private function uncompressSrcs(\Generator $query)
5800 {
5801 $lastUrl = '';
5802 foreach ($query as $key => $value) {
5803 if ($key === 'src') {
5804 $prefixLength = (int) base_convert(substr($value, 0, 2), 36, 10);
5805 $suffix = substr($value, 2);
5806 $value = substr($lastUrl, 0, $prefixLength) . $suffix;
5807 $lastUrl = $value;
5808 }
5809 (yield $key => $value);
5810 }
5811 }
5812 }
5813 namespace Kibo\Phast\Services\Bundler;
5814
5815 class TokenRefMaker
5816 {
5817 private $cache;
5818 public function __construct(\Kibo\Phast\Cache\Cache $cache)
5819 {
5820 $this->cache = $cache;
5821 }
5822 public function getRef($token, array $params)
5823 {
5824 $ref = \Kibo\Phast\Common\Base64url::shortHash(\Kibo\Phast\Common\JSON::encode($params));
5825 $cachedParams = $this->cache->get($ref);
5826 if (!$cachedParams) {
5827 $this->cache->set($ref, $params);
5828 $cachedParams = $this->cache->get($ref);
5829 }
5830 if ($cachedParams === $params) {
5831 return $ref;
5832 }
5833 }
5834 public function getParams($ref)
5835 {
5836 return $this->cache->get($ref);
5837 }
5838 }
5839 namespace Kibo\Phast\Services\Bundler;
5840
5841 class TokenRefMakerFactory
5842 {
5843 public function make(array $config)
5844 {
5845 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'token-refs');
5846 return new \Kibo\Phast\Services\Bundler\TokenRefMaker($cache);
5847 }
5848 }
5849 namespace Kibo\Phast\Services\Bundler;
5850
5851 class ServiceParams
5852 {
5853 /**
5854 * @var string
5855 */
5856 private $token;
5857 /**
5858 * @var array
5859 */
5860 private $params;
5861 private function __construct()
5862 {
5863 }
5864 /**
5865 * @param array $params
5866 * @return ServiceParams
5867 */
5868 public static function fromArray(array $params)
5869 {
5870 $instance = new self();
5871 if (isset($params['token'])) {
5872 $instance->token = $params['token'];
5873 unset($params['token']);
5874 }
5875 $instance->params = $params;
5876 return $instance;
5877 }
5878 /**
5879 * @param ServiceSignature $signature
5880 * @return ServiceParams
5881 */
5882 public function sign(\Kibo\Phast\Security\ServiceSignature $signature)
5883 {
5884 $new = new self();
5885 $new->token = $this->makeToken($signature);
5886 $new->params = $this->params;
5887 return $new;
5888 }
5889 /**
5890 * @param ServiceSignature $signature
5891 * @return bool
5892 */
5893 public function verify(\Kibo\Phast\Security\ServiceSignature $signature)
5894 {
5895 if (!isset($this->token)) {
5896 return false;
5897 }
5898 return $this->token == $this->makeToken($signature);
5899 }
5900 /**
5901 * @return mixed
5902 */
5903 public function toArray()
5904 {
5905 $params = $this->params;
5906 if ($this->token) {
5907 $params['token'] = $this->token;
5908 }
5909 return $params;
5910 }
5911 public function serialize()
5912 {
5913 return \Kibo\Phast\Common\JSON::encode($this->toArray());
5914 }
5915 private function makeToken(\Kibo\Phast\Security\ServiceSignature $signature)
5916 {
5917 $params = $this->params;
5918 if (isset($params['cacheMarker'])) {
5919 unset($params['cacheMarker']);
5920 }
5921 ksort($params);
5922 array_walk($params, function (&$item) {
5923 $item = (string) $item;
5924 });
5925 return $signature->sign(json_encode($params));
5926 }
5927 public function replaceByTokenRef(\Kibo\Phast\Services\Bundler\TokenRefMaker $maker)
5928 {
5929 if (!isset($this->token)) {
5930 return $this;
5931 }
5932 $ref = $maker->getRef($this->token, $this->toArray());
5933 return $ref ? \Kibo\Phast\Services\Bundler\ServiceParams::fromArray(['ref' => $ref]) : $this;
5934 }
5935 }
5936 namespace Kibo\Phast\Services\Bundler;
5937
5938 class Factory
5939 {
5940 use \Kibo\Phast\Services\ServiceFactoryTrait;
5941 public function make(array $config)
5942 {
5943 $cssServiceFactory = new \Kibo\Phast\Services\Css\Factory();
5944 $jsServiceFactory = new \Kibo\Phast\Services\Scripts\Factory();
5945 $cssFilter = $this->makeCachingServiceFilter($config, $cssServiceFactory->makeFilter($config), 'bundler-css');
5946 $jsFilter = $this->makeCachingServiceFilter($config, $jsServiceFactory->makeFilter($config), 'bundler-js');
5947 return new \Kibo\Phast\Services\Bundler\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), $cssServiceFactory->makeRetriever($config), $cssFilter, $jsServiceFactory->makeRetriever($config), $jsFilter, (new \Kibo\Phast\Services\Bundler\TokenRefMakerFactory())->make($config));
5948 }
5949 }
5950 namespace Kibo\Phast\Services\Css;
5951
5952 class Factory
5953 {
5954 use \Kibo\Phast\Services\ServiceFactoryTrait;
5955 public function make(array $config)
5956 {
5957 $cssComposite = $this->makeFilter($config);
5958 $composite = $this->makeCachingServiceFilter($config, $cssComposite, 'css-processing-2');
5959 return new \Kibo\Phast\Services\Css\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), [], $this->makeRetriever($config), $composite, $config);
5960 }
5961 public function makeRetriever(array $config)
5962 {
5963 return $this->makeUniversalCachingRetriever($config, 'css');
5964 }
5965 public function makeFilter(array $config)
5966 {
5967 return (new \Kibo\Phast\Filters\CSS\Composite\Factory())->make($config);
5968 }
5969 }
5970 namespace Kibo\Phast\Services\Diagnostics;
5971
5972 class Factory
5973 {
5974 public function make(array $config)
5975 {
5976 $logRoot = null;
5977 foreach ($config['logging']['logWriters'] as $writerConfig) {
5978 if ($writerConfig['class'] == \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer::class) {
5979 $logRoot = $writerConfig['logRoot'];
5980 break;
5981 }
5982 }
5983 return new \Kibo\Phast\Services\Diagnostics\Service($logRoot);
5984 }
5985 }
5986 namespace Kibo\Phast\Services\Diagnostics;
5987
5988 class Service
5989 {
5990 private $logRoot;
5991 public function __construct($logRoot)
5992 {
5993 $this->logRoot = $logRoot;
5994 }
5995 public function serve(\Kibo\Phast\Services\ServiceRequest $request)
5996 {
5997 $params = $request->getParams();
5998 if (isset($params['documentRequestId'])) {
5999 $items = $this->getRequestLog($params['documentRequestId']);
6000 } else {
6001 $items = $this->getSystemDiagnostics();
6002 }
6003 $response = new \Kibo\Phast\HTTP\Response();
6004 $response->setContent(\Kibo\Phast\Common\JSON::prettyEncode($items));
6005 $response->setHeader('Content-Type', 'application/json');
6006 return $response;
6007 }
6008 private function getRequestLog($requestId)
6009 {
6010 return iterator_to_array((new \Kibo\Phast\Logging\LogReaders\JSONLFile\Reader($this->logRoot, $requestId))->readEntries());
6011 }
6012 private function getSystemDiagnostics()
6013 {
6014 return (new \Kibo\Phast\Diagnostics\SystemDiagnostics())->run(require PHAST_CONFIG_FILE);
6015 }
6016 }
6017 namespace Kibo\Phast\Services\Scripts;
6018
6019 class Factory
6020 {
6021 use \Kibo\Phast\Services\ServiceFactoryTrait;
6022 public function make(array $config)
6023 {
6024 $cachedComposite = $this->makeFilter($config);
6025 return new \Kibo\Phast\Services\Scripts\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), $config['scripts']['whitelist'], $this->makeRetriever($config), $this->makeCachingServiceFilter($config, $cachedComposite, 'scripts-minified'), $config);
6026 }
6027 public function makeRetriever(array $config)
6028 {
6029 return $this->makeUniversalCachingRetriever($config, 'scripts');
6030 }
6031 public function makeFilter(array $config)
6032 {
6033 $filter = new \Kibo\Phast\Filters\Service\CompositeFilter();
6034 $filter->addFilter(new \Kibo\Phast\Filters\Text\Decode\Filter());
6035 $filter->addFilter(new \Kibo\Phast\Filters\JavaScript\Minification\JSMinifierFilter(@$config['scripts']['removeLicenseHeaders']));
6036 return $filter;
6037 }
6038 }
6039 namespace Kibo\Phast\Services\Images;
6040
6041 class Factory
6042 {
6043 public function make(array $config)
6044 {
6045 if ($config['images']['api-mode']) {
6046 $retriever = new \Kibo\Phast\Retrievers\PostDataRetriever();
6047 } else {
6048 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
6049 $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']));
6050 $retriever->addRetriever((new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($config));
6051 }
6052 return new \Kibo\Phast\Services\Images\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), $config['images']['whitelist'], $retriever, (new \Kibo\Phast\Filters\Image\Composite\Factory($config))->make(), $config);
6053 }
6054 }
6055 namespace Kibo\Phast\Services;
6056
6057 abstract class BaseService
6058 {
6059 use \Kibo\Phast\Logging\LoggingTrait;
6060 /**
6061 * @var ServiceSignature
6062 */
6063 protected $signature;
6064 /**
6065 * @var string[]
6066 */
6067 protected $whitelist = array();
6068 /**
6069 * @var Retriever
6070 */
6071 protected $retriever;
6072 /**
6073 * @var ServiceFilter
6074 */
6075 protected $filter;
6076 /**
6077 * @var array
6078 */
6079 protected $config;
6080 /**
6081 * BaseService constructor.
6082 * @param ServiceSignature $signature
6083 * @param array $whitelist
6084 * @param Retriever $retriever
6085 * @param ServiceFilter $filter
6086 * @param array $config
6087 */
6088 public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, array $whitelist, \Kibo\Phast\Retrievers\Retriever $retriever, \Kibo\Phast\Services\ServiceFilter $filter, array $config)
6089 {
6090 $this->signature = $signature;
6091 $this->whitelist = $whitelist;
6092 $this->retriever = $retriever;
6093 $this->filter = $filter;
6094 $this->config = $config;
6095 }
6096 /**
6097 * @param ServiceRequest $request
6098 * @return Response
6099 */
6100 public function serve(\Kibo\Phast\Services\ServiceRequest $request)
6101 {
6102 $this->validateRequest($request);
6103 $request = $this->getParams($request);
6104 $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever(\Kibo\Phast\ValueObjects\URL::fromString(isset($request['src']) ? $request['src'] : ''), $this->retriever);
6105 $filtered = $this->filter->apply($resource, $request);
6106 return $this->makeResponse($filtered, $request);
6107 }
6108 /**
6109 * @param ServiceRequest $request
6110 * @return array
6111 */
6112 protected function getParams(\Kibo\Phast\Services\ServiceRequest $request)
6113 {
6114 return $request->getParams();
6115 }
6116 /**
6117 * @param Resource $resource
6118 * @param array $request
6119 * @return Response
6120 */
6121 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
6122 {
6123 $response = new \Kibo\Phast\HTTP\Response();
6124 $response->setContent($resource->getContent());
6125 return $response;
6126 }
6127 protected function validateRequest(\Kibo\Phast\Services\ServiceRequest $request)
6128 {
6129 $this->validateIntegrity($request);
6130 try {
6131 $this->validateToken($request);
6132 } catch (\Kibo\Phast\Exceptions\UnauthorizedException $e) {
6133 $this->validateWhitelisted($request);
6134 }
6135 }
6136 protected function validateIntegrity(\Kibo\Phast\Services\ServiceRequest $request)
6137 {
6138 $params = $request->getParams();
6139 if (!isset($params['src'])) {
6140 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('No source is set!');
6141 }
6142 }
6143 protected function validateToken(\Kibo\Phast\Services\ServiceRequest $request)
6144 {
6145 if (!$request->verify($this->signature)) {
6146 throw new \Kibo\Phast\Exceptions\UnauthorizedException('Invalid token in request: ' . $request->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY));
6147 }
6148 }
6149 protected function validateWhitelisted(\Kibo\Phast\Services\ServiceRequest $request)
6150 {
6151 $params = $request->getParams();
6152 foreach ($this->whitelist as $pattern) {
6153 if (preg_match($pattern, $params['src'])) {
6154 return;
6155 }
6156 }
6157 throw new \Kibo\Phast\Exceptions\UnauthorizedException('Not allowed url: ' . $params['src']);
6158 }
6159 }
6160 namespace Kibo\Phast\Services;
6161
6162 class ServiceRequest
6163 {
6164 const FORMAT_QUERY = 1;
6165 const FORMAT_PATH = 2;
6166 private static $defaultSerializationMode = self::FORMAT_PATH;
6167 /**
6168 * @var string
6169 */
6170 private static $propagatedSwitches = '';
6171 /**
6172 * @var Switches
6173 */
6174 private static $switches;
6175 /**
6176 * @var string
6177 */
6178 private static $documentRequestId;
6179 /**
6180 * @var Request
6181 */
6182 private $httpRequest;
6183 /**
6184 * @var URL
6185 */
6186 private $url;
6187 /**
6188 * @var Query
6189 */
6190 private $query;
6191 /**
6192 * @var string
6193 */
6194 private $token;
6195 public function __construct()
6196 {
6197 if (!isset(self::$switches)) {
6198 self::$switches = new \Kibo\Phast\Environment\Switches();
6199 }
6200 $this->query = new \Kibo\Phast\ValueObjects\Query();
6201 }
6202 public static function resetRequestState()
6203 {
6204 self::$defaultSerializationMode = self::FORMAT_PATH;
6205 self::$propagatedSwitches = '';
6206 self::$switches = null;
6207 self::$documentRequestId = null;
6208 }
6209 public static function setDefaultSerializationMode($mode)
6210 {
6211 self::$defaultSerializationMode = $mode;
6212 }
6213 public static function getDefaultSerializationMode()
6214 {
6215 return self::$defaultSerializationMode;
6216 }
6217 public static function fromHTTPRequest(\Kibo\Phast\HTTP\Request $request)
6218 {
6219 $query = $request->getQuery();
6220 if ($query->get('src')) {
6221 $query->set('src', preg_replace('~^hxxp(?=s?://)~', 'http', $query->get('src')));
6222 }
6223 $pathInfo = $request->getPathInfo();
6224 if ($pathParams = self::parseBase64PathInfo($pathInfo)) {
6225 $query->update($pathParams);
6226 } elseif ($pathInfo) {
6227 $query->update(self::parsePathInfo($pathInfo));
6228 }
6229 $instance = new self();
6230 self::$switches = new \Kibo\Phast\Environment\Switches();
6231 $instance->httpRequest = $request;
6232 if ($request->getCookie('phast')) {
6233 self::$switches = \Kibo\Phast\Environment\Switches::fromString($request->getCookie('phast'));
6234 }
6235 if ($token = $query->pop('token')) {
6236 $instance->token = $token;
6237 }
6238 $instance->query = $query;
6239 if ($query->get('phast')) {
6240 self::$propagatedSwitches = $query->get('phast');
6241 $paramsSwitches = \Kibo\Phast\Environment\Switches::fromString($query->get('phast'));
6242 self::$switches = self::$switches->merge($paramsSwitches);
6243 }
6244 if ($query->get('documentRequestId')) {
6245 self::$documentRequestId = $query->get('documentRequestId');
6246 } else {
6247 self::$documentRequestId = (string) mt_rand(0, 999999999);
6248 }
6249 return $instance;
6250 }
6251 public function hasRequestSwitchesSet()
6252 {
6253 return !empty(self::$propagatedSwitches);
6254 }
6255 /**
6256 * @return Switches
6257 */
6258 public function getSwitches()
6259 {
6260 return self::$switches;
6261 }
6262 /**
6263 * @return array
6264 */
6265 public function getParams()
6266 {
6267 return $this->query->toAssoc();
6268 }
6269 /**
6270 * @return Query
6271 */
6272 public function getQuery()
6273 {
6274 return $this->query;
6275 }
6276 /**
6277 * @return Request
6278 */
6279 public function getHTTPRequest()
6280 {
6281 return $this->httpRequest;
6282 }
6283 /**
6284 * @return string
6285 */
6286 public function getDocumentRequestId()
6287 {
6288 return self::$documentRequestId;
6289 }
6290 /**
6291 * @param array $params
6292 * @return ServiceRequest
6293 */
6294 public function withParams(array $params)
6295 {
6296 $result = clone $this;
6297 $result->query = \Kibo\Phast\ValueObjects\Query::fromAssoc($params);
6298 return $result;
6299 }
6300 /**
6301 * @param URL $url
6302 * @return ServiceRequest
6303 */
6304 public function withUrl(\Kibo\Phast\ValueObjects\URL $url)
6305 {
6306 $result = clone $this;
6307 $result->url = $url;
6308 return $result;
6309 }
6310 /**
6311 * @param ServiceSignature $signature
6312 * @return ServiceRequest
6313 */
6314 public function sign(\Kibo\Phast\Security\ServiceSignature $signature)
6315 {
6316 $token = $signature->sign($this->getVerificationString());
6317 $result = clone $this;
6318 $result->token = $token;
6319 return $result;
6320 }
6321 /**
6322 * @param ServiceSignature $signature
6323 * @return bool
6324 */
6325 public function verify(\Kibo\Phast\Security\ServiceSignature $signature)
6326 {
6327 return $signature->verify($this->token, $this->getVerificationString()) || $signature->verify($this->token, $this->getVerificationStringWithoutStemSuffix());
6328 }
6329 private static function parsePathInfo($string)
6330 {
6331 $values = new \Kibo\Phast\ValueObjects\Query();
6332 $parts = explode('/', $string);
6333 foreach ($parts as $part) {
6334 if ($part === '') {
6335 continue;
6336 }
6337 $pair = explode('=', $part);
6338 if (isset($pair[1])) {
6339 $values->set($pair[0], self::decodeSingleValue($pair[1]));
6340 } elseif (preg_match('/^__p__(@[1-9][0-9]*x)?\\./', $pair[0], $match)) {
6341 if (!empty($match[1]) && $values->has('src')) {
6342 $values->set('src', self::appendStemSuffix($values->get('src'), $match[1]));
6343 }
6344 break;
6345 } else {
6346 $values->set('src', self::decodeSingleValue($pair[0]));
6347 }
6348 }
6349 return $values;
6350 }
6351 private static function decodeSingleValue($value)
6352 {
6353 return urldecode(str_replace('-', '%', $value));
6354 }
6355 private static function appendStemSuffix($src, $suffix)
6356 {
6357 $url = \Kibo\Phast\ValueObjects\URL::fromString($src);
6358 $path = preg_replace_callback('/\\.\\w+$/', function ($match) use($suffix) {
6359 return $suffix . $match[0];
6360 }, $url->getPath());
6361 return $url->withPath($path)->toString();
6362 }
6363 private static function parseBase64PathInfo($string)
6364 {
6365 if (!preg_match('~^/([a-z0-9_-]+)\\.q\\.js$~i', $string, $match)) {
6366 return null;
6367 }
6368 return \Kibo\Phast\ValueObjects\Query::fromString(\Kibo\Phast\Common\Base64url::decode($match[1]));
6369 }
6370 /**
6371 * @param callable $paramsFilter
6372 * @return string
6373 */
6374 private function getVerificationString($paramsFilter = null)
6375 {
6376 $params = $this->getAllParams();
6377 if ($paramsFilter) {
6378 $params = $paramsFilter($params);
6379 }
6380 ksort($params);
6381 return http_build_query($params);
6382 }
6383 private function getVerificationStringWithoutStemSuffix()
6384 {
6385 return $this->getVerificationString(function ($params) {
6386 if (isset($params['src'])) {
6387 $params['src'] = $this->stripStemSuffix($params['src']);
6388 }
6389 return $params;
6390 });
6391 }
6392 private function stripStemSuffix($src)
6393 {
6394 $url = \Kibo\Phast\ValueObjects\URL::fromString($src);
6395 $path = preg_replace('/@[1-9][0-9]*x(?=\\.\\w+$)/', '', $url->getPath());
6396 return $url->withPath($path)->toString();
6397 }
6398 public function serialize($format = null)
6399 {
6400 $params = $this->getAllParams();
6401 if ($this->token) {
6402 $params['token'] = $this->token;
6403 }
6404 if (is_null($format)) {
6405 $format = self::$defaultSerializationMode;
6406 }
6407 if ($format == self::FORMAT_PATH) {
6408 return $this->serializeToPathFormat($params);
6409 }
6410 return $this->serializeToQueryFormat($params);
6411 }
6412 private function getAllParams()
6413 {
6414 $urlParams = [];
6415 if ($this->url) {
6416 parse_str($this->url->getQuery(), $urlParams);
6417 }
6418 $params = array_merge($urlParams, $this->query->toAssoc());
6419 if (!empty(self::$propagatedSwitches)) {
6420 $params['phast'] = self::$propagatedSwitches;
6421 }
6422 if (self::$switches->isOn(\Kibo\Phast\Environment\Switches::SWITCH_DIAGNOSTICS)) {
6423 $params['documentRequestId'] = self::$documentRequestId;
6424 }
6425 return $params;
6426 }
6427 private function serializeToQueryFormat(array $params)
6428 {
6429 $encoded = http_build_query($params);
6430 if (!isset($this->url)) {
6431 return $encoded;
6432 }
6433 $serialized = preg_replace('~\\?.*~', '', (string) $this->url);
6434 if (self::$defaultSerializationMode === self::FORMAT_PATH && !preg_match('~/$~', $serialized)) {
6435 $serialized .= '/' . $this->getDummyFilename($params);
6436 }
6437 return $serialized . '?' . $encoded;
6438 }
6439 /** @return string */
6440 private function serializeToPathFormat(array $params)
6441 {
6442 $encodedSrc = null;
6443 $values = [];
6444 foreach (explode('&', http_build_query($params)) as $element) {
6445 list($key, $value) = explode('=', $element, 2);
6446 $encodedValue = str_replace(['-', '%'], ['%2D', '-'], $value);
6447 if ($key == 'src') {
6448 $encodedSrc = $encodedValue;
6449 } else {
6450 $values[] = $key . '=' . $encodedValue;
6451 }
6452 }
6453 if ($encodedSrc) {
6454 array_unshift($values, $encodedSrc);
6455 }
6456 $params = '/' . join('/', $values) . '/' . $this->getDummyFilename($params);
6457 if (isset($this->url)) {
6458 return preg_replace(['~\\?.*~', '~/$~'], '', $this->url) . $params;
6459 }
6460 return $params;
6461 }
6462 private function getDummyFilename(array $params)
6463 {
6464 return '__p__.' . $this->getDummyExtension($params);
6465 }
6466 private function getDummyExtension(array $params)
6467 {
6468 $default = 'js';
6469 if (empty($params['src'])) {
6470 return $default;
6471 }
6472 $url = \Kibo\Phast\ValueObjects\URL::fromString($params['src']);
6473 $ext = strtolower($url->getExtension());
6474 if (preg_match('/^(jpe?g|gif|png|js|css)$/', $ext)) {
6475 return $ext;
6476 }
6477 return $default;
6478 }
6479 }
6480 namespace Kibo\Phast\Security;
6481
6482 class ServiceSignature
6483 {
6484 const AUTO_TOKEN_SIZE = 128;
6485 const SIGNATURE_LENGTH = 16;
6486 /**
6487 * @var Cache
6488 */
6489 private $cache;
6490 /**
6491 * @var array
6492 */
6493 private $identities;
6494 /**
6495 * ServiceSignature constructor.
6496 *
6497 * @param Cache $cache
6498 */
6499 public function __construct(\Kibo\Phast\Cache\Cache $cache)
6500 {
6501 $this->cache = $cache;
6502 }
6503 /**
6504 * @param string|array $identities
6505 */
6506 public function setIdentities($identities)
6507 {
6508 if (is_string($identities)) {
6509 $this->identities = ['' => $identities];
6510 } else {
6511 $this->identities = $identities;
6512 }
6513 }
6514 /**
6515 * @return string
6516 */
6517 public function getCacheSalt()
6518 {
6519 $identities = $this->getIdentities();
6520 return md5(join('=>', array_merge(array_keys($identities), array_values($identities))));
6521 }
6522 public function sign($value)
6523 {
6524 $identities = $this->getIdentities();
6525 $users = array_keys($identities);
6526 list($user, $token) = [array_shift($users), array_shift($identities)];
6527 return $user . substr(md5($token . $value), 0, self::SIGNATURE_LENGTH);
6528 }
6529 public function verify($signature, $value)
6530 {
6531 $user = substr($signature, 0, -self::SIGNATURE_LENGTH);
6532 $identities = $this->getIdentities();
6533 if (!isset($identities[$user])) {
6534 return false;
6535 }
6536 $token = $identities[$user];
6537 $signer = new self($this->cache);
6538 $signer->setIdentities([$user => $token]);
6539 return $signature === $signer->sign($value);
6540 }
6541 public static function generateToken()
6542 {
6543 $token = '';
6544 for ($i = 0; $i < self::AUTO_TOKEN_SIZE; $i++) {
6545 $token .= chr(mt_rand(33, 126));
6546 }
6547 return $token;
6548 }
6549 private function getIdentities()
6550 {
6551 if (!isset($this->identities)) {
6552 $token = $this->cache->get('security-token', function () {
6553 return self::generateToken();
6554 });
6555 $this->identities = ['' => $token];
6556 }
6557 return $this->identities;
6558 }
6559 }
6560 namespace Kibo\Phast\Security;
6561
6562 class ServiceSignatureFactory
6563 {
6564 public function make(array $config)
6565 {
6566 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'signature');
6567 $signature = new \Kibo\Phast\Security\ServiceSignature($cache);
6568 if (isset($config['securityToken'])) {
6569 $signature->setIdentities($config['securityToken']);
6570 }
6571 return $signature;
6572 }
6573 }
6574 namespace Kibo\Phast\Exceptions;
6575
6576 class LogicException extends \LogicException
6577 {
6578 }
6579 namespace Kibo\Phast\Exceptions;
6580
6581 class RuntimeException extends \RuntimeException
6582 {
6583 }
6584 namespace Kibo\Phast\Exceptions;
6585
6586 class CachedExceptionException extends \Exception
6587 {
6588 }
6589 namespace Kibo\Phast\Exceptions;
6590
6591 class ItemNotFoundException extends \Exception
6592 {
6593 /**
6594 * @var URL
6595 */
6596 private $url;
6597 public function __construct($message = '', $code = 0, \Throwable $previous = null, \Kibo\Phast\ValueObjects\URL $failed = null)
6598 {
6599 parent::__construct($message, $code, $previous);
6600 $this->url = $failed;
6601 }
6602 /**
6603 * @return URL
6604 */
6605 public function getUrl()
6606 {
6607 return $this->url;
6608 }
6609 }
6610 namespace Kibo\Phast\Exceptions;
6611
6612 class UnauthorizedException extends \Exception
6613 {
6614 }
6615 namespace Kibo\Phast\Exceptions;
6616
6617 class UndefinedObjectifiedFunction extends \RuntimeException
6618 {
6619 }
6620 namespace JSMin;
6621
6622 class UnterminatedCommentException extends \Exception
6623 {
6624 }
6625 namespace JSMin;
6626
6627 class UnterminatedRegExpException extends \Exception
6628 {
6629 }
6630 namespace JSMin;
6631
6632 /**
6633 * JSMin.php - modified PHP implementation of Douglas Crockford's JSMin.
6634 *
6635 * <code>
6636 * $minifiedJs = JSMin::minify($js);
6637 * </code>
6638 *
6639 * This is a modified port of jsmin.c. Improvements:
6640 *
6641 * Does not choke on some regexp literals containing quote characters. E.g. /'/
6642 *
6643 * Spaces are preserved after some add/sub operators, so they are not mistakenly
6644 * converted to post-inc/dec. E.g. a + ++b -> a+ ++b
6645 *
6646 * Preserves multi-line comments that begin with /*!
6647 *
6648 * PHP 5 or higher is required.
6649 *
6650 * Permission is hereby granted to use this version of the library under the
6651 * same terms as jsmin.c, which has the following license:
6652 *
6653 * --
6654 * Copyright (c) 2002 Douglas Crockford (www.crockford.com)
6655 *
6656 * Permission is hereby granted, free of charge, to any person obtaining a copy of
6657 * this software and associated documentation files (the "Software"), to deal in
6658 * the Software without restriction, including without limitation the rights to
6659 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
6660 * of the Software, and to permit persons to whom the Software is furnished to do
6661 * so, subject to the following conditions:
6662 *
6663 * The above copyright notice and this permission notice shall be included in all
6664 * copies or substantial portions of the Software.
6665 *
6666 * The Software shall be used for Good, not Evil.
6667 *
6668 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6669 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
6670 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
6671 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
6672 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
6673 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
6674 * SOFTWARE.
6675 * --
6676 *
6677 * @package JSMin
6678 * @author Ryan Grove <ryan@wonko.com> (PHP port)
6679 * @author Steve Clay <steve@mrclay.org> (modifications + cleanup)
6680 * @author Andrea Giammarchi <http://www.3site.eu> (spaceBeforeRegExp)
6681 * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
6682 * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
6683 * @license http://opensource.org/licenses/mit-license.php MIT License
6684 * @link http://code.google.com/p/jsmin-php/
6685 */
6686 class JSMin
6687 {
6688 const ACTION_KEEP_A = 1;
6689 const ACTION_DELETE_A = 2;
6690 const ACTION_DELETE_A_B = 3;
6691 protected $a = "\n";
6692 protected $b = '';
6693 protected $input = '';
6694 protected $inputIndex = 0;
6695 protected $inputLength = 0;
6696 protected $lookAhead = null;
6697 protected $output = '';
6698 protected $lastByteOut = '';
6699 protected $keptComment = '';
6700 /**
6701 * Minify Javascript.
6702 *
6703 * @param string $js Javascript to be minified
6704 *
6705 * @return string
6706 */
6707 public static function minify($js)
6708 {
6709 $jsmin = new \JSMin\JSMin($js);
6710 return $jsmin->min();
6711 }
6712 /**
6713 * @param string $input
6714 */
6715 public function __construct($input)
6716 {
6717 $this->input = $input;
6718 }
6719 /**
6720 * Perform minification, return result
6721 *
6722 * @return string
6723 */
6724 public function min()
6725 {
6726 if ($this->output !== '') {
6727 // min already run
6728 return $this->output;
6729 }
6730 $mbIntEnc = null;
6731 if (function_exists('mb_strlen') && (int) ini_get('mbstring.func_overload') & 2) {
6732 $mbIntEnc = mb_internal_encoding();
6733 mb_internal_encoding('8bit');
6734 }
6735 if (isset($this->input[0]) && $this->input[0] === "\357") {
6736 $this->input = substr($this->input, 3);
6737 }
6738 $this->input = str_replace("\r\n", "\n", $this->input);
6739 $this->inputLength = strlen($this->input);
6740 $this->action(self::ACTION_DELETE_A_B);
6741 while ($this->a !== null) {
6742 // determine next command
6743 $command = self::ACTION_KEEP_A;
6744 // default
6745 if ($this->isWhiteSpace($this->a)) {
6746 if (($this->lastByteOut === '+' || $this->lastByteOut === '-') && $this->b === $this->lastByteOut) {
6747 // Don't delete this space. If we do, the addition/subtraction
6748 // could be parsed as a post-increment
6749 } elseif (!$this->isAlphaNum($this->b)) {
6750 $command = self::ACTION_DELETE_A;
6751 }
6752 } elseif ($this->isLineTerminator($this->a)) {
6753 if ($this->isWhiteSpace($this->b)) {
6754 $command = self::ACTION_DELETE_A_B;
6755 // in case of mbstring.func_overload & 2, must check for null b,
6756 // otherwise mb_strpos will give WARNING
6757 } elseif ($this->b === null || false === strpos('{[(+-!~', $this->b) && !$this->isAlphaNum($this->b)) {
6758 $command = self::ACTION_DELETE_A;
6759 }
6760 } elseif (!$this->isAlphaNum($this->a)) {
6761 if ($this->isWhiteSpace($this->b) || $this->isLineTerminator($this->b) && false === strpos('}])+-"\'', $this->a)) {
6762 $command = self::ACTION_DELETE_A_B;
6763 }
6764 }
6765 $this->action($command);
6766 }
6767 $this->output = trim($this->output);
6768 if ($mbIntEnc !== null) {
6769 mb_internal_encoding($mbIntEnc);
6770 }
6771 return $this->output;
6772 }
6773 /**
6774 * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
6775 * ACTION_DELETE_A = Copy B to A. Get the next B.
6776 * ACTION_DELETE_A_B = Get the next B.
6777 *
6778 * @param int $command
6779 * @throws UnterminatedRegExpException|UnterminatedStringException
6780 */
6781 protected function action($command)
6782 {
6783 // make sure we don't compress "a + ++b" to "a+++b", etc.
6784 if ($command === self::ACTION_DELETE_A_B && $this->b === ' ' && ($this->a === '+' || $this->a === '-')) {
6785 // Note: we're at an addition/substraction operator; the inputIndex
6786 // will certainly be a valid index
6787 if ($this->input[$this->inputIndex] === $this->a) {
6788 // This is "+ +" or "- -". Don't delete the space.
6789 $command = self::ACTION_KEEP_A;
6790 }
6791 }
6792 switch ($command) {
6793 case self::ACTION_KEEP_A:
6794 // 1
6795 $this->output .= $this->a;
6796 if ($this->keptComment) {
6797 $this->output = rtrim($this->output, "\n");
6798 $this->output .= $this->keptComment;
6799 $this->keptComment = '';
6800 }
6801 $this->lastByteOut = $this->a;
6802 // fallthrough intentional
6803 case self::ACTION_DELETE_A:
6804 // 2
6805 $this->a = $this->b;
6806 if ($this->a === "'" || $this->a === '"' || $this->a === '`') {
6807 // string/template literal
6808 $delimiter = $this->a;
6809 $str = $this->a;
6810 // in case needed for exception
6811 for (;;) {
6812 $this->output .= $this->a;
6813 $this->lastByteOut = $this->a;
6814 $this->a = $this->get();
6815 if ($this->a === $this->b) {
6816 // end quote
6817 break;
6818 }
6819 if ($delimiter === '`' && $this->isLineTerminator($this->a)) {
6820 // leave the newline
6821 } elseif ($this->isEOF($this->a)) {
6822 $byte = $this->inputIndex - 1;
6823 throw new \JSMin\UnterminatedStringException("JSMin: Unterminated String at byte {$byte}: {$str}");
6824 }
6825 $str .= $this->a;
6826 if ($this->a === '\\') {
6827 $this->output .= $this->a;
6828 $this->lastByteOut = $this->a;
6829 $this->a = $this->get();
6830 $str .= $this->a;
6831 }
6832 }
6833 }
6834 // fallthrough intentional
6835 case self::ACTION_DELETE_A_B:
6836 // 3
6837 $this->b = $this->next();
6838 if ($this->b === '/' && $this->isRegexpLiteral()) {
6839 $this->output .= $this->a . $this->b;
6840 $pattern = '/';
6841 // keep entire pattern in case we need to report it in the exception
6842 for (;;) {
6843 $this->a = $this->get();
6844 $pattern .= $this->a;
6845 if ($this->a === '[') {
6846 for (;;) {
6847 $this->output .= $this->a;
6848 $this->a = $this->get();
6849 $pattern .= $this->a;
6850 if ($this->a === ']') {
6851 break;
6852 }
6853 if ($this->a === '\\') {
6854 $this->output .= $this->a;
6855 $this->a = $this->get();
6856 $pattern .= $this->a;
6857 }
6858 if ($this->isEOF($this->a)) {
6859 throw new \JSMin\UnterminatedRegExpException("JSMin: Unterminated set in RegExp at byte " . $this->inputIndex . ": {$pattern}");
6860 }
6861 }
6862 }
6863 if ($this->a === '/') {
6864 // end pattern
6865 break;
6866 // while (true)
6867 } elseif ($this->a === '\\') {
6868 $this->output .= $this->a;
6869 $this->a = $this->get();
6870 $pattern .= $this->a;
6871 } elseif ($this->isEOF($this->a)) {
6872 $byte = $this->inputIndex - 1;
6873 throw new \JSMin\UnterminatedRegExpException("JSMin: Unterminated RegExp at byte {$byte}: {$pattern}");
6874 }
6875 $this->output .= $this->a;
6876 $this->lastByteOut = $this->a;
6877 }
6878 $this->b = $this->next();
6879 }
6880 }
6881 }
6882 /**
6883 * @return bool
6884 */
6885 protected function isRegexpLiteral()
6886 {
6887 if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) {
6888 // we can't divide after these tokens
6889 return true;
6890 }
6891 // check if first non-ws token is "/" (see starts-regex.js)
6892 $length = strlen($this->output);
6893 if ($this->isWhiteSpace($this->a) || $this->isLineTerminator($this->a)) {
6894 if ($length < 2) {
6895 // weird edge case
6896 return true;
6897 }
6898 }
6899 // if the "/" follows a keyword, it must be a regexp, otherwise it's best to assume division
6900 $subject = $this->output . trim($this->a);
6901 if (!preg_match('/(?:case|else|in|return|typeof)$/', $subject, $m)) {
6902 // not a keyword
6903 return false;
6904 }
6905 // can't be sure it's a keyword yet (see not-regexp.js)
6906 $charBeforeKeyword = substr($subject, 0 - strlen($m[0]) - 1, 1);
6907 if ($this->isAlphaNum($charBeforeKeyword)) {
6908 // this is really an identifier ending in a keyword, e.g. "xreturn"
6909 return false;
6910 }
6911 // it's a regexp. Remove unneeded whitespace after keyword
6912 if ($this->isWhiteSpace($this->a) || $this->isLineTerminator($this->a)) {
6913 $this->a = '';
6914 }
6915 return true;
6916 }
6917 /**
6918 * Return the next character from stdin. Watch out for lookahead. If the character is a control character,
6919 * translate it to a space or linefeed.
6920 *
6921 * @return string
6922 */
6923 protected function get()
6924 {
6925 $c = $this->lookAhead;
6926 $this->lookAhead = null;
6927 if ($c === null) {
6928 // getc(stdin)
6929 if ($this->inputIndex < $this->inputLength) {
6930 $c = $this->input[$this->inputIndex];
6931 $this->inputIndex += 1;
6932 } else {
6933 $c = null;
6934 }
6935 }
6936 if ($c === "\r") {
6937 return "\n";
6938 }
6939 return $c;
6940 }
6941 /**
6942 * Does $a indicate end of input?
6943 *
6944 * @param string $a
6945 * @return bool
6946 */
6947 protected function isEOF($a)
6948 {
6949 return $a === null || $this->isLineTerminator($a);
6950 }
6951 /**
6952 * Get next char (without getting it). If is ctrl character, translate to a space or newline.
6953 *
6954 * @return string
6955 */
6956 protected function peek()
6957 {
6958 $this->lookAhead = $this->get();
6959 return $this->lookAhead;
6960 }
6961 /**
6962 * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character.
6963 *
6964 * @param string $c
6965 *
6966 * @return bool
6967 */
6968 protected function isAlphaNum($c)
6969 {
6970 return preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126;
6971 }
6972 /**
6973 * Consume a single line comment from input (possibly retaining it)
6974 */
6975 protected function consumeSingleLineComment()
6976 {
6977 $comment = '';
6978 while (true) {
6979 $get = $this->get();
6980 $comment .= $get;
6981 if ($this->isEOF($get)) {
6982 // if IE conditional comment
6983 if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
6984 $this->keptComment .= "/{$comment}";
6985 }
6986 return;
6987 }
6988 }
6989 }
6990 /**
6991 * Consume a multiple line comment from input (possibly retaining it)
6992 *
6993 * @throws UnterminatedCommentException
6994 */
6995 protected function consumeMultipleLineComment()
6996 {
6997 $this->get();
6998 $comment = '';
6999 for (;;) {
7000 $get = $this->get();
7001 if ($get === '*') {
7002 if ($this->peek() === '/') {
7003 // end of comment reached
7004 $this->get();
7005 if (0 === strpos($comment, '!')) {
7006 // preserved by YUI Compressor
7007 if (!$this->keptComment) {
7008 // don't prepend a newline if two comments right after one another
7009 $this->keptComment = "\n";
7010 }
7011 $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n";
7012 } else {
7013 if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
7014 // IE conditional
7015 $this->keptComment .= "/*{$comment}*/";
7016 }
7017 }
7018 return;
7019 }
7020 } elseif ($get === null) {
7021 throw new \JSMin\UnterminatedCommentException("JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}");
7022 }
7023 $comment .= $get;
7024 }
7025 }
7026 /**
7027 * Get the next character, skipping over comments. Some comments may be preserved.
7028 *
7029 * @return string
7030 */
7031 protected function next()
7032 {
7033 $get = $this->get();
7034 if ($get === '/') {
7035 switch ($this->peek()) {
7036 case '/':
7037 $this->consumeSingleLineComment();
7038 $get = "\n";
7039 break;
7040 case '*':
7041 $this->consumeMultipleLineComment();
7042 $get = ' ';
7043 break;
7044 }
7045 }
7046 return $get;
7047 }
7048 protected function isWhiteSpace($s)
7049 {
7050 // https://www.ecma-international.org/ecma-262/#sec-white-space
7051 return $s !== null && strpos(" \t\v\f", $s) !== false;
7052 }
7053 protected function isLineTerminator($s)
7054 {
7055 // https://www.ecma-international.org/ecma-262/#sec-line-terminators
7056 return $s !== null && strpos("\n\r", $s) !== false;
7057 }
7058 }
7059 namespace JSMin;
7060
7061 class UnterminatedStringException extends \Exception
7062 {
7063 }
7064 namespace Kibo\PhastPlugins\SDK;
7065
7066 /**
7067 * Provides commonly needed URLs
7068 *
7069 * Interface HostURLs
7070 * @see URL
7071 */
7072 interface HostURLs
7073 {
7074 /**
7075 * The URL at which static resource (JS, CSS, IMG)
7076 * optimizations reside
7077 *
7078 * @return URL
7079 */
7080 public function getServicesURL();
7081 /**
7082 * The full URL of the root of the current site
7083 *
7084 * @return URL
7085 */
7086 public function getSiteURL();
7087 /**
7088 * The CDN equivalent of a specified URL
7089 *
7090 * @return URL
7091 */
7092 public function getCDNURL(\Kibo\Phast\ValueObjects\URL $url);
7093 /**
7094 * URL of the admin page at which
7095 * the plugin's settings are located
7096 *
7097 * @return URL
7098 */
7099 public function getSettingsURL();
7100 /**
7101 * URL for admin panel AJAX communication
7102 *
7103 * @return URL
7104 */
7105 public function getAJAXEndPoint();
7106 /**
7107 * A URL to a publicly available image.
7108 *
7109 * @return URL
7110 */
7111 public function getTestImageURL();
7112 }
7113 namespace Kibo\PhastPlugins\SDK;
7114
7115 /**
7116 * Services container for the Phast Plugins Services SDK
7117 *
7118 * Class SDK
7119 */
7120 class ServiceSDK
7121 {
7122 /**
7123 * @var ServiceHost
7124 */
7125 protected $host;
7126 /**
7127 * @var EnvironmentIdentifier
7128 */
7129 private $environmentIdentifier;
7130 public function __construct(\Kibo\PhastPlugins\SDK\ServiceHost $host)
7131 {
7132 $this->host = $host;
7133 $this->environmentIdentifier = new \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier();
7134 }
7135 public function getServiceAPI()
7136 {
7137 return new \Kibo\PhastPlugins\SDK\APIs\Service($this->getServiceConfiguration());
7138 }
7139 public function getServiceConfiguration()
7140 {
7141 return new \Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration($this->getPHPFilesServiceConfigurationRepository(), $this->getEnvironmentIdentifier(), $this->getCacheRootManager(), [$this->host, 'onServiceConfigurationLoad']);
7142 }
7143 public function getCacheRootManager()
7144 {
7145 return new \Kibo\PhastPlugins\SDK\Caching\CacheRootManager($this->host->getCacheRootCandidatesProvider());
7146 }
7147 /**
7148 * Returns a default implementation of the
7149 * ServiceConfigurationRepository interface
7150 *
7151 * @see ServiceConfigurationRepository
7152 * @return PHPFilesServiceConfigurationRepository
7153 */
7154 public function getPHPFilesServiceConfigurationRepository()
7155 {
7156 return new \Kibo\PhastPlugins\SDK\Configuration\PHPFilesServiceConfigurationRepository($this->getCacheRootManager());
7157 }
7158 public function getEnvironmentIdentifier()
7159 {
7160 return $this->environmentIdentifier;
7161 }
7162 }
7163 namespace Kibo\PhastPlugins\SDK;
7164
7165 /**
7166 * Services container for the Phast Plugins SDK
7167 *
7168 * Class SDK
7169 */
7170 class SDK extends \Kibo\PhastPlugins\SDK\ServiceSDK
7171 {
7172 /**
7173 * @var PluginHost
7174 */
7175 protected $host;
7176 /**
7177 * SDK constructor.
7178 * @param PluginHost $host
7179 */
7180 public function __construct(\Kibo\PhastPlugins\SDK\PluginHost $host)
7181 {
7182 parent::__construct($host);
7183 }
7184 /**
7185 * The current SDK version
7186 *
7187 * @return string
7188 */
7189 public function getSDKVersion()
7190 {
7191 return '8';
7192 }
7193 /**
7194 * The current plugin version.
7195 * Composed from the host plugin name,
7196 * the host plugin version
7197 * and the SDK version
7198 *
7199 * @return string
7200 */
7201 public function getPluginVersion()
7202 {
7203 return join('-', [$this->host->getPluginHostName(), $this->host->getPluginHostVersion(), $this->getSDKVersion()]);
7204 }
7205 /**
7206 * @return Phast
7207 */
7208 public function getPhastAPI()
7209 {
7210 return new \Kibo\PhastPlugins\SDK\APIs\Phast($this->getPhastConfiguration());
7211 }
7212 public function getAdminPanel()
7213 {
7214 return new \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanel($this->host->getPhastUser(), $this->host->getHostURLs()->getAJAXEndPoint(), $this->getAdminPanelData(), $this->getTranslationsManager(), $this->host->isDev());
7215 }
7216 public function getAJAXRequestsDispatcher()
7217 {
7218 return new \Kibo\PhastPlugins\SDK\AJAX\RequestsDispatcher($this->host->getPhastUser(), $this);
7219 }
7220 public function getAdminPanelData()
7221 {
7222 return new \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanelData($this->getPluginConfiguration(), $this->getServiceConfigurationGenerator(), $this->getPhastConfiguration(), $this->getCacheRootManager(), $this->host);
7223 }
7224 public function getInstallNotice()
7225 {
7226 return new \Kibo\PhastPlugins\SDK\AdminPanel\InstallNotice($this->getPluginConfiguration(), $this->host->getInstallNoticeRenderer(), $this->getTranslationsManager(), $this->host->getHostURLs()->getSettingsURL(), $this->host->getHostURLs()->getAJAXEndPoint());
7227 }
7228 public function getPluginConfiguration()
7229 {
7230 return new \Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration($this->getPluginConfigurationRepository(), $this->getServiceConfigurationGenerator(), $this->getCacheRootManager(), $this->host->getPhastUser(), $this->host->getNonceChecker());
7231 }
7232 public function getPhastConfiguration()
7233 {
7234 return new \Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration($this->getServiceConfigurationGenerator(), $this->getServiceConfiguration(), $this->getPluginConfiguration(), [$this->host, 'onPhastConfigurationLoad']);
7235 }
7236 public function getAutoConfiguration()
7237 {
7238 return new \Kibo\PhastPlugins\SDK\Configuration\AutoConfiguration($this->getPluginConfiguration(), $this->getPhastConfiguration(), $this->host->getHostURLs()->getServicesURL(), $this->host->getHostURLs()->getTestImageURL(), $this->host->getNonce(), $this->host->getHostURLs()->getAJAXEndPoint());
7239 }
7240 public function getTranslationsManager()
7241 {
7242 return new \Kibo\PhastPlugins\SDK\AdminPanel\TranslationsManager($this->host->getLocale(), $this->host->getPluginName());
7243 }
7244 private function getServiceConfigurationGenerator()
7245 {
7246 return new \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator($this->getPHPFilesServiceConfigurationRepository(), $this->getEnvironmentIdentifier(), $this->getPluginVersion(), $this->host->getHostURLs());
7247 }
7248 public function updatePreviewCookie($enable = true)
7249 {
7250 if (headers_sent()) {
7251 return false;
7252 }
7253 $enabled = isset($_COOKIE['PHAST_PREVIEW']) && (bool) $_COOKIE['PHAST_PREVIEW'];
7254 if (!$enabled && $enable) {
7255 return setcookie('PHAST_PREVIEW', '1', 0, '/');
7256 }
7257 if ($enabled && !$enable) {
7258 return setcookie('PHAST_PREVIEW', '0', 0, '/');
7259 }
7260 return true;
7261 }
7262 private function getPluginConfigurationRepository()
7263 {
7264 return new \Kibo\PhastPlugins\SDK\Configuration\PluginConfigurationRepository($this->host->getKeyValueStore());
7265 }
7266 }
7267 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7268
7269 /**
7270 * Represents the data that needs to be send
7271 * to the plugin's admin panel
7272 *
7273 * Class AdminPanelData
7274 */
7275 class AdminPanelData
7276 {
7277 /**
7278 * @var PluginConfiguration
7279 */
7280 private $pluginConfig;
7281 /**
7282 * @var ServiceConfigurationGenerator
7283 */
7284 private $serviceConfigGenerator;
7285 /**
7286 * @var PhastConfiguration
7287 */
7288 private $phastConfig;
7289 /**
7290 * @var CacheRootManager
7291 */
7292 private $cacheRootManager;
7293 /**
7294 * @var PluginHost
7295 */
7296 private $host;
7297 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig, \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator $serviceConfigGenerator, \Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration $phastConfig, \Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager, \Kibo\PhastPlugins\SDK\PluginHost $host)
7298 {
7299 $this->pluginConfig = $pluginConfig;
7300 $this->serviceConfigGenerator = $serviceConfigGenerator;
7301 $this->phastConfig = $phastConfig;
7302 $this->cacheRootManager = $cacheRootManager;
7303 $this->host = $host;
7304 }
7305 public function get()
7306 {
7307 $siteUrl = $this->host->getHostURLs()->getSiteURL();
7308 $urlWithPhast = $this->addQueryParam($siteUrl, 'phast', 'phast');
7309 $urlWithoutPhast = $this->addQueryParam($siteUrl, 'phast', '-phast');
7310 $pageSpeedToolUrl = 'https://developers.google.com/speed/pagespeed/insights/?url=';
7311 $errors = [];
7312 if (!$this->cacheRootManager->hasCacheRoot()) {
7313 $errors[] = ['type' => 'no-cache-root', 'params' => $this->cacheRootManager->getCacheRootCandidates()];
7314 }
7315 if (!$this->serviceConfigGenerator->generateIfNotExists($this->pluginConfig)) {
7316 $errors[] = ['type' => 'no-service-config', 'params' => $this->cacheRootManager->getCacheRootCandidates()];
7317 }
7318 $warnings = [];
7319 $api_client_warning = [];
7320 $phast_config = $this->phastConfig->get();
7321 $diagnostics = new \Kibo\Phast\Diagnostics\SystemDiagnostics();
7322 foreach ($diagnostics->run($phast_config) as $status) {
7323 if ($status->isAvailable()) {
7324 continue;
7325 }
7326 $package = $status->getPackage();
7327 $type = $package->getType();
7328 if ($type == 'Cache') {
7329 $errors[] = ['type' => 'cache', 'params' => [$status->getReason()]];
7330 } elseif ($type == 'ImageFilter') {
7331 $name = substr($package->getNamespace(), strrpos($package->getNamespace(), '\\') + 1);
7332 if ($name === 'ImageAPIClient') {
7333 $api_client_warning[] = 'Image optimization API error: ' . $status->getReason();
7334 } else {
7335 $warnings[] = $status->getReason();
7336 }
7337 }
7338 }
7339 $phastpress_config = $this->pluginConfig->get();
7340 if ($phastpress_config['img-optimization-api']) {
7341 $warnings = $api_client_warning;
7342 }
7343 $nonce = $this->host->getNonce();
7344 return ['config' => $phastpress_config, 'settingsStrings' => ['urlWithPhast' => $pageSpeedToolUrl . rawurlencode($urlWithPhast), 'urlWithoutPhast' => $pageSpeedToolUrl . rawurlencode($urlWithoutPhast), 'maxImageWidth' => 1920 * 2, 'maxImageHeight' => 1080 * 2], 'errors' => $errors, 'warnings' => $warnings, 'nonce' => $nonce->getValue(), 'nonceName' => $nonce->getFieldName(), 'pluginName' => $this->host->getPluginName(), 'pluginVersion' => $this->host->getPluginHostVersion()];
7345 }
7346 private function addQueryParam(\Kibo\Phast\ValueObjects\URL $url, $key, $value)
7347 {
7348 // TODO: Move this functionality to URL class
7349 $urlStr = (string) $url;
7350 $glue = strpos($urlStr, '?') === false ? '?' : '&';
7351 return $urlStr . $glue . $key . '=' . $value;
7352 }
7353 }
7354 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7355
7356 /**
7357 * Represents an installation notice
7358 * displayed everywhere in the host system's admin panel
7359 * upon plugin activation.
7360 *
7361 * Class InstallNotice
7362 */
7363 class InstallNotice
7364 {
7365 /**
7366 * @var PluginConfiguration
7367 */
7368 private $config;
7369 /**
7370 * @var InstallNoticeRenderer
7371 */
7372 private $renderer;
7373 /**
7374 * @var TranslationsManager
7375 */
7376 private $translations;
7377 /**
7378 * @var URL
7379 */
7380 private $settingsUrl;
7381 /**
7382 * @var URL
7383 */
7384 private $ajaxEntryPoint;
7385 /**
7386 * InstallNotice constructor.
7387 * @param PluginConfiguration $config
7388 * @param InstallNoticeRenderer $renderer
7389 * @param TranslationsManager $translations
7390 * @param URL $settingsUrl
7391 * @param URL $ajaxEndPoint
7392 */
7393 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $config, \Kibo\PhastPlugins\SDK\AdminPanel\InstallNoticeRenderer $renderer, \Kibo\PhastPlugins\SDK\AdminPanel\TranslationsManager $translations, \Kibo\Phast\ValueObjects\URL $settingsUrl, \Kibo\Phast\ValueObjects\URL $ajaxEndPoint)
7394 {
7395 $this->config = $config;
7396 $this->renderer = $renderer;
7397 $this->translations = $translations;
7398 $this->settingsUrl = $settingsUrl;
7399 $this->ajaxEntryPoint = $ajaxEndPoint;
7400 }
7401 /**
7402 * @return string The HTML to render the notice
7403 */
7404 public function render()
7405 {
7406 $display_message = $this->config->shouldShowActivationNotification();
7407 if (!$display_message) {
7408 return '';
7409 }
7410 $config = $this->config->get();
7411 if ($config['enabled'] && $config['admin-only']) {
7412 $status = 'Backend.status.admin';
7413 } elseif ($config['enabled']) {
7414 $status = 'Backend.status.on';
7415 } else {
7416 $status = 'Backend.status.off';
7417 }
7418 $message = $this->translations->get('Backend.install-notice', ['pluginState' => $this->translations->get($status), 'settingsUrl' => (string) $this->settingsUrl]);
7419 $onCloseFunction = "\n function () {\n var data = new FormData();\n data.append('phast-plugins-action', 'dismiss-notice')\n var xhr = new XMLHttpRequest();\n xhr.open('POST', '{$this->ajaxEntryPoint}')\n xhr.send(data)\n }\n ";
7420 return $this->renderer->render($message, $onCloseFunction);
7421 }
7422 }
7423 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7424
7425 interface InstallNoticeRenderer
7426 {
7427 /**
7428 * Renders a system notice.
7429 *
7430 * @param string $notice The message to show in the notice
7431 * @param string $onCloseJSFunction JavaScript function to call on the client when
7432 * an event that closes the notice occurs.
7433 * @return string HTML for the notice
7434 * @see InstallNotice
7435 */
7436 public function render($notice, $onCloseJSFunction);
7437 }
7438 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7439
7440 /**
7441 * Represents a nonce form field used XSS defense
7442 *
7443 * Class Nonce
7444 */
7445 class Nonce implements \JsonSerializable
7446 {
7447 /**
7448 * @var string
7449 */
7450 private $fieldName;
7451 /**
7452 * @var string
7453 */
7454 private $value;
7455 private function __construct()
7456 {
7457 }
7458 /**
7459 * @param string $fieldName The name of the field in the form
7460 * @param string $value The value of the field
7461 * @return Nonce
7462 */
7463 public static function make($fieldName, $value)
7464 {
7465 $instance = new self();
7466 $instance->fieldName = $fieldName;
7467 $instance->value = $value;
7468 return $instance;
7469 }
7470 /**
7471 * @return string
7472 */
7473 public function getFieldName()
7474 {
7475 return $this->fieldName;
7476 }
7477 /**
7478 * @return string
7479 */
7480 public function getValue()
7481 {
7482 return $this->value;
7483 }
7484 public function jsonSerialize()
7485 {
7486 return ['fieldName' => $this->fieldName, 'value' => $this->value];
7487 }
7488 }
7489 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7490
7491 /**
7492 * Represents the admin panel used for plugin configuration.
7493 * Use this class for rendering the admin panel of the plugin.
7494 *
7495 * Class AdminPanel
7496 */
7497 class AdminPanel
7498 {
7499 /**
7500 * @var PhastUser
7501 */
7502 private $user;
7503 /**
7504 * @var URL
7505 */
7506 private $ajaxEndPoint;
7507 /**
7508 * @var AdminPanelData
7509 */
7510 private $data;
7511 /**
7512 * @var TranslationsManager
7513 */
7514 private $translations;
7515 private $isDev = 'prod';
7516 private $styles = array('prod' => array('app.css'), 'dev' => array());
7517 private $scripts = array('prod' => array('manifest.js', 'vendor.js', 'babel-polyfill.js', 'app.js'), 'dev' => array('http://localhost:25903/babel-polyfill.js', 'http://localhost:25903/app.js'));
7518 /**
7519 * AdminPanel constructor.
7520 * @param PhastUser $user
7521 * @param URL $ajaxEndPoint
7522 * @param AdminPanelData $data
7523 * @param TranslationsManager $translations
7524 * @param bool $isDev
7525 */
7526 public function __construct(\Kibo\PhastPlugins\SDK\Security\PhastUser $user, \Kibo\Phast\ValueObjects\URL $ajaxEndPoint, \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanelData $data, \Kibo\PhastPlugins\SDK\AdminPanel\TranslationsManager $translations, $isDev)
7527 {
7528 $this->user = $user;
7529 $this->ajaxEndPoint = $ajaxEndPoint;
7530 $this->data = $data;
7531 $this->translations = $translations;
7532 $this->isDev = (bool) $isDev;
7533 }
7534 /**
7535 * Returns the HTML needed to display the admin panel
7536 *
7537 * @return string
7538 */
7539 public function render()
7540 {
7541 if (!$this->user->mayModifySettings()) {
7542 return '';
7543 }
7544 $id = 'phast-plugins-sdk-admin-panel';
7545 $template = $this->getResourcesString();
7546 $template .= sprintf('
7547 <div id="%1$s"></div>
7548 <script>
7549 try {
7550 window.PHAST_PLUGINS_SDK_ADMIN_PANEL.apply(window, %2$s)
7551 } catch (e) {
7552 document.getElementById("%1$s").innerText = "Error: " + e.message
7553 throw e
7554 }
7555 </script>
7556 ', $id, json_encode([$id, $this->ajaxEndPoint->toString(), $this->data->get(), $this->translations->getAll()]));
7557 return $template;
7558 }
7559 private function getResourcesString()
7560 {
7561 return $this->isDev ? $this->getDevResourcesString() : $this->getProdResources();
7562 }
7563 private function getProdResources()
7564 {
7565 $resources = '';
7566 $base = __DIR__ . '/static/';
7567 $cssBase = $base . 'css/';
7568 foreach ($this->styles['prod'] as $style) {
7569 $resources .= '<style>' . file_get_contents($cssBase . $style) . '</style>';
7570 }
7571 $jsBase = $base . 'js/';
7572 foreach ($this->scripts['prod'] as $script) {
7573 $resources .= '<script>' . file_get_contents($jsBase . $script) . '</script>';
7574 }
7575 return $resources;
7576 }
7577 private function getDevResourcesString()
7578 {
7579 $resources = '';
7580 foreach ($this->styles['dev'] as $href) {
7581 $resources .= "<link rel=\"stylesheet\" href=\"{$href}\">";
7582 }
7583 foreach ($this->scripts['dev'] as $src) {
7584 $resources .= "<script src=\"{$src}\"></script>";
7585 }
7586 return $resources;
7587 }
7588 }
7589 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7590
7591 class TranslationsManager
7592 {
7593 const DEFAULT_LOCALE = 'en';
7594 /**
7595 * @var string
7596 */
7597 private $locale;
7598 /**
7599 * @var string
7600 */
7601 private $pluginName;
7602 /**
7603 * @var string
7604 */
7605 private $languagesDir;
7606 /**
7607 * @var array
7608 */
7609 private $modules = array();
7610 public function __construct($locale, $pluginName)
7611 {
7612 $data = \Kibo\PhastPlugins\SDK\Generated\Translations::DATA;
7613 if (isset($data[$this->locale])) {
7614 $this->locale = $locale;
7615 } else {
7616 $this->locale = self::DEFAULT_LOCALE;
7617 }
7618 $this->pluginName = $pluginName;
7619 }
7620 public function getAll()
7621 {
7622 return array_merge(['plugin-name' => $this->pluginName], \Kibo\PhastPlugins\SDK\Generated\Translations::DATA[$this->locale]);
7623 }
7624 public function get($key, $interpolationArguments = array())
7625 {
7626 $keyParts = explode('.', $key);
7627 $transArr = \Kibo\PhastPlugins\SDK\Generated\Translations::DATA[$this->locale];
7628 while (count($keyParts) > 0) {
7629 $part = array_shift($keyParts);
7630 if (!isset($transArr[$part])) {
7631 return $key;
7632 }
7633 $transArr = $transArr[$part];
7634 }
7635 if (is_string($transArr)) {
7636 return $this->interpolate($transArr, $interpolationArguments);
7637 }
7638 return $key;
7639 }
7640 private function interpolate($string, $arguments)
7641 {
7642 $keys = array_map(function ($str) {
7643 return '{' . $str . '}';
7644 }, array_keys($arguments));
7645 $params = array_combine($keys, array_values($arguments));
7646 $params['@:plugin-name'] = $this->pluginName;
7647 return strtr($string, $params);
7648 }
7649 }
7650 namespace Kibo\PhastPlugins\SDK\Configuration;
7651
7652 /**
7653 * Represents the javascript used
7654 * for auto-configuration done
7655 * immediately after activation of the plugin.
7656 *
7657 * Class AutoConfiguration
7658 */
7659 class AutoConfiguration
7660 {
7661 /**
7662 * @var PluginConfiguration
7663 */
7664 private $pluginConfig;
7665 /**
7666 * @var PhastConfiguration
7667 */
7668 private $phastConfig;
7669 /**
7670 * @var URL
7671 */
7672 private $servicesUrl;
7673 /**
7674 * @var URL
7675 */
7676 private $testImageUrl;
7677 /**
7678 * @var Nonce
7679 */
7680 private $nonce;
7681 /**
7682 * @var URL
7683 */
7684 private $ajaxEndPoint;
7685 /**
7686 * AutoConfiguration constructor.
7687 * @param PluginConfiguration $pluginConfig
7688 * @param PhastConfiguration $phastConfig
7689 * @param URL $servicesUrl
7690 * @param URL $testImageUrl
7691 * @param Nonce $nonce
7692 * @param URL $ajaxEndPoint
7693 */
7694 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig, \Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration $phastConfig, \Kibo\Phast\ValueObjects\URL $servicesUrl, \Kibo\Phast\ValueObjects\URL $testImageUrl, \Kibo\PhastPlugins\SDK\AdminPanel\Nonce $nonce, \Kibo\Phast\ValueObjects\URL $ajaxEndPoint)
7695 {
7696 $this->pluginConfig = $pluginConfig;
7697 $this->phastConfig = $phastConfig;
7698 $this->servicesUrl = $servicesUrl;
7699 $this->testImageUrl = $testImageUrl;
7700 $this->nonce = $nonce;
7701 $this->ajaxEndPoint = $ajaxEndPoint;
7702 }
7703 /**
7704 * Returns the script that needs to rendered
7705 * in order for the script to get executed.
7706 *
7707 * @return string
7708 */
7709 public function renderScript()
7710 {
7711 if (!$this->pluginConfig->shouldAutoConfigure()) {
7712 return '';
7713 }
7714 $config = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration(new \Kibo\Phast\Environment\Configuration($this->phastConfig->get()))->getRuntimeConfig()->toArray();
7715 $signature = (new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config);
7716 $service_image_url = (new \Kibo\Phast\Services\ServiceRequest())->withUrl($this->servicesUrl)->withParams(['service' => 'images', 'src' => (string) $this->testImageUrl])->sign($signature)->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_PATH);
7717 $nonce = json_encode($this->nonce);
7718 return '<script>(function (imageUrl, nonce, ajaxEndPoint) {' . "var logPrefix=\"[Phast autoconfiguration]\";var imageRequest=new XMLHttpRequest;imageRequest.open(\"GET\",imageUrl);imageRequest.onload=function(){var a=imageRequest.status>=200&&imageRequest.status<300;console.log(logPrefix,\"Got status\",imageRequest.status,\"which is\",a?\"successful\":\"unsuccessful\");configureRequestsFormat(a)};imageRequest.onerror=function(){console.log(logPrefix,\"Got error\");configureRequestsFormat(false)};imageRequest.ontimeout=function(){console.log(logPrefix,\"Request timed out\");configureRequestsFormat(false)};console.log(logPrefix,\"Requesting testing image through Phast service\");console.log(logPrefix,\"URL:\",imageUrl);imageRequest.send();function configureRequestsFormat(b){console.log(logPrefix,\"Configuring Phast with path info\",b?\"on\":\"off\");var c=new FormData;c.append(\"phast-plugins-action\",\"save-settings\");c.append(\"phastpress-pathinfo-query-format\",b?\"on\":\"off\");c.append(nonce.fieldName,nonce.value);var d=new XMLHttpRequest;d.open(\"POST\",ajaxEndPoint);d.responseType=\"json\";d.addEventListener(\"load\",function(){var e=d.response;if(typeof e===\"object\"&&e[\"phast-success\"]===true){console.log(logPrefix,\"Successfully autoconfigured! Dispatching event!\");var f=new CustomEvent(\"phast-auto-config\",{detail:e[\"phast-data\"]});window.dispatchEvent(f)}});d.send(c)}\n" . "})('{$service_image_url}', {$nonce}, '{$this->ajaxEndPoint}')</script>";
7719 }
7720 }
7721 namespace Kibo\PhastPlugins\SDK\Configuration;
7722
7723 class ServiceConfigurationGenerator
7724 {
7725 /**
7726 * @var ServiceConfigurationRepository
7727 */
7728 private $repository;
7729 /**
7730 * @var EnvironmentIdentifier
7731 */
7732 private $environmentIdentifier;
7733 /**
7734 * @var URL
7735 */
7736 private $servicesUrl;
7737 /**
7738 * @var URL
7739 */
7740 private $cdnServicesUrl;
7741 /**
7742 * @var string
7743 */
7744 private $pluginVersion;
7745 /**
7746 * @var string
7747 */
7748 private $cdnHost;
7749 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository $repository, \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier $environmentIdentifier, $pluginVersion, \Kibo\PhastPlugins\SDK\HostURLs $hostUrls)
7750 {
7751 $this->repository = $repository;
7752 $this->environmentIdentifier = $environmentIdentifier;
7753 $this->pluginVersion = $pluginVersion;
7754 $this->servicesUrl = $hostUrls->getServicesURL();
7755 $this->cdnServicesUrl = $hostUrls->getCDNURL($hostUrls->getServicesURL());
7756 $this->cdnHost = $hostUrls->getCDNURL($hostUrls->getSiteURL())->getHost();
7757 }
7758 public function generateIfNotExists(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig)
7759 {
7760 if (!$this->repository->has()) {
7761 return $this->generate($pluginConfig);
7762 }
7763 $config = $this->repository->get();
7764 $envId = $this->environmentIdentifier->getValue();
7765 if (empty($config['plugin_version']) || $config['plugin_version'] != $this->pluginVersion || empty($config['alternativeServicesUrls'][$envId]) || $config['alternativeServicesUrls'][$envId] != $this->getServicesURLString($pluginConfig)) {
7766 return $this->generate($pluginConfig);
7767 }
7768 return true;
7769 }
7770 public function generate(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig)
7771 {
7772 $previousConfig = $this->repository->get();
7773 $plugin_config = $pluginConfig->get();
7774 $plugin_version = $this->pluginVersion;
7775 $config = ['plugin_version' => $plugin_version, 'servicesUrl' => $this->getServicesURLString($pluginConfig), 'securityToken' => empty($previousConfig['securityToken']) ? \Kibo\Phast\Security\ServiceSignature::generateToken() : $previousConfig['securityToken'], 'images' => ['filters' => [\Kibo\Phast\Filters\Image\ImageAPIClient\Filter::class => ['enabled' => $plugin_config['img-optimization-api'], 'plugin-version' => $plugin_version]]], 'styles' => ['filters' => [\Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter::class => ['enabled' => $plugin_config['img-optimization-css']]]], 'serviceRequestFormat' => $plugin_config['pathinfo-query-format'] ? \Kibo\Phast\Services\ServiceRequest::FORMAT_PATH : \Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY, 'compressServiceResponse' => isset($plugin_config['compress-service-response']) ? !!$plugin_config['compress-service-response'] : true, 'cdnHost' => $this->cdnHost];
7776 if (isset($previousConfig['alternativeServicesUrls'])) {
7777 $config['alternativeServicesUrls'] = $previousConfig['alternativeServicesUrls'];
7778 } else {
7779 $config['alternativeServicesUrls'] = [];
7780 }
7781 $id = $this->environmentIdentifier->getValue();
7782 unset($config['alternativeServicesUrls'][$id]);
7783 $config['alternativeServicesUrls'][$id] = $this->getServicesURLString($pluginConfig);
7784 $config['alternativeServicesUrls'] = array_slice($config['alternativeServicesUrls'], -1000);
7785 return $this->repository->store($config);
7786 }
7787 private function getServicesURLString(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig)
7788 {
7789 $plugin_config = $pluginConfig->get();
7790 if ($plugin_config['pathinfo-query-format']) {
7791 return (string) $this->cdnServicesUrl;
7792 }
7793 return (string) $this->servicesUrl;
7794 }
7795 }
7796 namespace Kibo\PhastPlugins\SDK\Configuration;
7797
7798 /**
7799 * Represents the configuration
7800 * that needs to be passed to be returned
7801 * by the callback passed to
7802 * \Kibo\Phast\PhastServices::serve()
7803 *
7804 * @see \Kibo\Phast\PhastServices::serve()
7805 * Class ServiceConfiguration
7806 */
7807 class ServiceConfiguration
7808 {
7809 /**
7810 * @var ServiceConfigurationRepository
7811 */
7812 private $repository;
7813 /**
7814 * @var EnvironmentIdentifier
7815 */
7816 private $environmentIdentifier;
7817 /**
7818 * @var CacheRootManager
7819 */
7820 private $cacheRootManager;
7821 /**
7822 * @var callable
7823 */
7824 private $onLoadCb;
7825 /**
7826 * ServiceConfiguration constructor.
7827 * @param ServiceConfigurationRepository $repository
7828 * @param CacheRootManager $cacheRootManager
7829 * @param callable $onLoadCb
7830 */
7831 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository $repository, \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier $environmentIdentifier, \Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager, callable $onLoadCb)
7832 {
7833 $this->repository = $repository;
7834 $this->environmentIdentifier = $environmentIdentifier;
7835 $this->cacheRootManager = $cacheRootManager;
7836 $this->onLoadCb = $onLoadCb;
7837 }
7838 /**
7839 * Returns the configuration as config
7840 *
7841 * @return array|bool|mixed
7842 */
7843 public function get()
7844 {
7845 $config = $this->repository->get();
7846 $envId = $this->environmentIdentifier->getValue();
7847 if (isset($config['alternativeServicesUrls'][$envId])) {
7848 $config['servicesUrl'] = $config['alternativeServicesUrls'][$envId];
7849 }
7850 if (!empty($config['cdnHost'])) {
7851 $config['retrieverMap'][$config['cdnHost']] = \Kibo\Phast\HTTP\Request::fromGlobals()->getDocumentRoot();
7852 }
7853 $config['cache'] = ['cacheRoot' => $this->cacheRootManager->getCacheRoot()];
7854 $apiFilterName = \Kibo\Phast\Filters\Image\ImageAPIClient\Filter::class;
7855 $api_enabled = $config['images']['filters'][$apiFilterName]['enabled'];
7856 if (!$api_enabled) {
7857 unset($config['images']['filters'][$apiFilterName]);
7858 return call_user_func($this->onLoadCb, $config);
7859 }
7860 $config['images']['filters'][$apiFilterName]['host-name'] = $_SERVER['HTTP_HOST'];
7861 $config['images']['filters'][$apiFilterName]['request-uri'] = $_SERVER['REQUEST_URI'];
7862 $config['images']['filters'][$apiFilterName]['api-url'] = 'https://optimize.phast.io/?service=images';
7863 return call_user_func($this->onLoadCb, $config);
7864 }
7865 }
7866 namespace Kibo\PhastPlugins\SDK\Configuration;
7867
7868 class EnvironmentIdentifier
7869 {
7870 private $value;
7871 public function __construct()
7872 {
7873 $this->value = sprintf('%s://%s%s', empty($_SERVER['HTTPS']) ? 'http' : 'https', empty($_SERVER['HTTP_HOST']) ? '' : $_SERVER['HTTP_HOST'], empty($_SERVER['SERVER_PORT']) ? '' : ":{$_SERVER['SERVER_PORT']}");
7874 }
7875 public function getValue()
7876 {
7877 return $this->value;
7878 }
7879 }
7880 namespace Kibo\PhastPlugins\SDK\Configuration;
7881
7882 /**
7883 * Represents the configuration
7884 * that needs to be passed to
7885 * \Kibo\Phast\PhastDocumentFilters::deploy()
7886 * and
7887 * \Kibo\Phast\PhastDocumentFilters::apply()
7888 *
7889 * @see \Kibo\Phast\PhastDocumentFilters
7890 * Class PhastConfiguration
7891 */
7892 class PhastConfiguration
7893 {
7894 const SETTINGS_2_FILTERS = array('img-optimization-tags' => array(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags\Filter::class), 'img-optimization-css' => array(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter::class, \Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter::class), 'img-lazy' => array(\Kibo\Phast\Filters\HTML\LazyImageLoading\Filter::class), 'css-optimization' => array(\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class), 'scripts-defer' => array(\Kibo\Phast\Filters\HTML\ScriptsDeferring\Filter::class), 'scripts-proxy' => array(\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class), 'iframe-defer' => array(\Kibo\Phast\Filters\HTML\DelayedIFrameLoading\Filter::class), 'minify-html' => array(\Kibo\Phast\Filters\HTML\Minify\Filter::class), 'minify-inline-scripts' => array(\Kibo\Phast\Filters\HTML\MinifyScripts\Filter::class));
7895 /**
7896 * @var ServiceConfigurationGenerator
7897 */
7898 private $serviceConfigGenerator;
7899 /**
7900 * @var ServiceConfiguration
7901 */
7902 private $serviceConfig;
7903 /**
7904 * @var PluginConfiguration
7905 */
7906 private $pluginConfig;
7907 /**
7908 * @var callable
7909 */
7910 private $onLoadCb;
7911 /**
7912 * PhastConfiguration constructor.
7913 * @param ServiceConfigurationGenerator $serviceConfigGenerator
7914 * @param ServiceConfiguration $serviceConfig
7915 * @param PluginConfiguration $pluginConfig
7916 * @param callable $onLoadCb
7917 */
7918 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator $serviceConfigGenerator, \Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration $serviceConfig, \Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig, callable $onLoadCb)
7919 {
7920 $this->serviceConfigGenerator = $serviceConfigGenerator;
7921 $this->serviceConfig = $serviceConfig;
7922 $this->pluginConfig = $pluginConfig;
7923 $this->onLoadCb = $onLoadCb;
7924 }
7925 /**
7926 * Returns the configuration to use on full html documents as an array
7927 *
7928 * @return array|bool|mixed
7929 */
7930 public function getForDocuments()
7931 {
7932 list($phastConfig, $pluginConfig) = $this->getPhastAndPluginConfigs();
7933 foreach (array_keys(self::SETTINGS_2_FILTERS) as $setting) {
7934 $this->setSettingInPhastConfig($setting, $pluginConfig, $phastConfig);
7935 }
7936 return call_user_func($this->onLoadCb, $phastConfig);
7937 }
7938 public function getForHTMLSnippets()
7939 {
7940 list($phastConfig, $pluginConfig) = $this->getPhastAndPluginConfigs();
7941 $defaultConfig = \Kibo\Phast\Environment\Configuration::fromDefaults()->toArray();
7942 $allFilters = array_keys($defaultConfig['documents']['filters']);
7943 foreach ($allFilters as $filter) {
7944 $phastConfig['documents']['filters'][$filter]['enabled'] = false;
7945 }
7946 $this->setSettingInPhastConfig('img-optimization-tags', $pluginConfig, $phastConfig);
7947 $this->setSettingInPhastConfig('img-optimization-css', $pluginConfig, $phastConfig);
7948 $this->setSettingInPhastConfig('img-lazy', $pluginConfig, $phastConfig);
7949 $phastConfig['optimizeHTMLDocumentsOnly'] = false;
7950 $phastConfig['outputServerSideStats'] = false;
7951 return call_user_func($this->onLoadCb, $phastConfig);
7952 }
7953 private function getPhastAndPluginConfigs()
7954 {
7955 // TODO: Optimize so we do not read from the service config file a bunch of times
7956 $this->serviceConfigGenerator->generateIfNotExists($this->pluginConfig);
7957 $pluginConfig = $this->pluginConfig->get();
7958 $phastConfig = $this->serviceConfig->get();
7959 $phastConfig['documents']['filters'] = [];
7960 $phastConfig['switches']['phast'] = $phastConfig && $this->pluginConfig->shouldDeployFilters();
7961 return [$phastConfig, $pluginConfig];
7962 }
7963 private function setSettingInPhastConfig($settingName, $pluginConfig, &$phastConfig)
7964 {
7965 foreach (self::SETTINGS_2_FILTERS[$settingName] as $filterClass) {
7966 if (!class_exists($filterClass)) {
7967 throw new \LogicException("No such filter: {$filterClass}");
7968 }
7969 if (strpos($filterClass, \Kibo\Phast\Filters\HTML::class . '\\') === 0) {
7970 $object = 'documents';
7971 } elseif (strpos($filterClass, \Kibo\Phast\Filters\CSS::class . '\\') === 0) {
7972 $object = 'styles';
7973 } else {
7974 throw new \LogicException("Invalid filter namespace: {$filterClass}");
7975 }
7976 $phastConfig[$object]['filters'][$filterClass] = ['enabled' => $settingName];
7977 $phastConfig['switches'][$settingName] = $pluginConfig[$settingName];
7978 }
7979 }
7980 /**
7981 * Returns the configuration as an array
7982 *
7983 * @return array|bool|mixed
7984 * @deprecated use PhastConfiguration::getForDocuments()
7985 */
7986 public function get()
7987 {
7988 return $this->getForDocuments();
7989 }
7990 }
7991 namespace Kibo\PhastPlugins\SDK\Configuration;
7992
7993 /**
7994 * Manages serialization of the phast service configuration.
7995 * Must be as fast as possible, having as little
7996 * dependency on the host system as possible (ideally - none).
7997 *
7998 * Interface ServiceConfigurationRepository
7999 */
8000 interface ServiceConfigurationRepository
8001 {
8002 /**
8003 * Store the given config
8004 *
8005 * @param array $config
8006 * @return bool TRUE on success, FALSE on failure
8007 */
8008 public function store(array $config);
8009 /**
8010 * Returns the previously stored config
8011 *
8012 * @return array|bool - The config on success or
8013 * FALSE on failure or if no config has been stored
8014 */
8015 public function get();
8016 /**
8017 * Tells whether a config has been previously stored
8018 *
8019 * @return bool
8020 */
8021 public function has();
8022 }
8023 namespace Kibo\PhastPlugins\SDK\Configuration;
8024
8025 class PluginConfigurationRepository
8026 {
8027 /**
8028 * @var KeyValueStore
8029 */
8030 private $store;
8031 /**
8032 * JSONKeyValueStore constructor.
8033 * @param KeyValueStore $store
8034 */
8035 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\KeyValueStore $store)
8036 {
8037 $this->store = $store;
8038 }
8039 /**
8040 * @param mixed $key
8041 * @param null $default
8042 * @return mixed|null
8043 */
8044 public function get($key, $default = null)
8045 {
8046 $value = $this->store->get($key);
8047 if (!is_string($value) || $value === 'null') {
8048 return $default;
8049 }
8050 $deserialised = @json_decode($value, true);
8051 if (is_null($deserialised)) {
8052 return $default;
8053 }
8054 return $deserialised;
8055 }
8056 /**
8057 * @param mixed $key
8058 * @param mixed $value
8059 */
8060 public function set($key, $value)
8061 {
8062 $this->store->set($key, json_encode($value));
8063 }
8064 }
8065 namespace Kibo\PhastPlugins\SDK\Configuration;
8066
8067 /**
8068 * A key-value store for use within the plugin's admin panel
8069 *
8070 * Interface KeyValueStore
8071 */
8072 interface KeyValueStore
8073 {
8074 /**
8075 * @param string $key
8076 * @return string|null The previously stored value or
8077 * null if there was no value stored for this key
8078 */
8079 public function get($key);
8080 /**
8081 * @param string $key
8082 * @param string $value
8083 * @return void
8084 */
8085 public function set($key, $value);
8086 }
8087 namespace Kibo\PhastPlugins\SDK\Configuration;
8088
8089 /**
8090 * Represents the configuration of the plugin
8091 *
8092 * Class PluginConfiguration
8093 */
8094 class PluginConfiguration
8095 {
8096 const KEY_SETTINGS = 'settings';
8097 const KEY_ACTIVATION_NOTIFICATION = 'activation-notification';
8098 /**
8099 * @var PluginConfigurationRepository
8100 */
8101 private $repo;
8102 /**
8103 * @var ServiceConfigurationGenerator
8104 */
8105 private $serviceConfigGenerator;
8106 /**
8107 * @var CacheRootManager
8108 */
8109 private $cacheRootManager;
8110 /**
8111 * @var PhastUser
8112 */
8113 private $user;
8114 /**
8115 * @var NonceChecker
8116 */
8117 private $nonceChecker;
8118 /**
8119 * PluginConfiguration constructor.
8120 * @param PluginConfigurationRepository $repo
8121 * @param ServiceConfigurationGenerator $serviceConfigGenerator
8122 * @param CacheRootManager $cacheRootManager
8123 * @param PhastUser $user
8124 * @param NonceChecker $nonceChecker
8125 */
8126 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PluginConfigurationRepository $repo, \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator $serviceConfigGenerator, \Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager, \Kibo\PhastPlugins\SDK\Security\PhastUser $user, \Kibo\PhastPlugins\SDK\Security\NonceChecker $nonceChecker)
8127 {
8128 $this->repo = $repo;
8129 $this->serviceConfigGenerator = $serviceConfigGenerator;
8130 $this->cacheRootManager = $cacheRootManager;
8131 $this->user = $user;
8132 $this->nonceChecker = $nonceChecker;
8133 }
8134 public function get()
8135 {
8136 $userSettings = $this->repo->get(self::KEY_SETTINGS, []);
8137 return array_merge($this->getDefaultAdminPanelSettings(), $userSettings);
8138 }
8139 public function save(array $newConfig)
8140 {
8141 if (!$this->nonceChecker->checkNonce($newConfig)) {
8142 return;
8143 }
8144 $keys = array_keys($this->getDefaultAdminPanelSettings());
8145 $settings = [];
8146 foreach ($keys as $key) {
8147 $newConfigKey = "phastpress-{$key}";
8148 if (!isset($newConfig[$newConfigKey])) {
8149 continue;
8150 }
8151 if ($newConfig[$newConfigKey] == 'on') {
8152 $settings[$key] = true;
8153 } elseif ($newConfig[$newConfigKey] == 'off') {
8154 $settings[$key] = false;
8155 }
8156 }
8157 $this->update($settings);
8158 }
8159 public function update(array $settings)
8160 {
8161 $this->repo->set(self::KEY_SETTINGS, array_merge($this->get(), $settings));
8162 $this->serviceConfigGenerator->generate($this);
8163 }
8164 public function shouldShowActivationNotification()
8165 {
8166 return $this->repo->get(self::KEY_ACTIVATION_NOTIFICATION, true);
8167 }
8168 public function hideActivationNotification()
8169 {
8170 $this->repo->set(self::KEY_ACTIVATION_NOTIFICATION, false);
8171 }
8172 public function shouldAutoConfigure()
8173 {
8174 return !$this->repo->get(self::KEY_SETTINGS);
8175 }
8176 public function shouldDeployFilters()
8177 {
8178 $plugin_config = $this->get();
8179 if (!$plugin_config['enabled']) {
8180 return false;
8181 }
8182 if (!$plugin_config['admin-only']) {
8183 return true;
8184 }
8185 return $this->user->seesPreviewMode();
8186 }
8187 public function shouldDisplayFooter()
8188 {
8189 return $this->get()['footer-link'] && $this->shouldDeployFilters();
8190 }
8191 private function getDefaultAdminPanelSettings()
8192 {
8193 return ['enabled' => true, 'admin-only' => false, 'pathinfo-query-format' => false, 'footer-link' => false, 'compress-service-response' => true, 'img-optimization-tags' => true, 'img-optimization-css' => true, 'img-optimization-api' => true, 'img-lazy' => true, 'css-optimization' => true, 'scripts-rearrangement' => false, 'scripts-defer' => true, 'scripts-proxy' => true, 'iframe-defer' => true, 'minify-html' => true, 'minify-inline-scripts' => true];
8194 }
8195 }
8196 namespace Kibo\PhastPlugins\SDK\Configuration;
8197
8198 /**
8199 * A default implementation of the ServiceConfigurationRepository interface
8200 *
8201 * Class PHPFilesServiceConfigurationRepository
8202 */
8203 class PHPFilesServiceConfigurationRepository implements \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository
8204 {
8205 /**
8206 * @var CacheRootManager
8207 */
8208 private $cacheRootManager;
8209 /**
8210 * PHPFilesServiceConfigurationRepository constructor.
8211 * @param CacheRootManager $cacheRootManager
8212 */
8213 public function __construct(\Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager)
8214 {
8215 $this->cacheRootManager = $cacheRootManager;
8216 }
8217 public function store(array $config)
8218 {
8219 return $this->storeInPHPFile($this->getServiceConfigurationFilePath(), $config) !== false;
8220 }
8221 public function get()
8222 {
8223 $json = $this->readFromPHPFile($this->getServiceConfigurationFilePath());
8224 if (!$json) {
8225 return false;
8226 }
8227 if (strpos($json, 'a:') === 0) {
8228 $config = unserialize($json);
8229 } else {
8230 $config = json_decode($json, true);
8231 }
8232 if ($config === null) {
8233 return false;
8234 }
8235 return $config;
8236 }
8237 public function has()
8238 {
8239 return !!$this->get();
8240 }
8241 private function getServiceConfigurationFilePath()
8242 {
8243 return $this->getCacheStoredFilePath('service-config');
8244 }
8245 private function getCacheStoredFilePath($filename)
8246 {
8247 $dir = $this->cacheRootManager->getCacheRoot();
8248 if (!$dir) {
8249 return false;
8250 }
8251 $legacyName = "{$dir}/{$filename}.php";
8252 if (@file_exists($legacyName)) {
8253 return $legacyName;
8254 }
8255 foreach (@scandir($dir) as $file) {
8256 if (!preg_match('~^' . preg_quote($filename, '~') . '-[a-zA-Z0-9]{16}$~', $file)) {
8257 continue;
8258 }
8259 $path = "{$dir}/{$file}";
8260 if (@is_file($path)) {
8261 return $path;
8262 }
8263 }
8264 return "{$dir}/service-config-{$this->generateRandomName()}";
8265 }
8266 private function generateRandomName()
8267 {
8268 $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8269 $o = '';
8270 for ($i = 0; $i < 16; $i++) {
8271 $o .= $chars[mt_rand(0, strlen($chars) - 1)];
8272 }
8273 return $o;
8274 }
8275 private function readFromPHPFile($filename)
8276 {
8277 $content = @file_get_contents($filename);
8278 if (!$content) {
8279 return false;
8280 }
8281 if (!preg_match('/^[^>]*>\\n([a-f0-9]{40})\\n(.*)$/s', $content, $match)) {
8282 return false;
8283 }
8284 if (sha1($match[2]) != $match[1]) {
8285 return false;
8286 }
8287 return $match[2];
8288 }
8289 private function storeInPHPFile($filename, $value)
8290 {
8291 $value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES) . "\n";
8292 $content = "<?php exit; ?>\n" . sha1($value) . "\n" . $value;
8293 return @file_put_contents($filename, $content, LOCK_EX);
8294 }
8295 }
8296 namespace Kibo\PhastPlugins\SDK\APIs;
8297
8298 /**
8299 * Presents convenient methods for common tasks.
8300 *
8301 * Class Service
8302 */
8303 class Service
8304 {
8305 /**
8306 * @var ServiceConfiguration
8307 */
8308 private $config;
8309 /**
8310 * Service constructor.
8311 * @param ServiceConfiguration $config
8312 */
8313 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration $config)
8314 {
8315 $this->config = $config;
8316 }
8317 /**
8318 * Configures the services and serves the request
8319 */
8320 public function serve()
8321 {
8322 \Kibo\Phast\PhastServices::serve(function () {
8323 return $this->config->get();
8324 });
8325 }
8326 }
8327 namespace Kibo\PhastPlugins\SDK\APIs;
8328
8329 /**
8330 * Presents convenient methods for common tasks.
8331 *
8332 * Class Phast
8333 */
8334 class Phast
8335 {
8336 /**
8337 * @var PhastConfiguration
8338 */
8339 private $config;
8340 /**
8341 * PhastAPI constructor.
8342 * @param PhastConfiguration $config
8343 */
8344 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration $config)
8345 {
8346 $this->config = $config;
8347 }
8348 /**
8349 * Applies phast filters to $html
8350 * with a configuration suited for full documents
8351 *
8352 * @param $html
8353 * @return string
8354 */
8355 public function applyFiltersForDocument($html)
8356 {
8357 return \Kibo\Phast\PhastDocumentFilters::apply($html, $this->config->getForDocuments());
8358 }
8359 /**
8360 * Applies phast filters to $html
8361 * with a configuration suited for html snippets
8362 *
8363 * @param $html
8364 * @return string
8365 */
8366 public function applyFiltersForSnippets($html)
8367 {
8368 return \Kibo\Phast\PhastDocumentFilters::apply($html, $this->config->getForHTMLSnippets());
8369 }
8370 /**
8371 * Deploys phast output buffer filters
8372 * with a configuration suited for full documents
8373 */
8374 public function deployOutputBufferForDocument()
8375 {
8376 return \Kibo\Phast\PhastDocumentFilters::deploy($this->config->getForDocuments());
8377 }
8378 /**
8379 * Deploys phast output buffer filters
8380 * with a configuration suited for html snippets
8381 */
8382 public function deployOutputBufferForSnippets()
8383 {
8384 return \Kibo\Phast\PhastDocumentFilters::deploy($this->config->getForHTMLSnippets());
8385 }
8386 }
8387 namespace Kibo\PhastPlugins\SDK\Caching;
8388
8389 interface CacheRootCandidatesProvider
8390 {
8391 /**
8392 * Return a list of folders that will potentially be used
8393 * for storing cache and service configuration files.
8394 * The directories will be checked for write access
8395 * in the order they were provided. The first one writable
8396 * will be used.
8397 *
8398 * @return string[]
8399 */
8400 public function getCacheRootCandidates();
8401 }
8402 namespace Kibo\PhastPlugins\SDK\Caching;
8403
8404 class CacheRootManager
8405 {
8406 /**
8407 * @var CacheRootCandidatesProvider
8408 */
8409 private $rootsProvider;
8410 /**
8411 * CacheRootManager constructor.
8412 * @param CacheRootCandidatesProvider $rootsProvider
8413 */
8414 public function __construct(\Kibo\PhastPlugins\SDK\Caching\CacheRootCandidatesProvider $rootsProvider)
8415 {
8416 $this->rootsProvider = $rootsProvider;
8417 }
8418 public function getCacheRootCandidates()
8419 {
8420 return $this->rootsProvider->getCacheRootCandidates();
8421 }
8422 public function getCacheRoot()
8423 {
8424 $key = $this->getKey();
8425 $candidates = $this->getCacheRootCandidates();
8426 if ($result = $this->findExistingCacheRoot($key, $candidates)) {
8427 return $result;
8428 }
8429 if ($this->createNewCacheRoot($key, $candidates)) {
8430 return $this->findExistingCacheRoot($key, $candidates);
8431 }
8432 return false;
8433 }
8434 public function hasCacheRoot()
8435 {
8436 return (bool) $this->getCacheRoot();
8437 }
8438 public function getAllCacheRoots()
8439 {
8440 return $this->findAllExistingCacheRoots($this->getKey(), $this->getCacheRootCandidates());
8441 }
8442 private function getKey()
8443 {
8444 return md5(@$_SERVER['DOCUMENT_ROOT']) . '.' . (new \Kibo\Phast\Common\System())->getUserId();
8445 }
8446 private function findExistingCacheRoot($key, $candidates)
8447 {
8448 foreach ($this->findAllExistingCacheRoots($key, $candidates) as $checkDir) {
8449 if (!is_writable($checkDir)) {
8450 continue;
8451 }
8452 if (function_exists('posix_geteuid') && fileowner($checkDir) !== posix_geteuid()) {
8453 continue;
8454 }
8455 $this->createIndexFile($checkDir);
8456 return $checkDir;
8457 }
8458 return false;
8459 }
8460 private function findAllExistingCacheRoots($key, $candidates)
8461 {
8462 foreach ($this->getCacheRootCandidates() as $dir) {
8463 $checkDirs = ["{$dir}/{$key}", "{$dir}/phastpress.{$key}", "{$dir}/phast.{$key}"];
8464 foreach ($checkDirs as $checkDir) {
8465 if (!is_dir($checkDir)) {
8466 continue;
8467 }
8468 (yield $checkDir);
8469 }
8470 }
8471 }
8472 private function createNewCacheRoot($key, $candidates)
8473 {
8474 foreach ($this->getCacheRootCandidates() as $dir) {
8475 if (@mkdir("{$dir}/phast.{$key}", 0777, true)) {
8476 return true;
8477 }
8478 }
8479 return false;
8480 }
8481 private function createIndexFile($dir)
8482 {
8483 $path = "{$dir}/index.html";
8484 if (!@file_exists($path)) {
8485 @touch($path);
8486 }
8487 }
8488 }
8489 namespace Kibo\PhastPlugins\SDK;
8490
8491 class Autoloader
8492 {
8493 private static $instance;
8494 private $psr4 = array();
8495 public static function getInstance()
8496 {
8497 if (!isset(self::$instance)) {
8498 self::$instance = new self();
8499 self::$instance->install();
8500 }
8501 return self::$instance;
8502 }
8503 public function install()
8504 {
8505 spl_autoload_register(function ($class) {
8506 $this->autoload($class);
8507 });
8508 }
8509 public function addPSR4($namespace, $dir)
8510 {
8511 $this->psr4[] = [$namespace, $dir];
8512 return $this;
8513 }
8514 private function autoload($class)
8515 {
8516 foreach ($this->psr4 as $psr4) {
8517 list($namespace, $dir) = $psr4;
8518 if (strcasecmp($namespace . '\\', substr($class, 0, strlen($namespace) + 1))) {
8519 continue;
8520 }
8521 $relativeName = substr($class, strlen($namespace) + 1);
8522 $relativePath = str_replace('\\', '/', $relativeName) . '.php';
8523 $fullPath = $dir . '/' . $relativePath;
8524 if (file_exists($fullPath)) {
8525 include $fullPath;
8526 return;
8527 }
8528 }
8529 }
8530 }
8531 namespace Kibo\PhastPlugins\SDK;
8532
8533 interface ServiceHost
8534 {
8535 /**
8536 * @return CacheRootCandidatesProvider
8537 */
8538 public function getCacheRootCandidatesProvider();
8539 /**
8540 * Called right after the service configuration
8541 * has been loaded. Use it to modify the config
8542 * and take any other needed action before
8543 * the service is started.
8544 *
8545 * @param array $config - The configuration that has been loaded
8546 * @return array - The configuration to use for the services
8547 */
8548 public function onServiceConfigurationLoad(array $config);
8549 }
8550 namespace Kibo\PhastPlugins\SDK\Generated;
8551
8552 class Translations
8553 {
8554 const DATA = array('en' => array('AdminPanel' => array('errors' => array('no-cache-root' => '@:plugin-name can not write to any cache directory! Please, make one of the following directories writable: {params}', 'no-service-config' => '@:plugin-name failed to create a service configuration in any of the following directories: {params}', 'network-error' => 'Failed to connect to plugin server! Please, try again later! {params}', 'cache' => '{params}'), 'warnings' => array('disabled' => '@:plugin-name optimizations are off!', 'admin-only' => '@:plugin-name optimizations will be applied only for logged-in users with the "Administrator" privilege. This is for previewing purposes. Select the "On" setting for "@:plugin-name optimizations" below to activate for all users!
8555 ')), 'Backend' => array('install-notice' => 'Thank you for using <b>@:plugin-name</b>. Optimizations are <b>{pluginState}</b>. Go to <b><a href="{settingsUrl}">Settings</a></b> to configure <b>@:plugin-name</b>.
8556 ', 'status' => array('on' => 'on', 'off' => 'off', 'admin' => 'on for administrators')), 'Information' => array('additional' => 'Additional information'), 'Notification' => array('error' => 'error', 'warning' => 'warning', 'information' => 'information', 'success' => 'success'), 'OnOffSwitch' => array('on' => 'On', 'off' => 'Off'), 'SavingStatus' => array('saving' => 'Saving', 'saved' => 'Saved'), 'Settings' => array('common' => array('tip' => 'Tip:', 'on' => 'On:', 'off' => 'Off:'), 'sections' => array('plugin' => array('title' => 'Plugin', 'enabled' => array('name' => '@:plugin-name optimizations', 'description' => array('main' => 'Test your site {without} and {with}', 'without' => 'without @:plugin-name', 'with' => 'with @:plugin-name')), 'admin-only' => array('name' => 'Only optimize for administrators', 'description' => array('on' => 'Only privileged users will be served with optimized version', 'off' => 'All users will be served with optimized version', 'tip' => 'Use this to preview your site before launching the optimizations')), 'pathinfo' => array('name' => 'Remove query string from processed resources', 'description' => array('start' => 'Make sure that processed resources don\'t have query strings, for a higher score in GTmetrix.', 'on' => 'Use the path for requests for processed resources. This requires a server that supports "PATH_INFO".', 'off' => 'Use the GET parameters for requests for processed resources.')), 'footer-link' => array('name' => 'Let the world know about @:plugin-name', 'description' => 'Add a "Optimized by @:plugin-name" notice to the footer of your site and help spread the word.'), 'compress-service-response' => array('name' => 'Enable gzip compression on processed resources', 'description' => 'This compresses the optimized and bundled JavaScript and CSS generated by PhastPress. Disable this if your server already compresses PhastPress responses.')), 'images' => array('title' => 'Images', 'tags' => array('name' => 'Optimize images in tags', 'description' => 'Compress images with optimal settings. {newline} Resize images to fit {width}x{height} pixels or to the appropriate size for {imgTag} tags with {widthAttr} or {heightAttr}. {newline} Reload changed images while still leveraging browser caching.'), 'css' => array('name' => 'Optimize images in CSS', 'description' => array(0 => 'Compress images in stylesheets with optional settings and resizes the to fit {width}x{height} pixels.', 1 => 'Reload changed images while still leveraging browser caching.')), 'api' => array('name' => 'Use the Phast Image Optimization API', 'description' => array(0 => 'Optimize your images on our servers free of charge.', 1 => 'This will give you the best possible results without installing any software and will reduce the load on your hosting.')), 'lazy' => array('name' => 'Lazy load images', 'description' => array(0 => 'This adds the loading=lazy attribute to img tags so that images are only load once they are visible on the page.', 1 => 'This helps pass the "Defer offscreen images" audit in PageSpeed Insights.'))), 'html-filters' => array('title' => 'HTML, CSS & JS', 'css' => array('name' => 'Optimize CSS', 'description' => array(0 => 'Incline critical styles first and prevent unused styles from blocking the page load.', 1 => 'Minify stylesheets and leverage browser caching.', 2 => 'Inline Google Fonts CSS to speed up font loading.')), 'async-js' => array('name' => 'Load scripts asynchronously', 'description' => 'Allow the page to finish loading before all scripts have been executed.'), 'minify-js' => array('name' => 'Minify scripts and improve caching', 'description' => array(0 => 'Minify scripts and fix caching for Google Analytics and Hotjar.', 1 => 'Reload changed scripts while still leveraging browser caching.')), 'iframe' => array('name' => 'Defer IFrame loading', 'description' => 'Start loading IFrames after the page has finished loading.'), 'minify-html' => array('name' => 'Minify HTML', 'description' => 'Remove unnecessary whitespace from the HTML code of the page.'), 'minify-inline-scripts' => array('name' => 'Minify inline scripts and JSON', 'description' => 'Remove unnecessary whitespace from inline scripts and JSON data.'))))));
8557 }
8558 namespace Kibo\PhastPlugins\SDK\AJAX;
8559
8560 /**
8561 * Handles AJAX requests to the plugin admin.
8562 * Use this class to handle requests to the plugin's admin AJAX end point
8563 *
8564 * Class RequestsDispatcher
8565 */
8566 class RequestsDispatcher
8567 {
8568 const KEY_ACTION = 'phast-plugins-action';
8569 /**
8570 * @var PhastUser
8571 */
8572 private $user;
8573 /**
8574 * @var SDK
8575 */
8576 private $sdk;
8577 /**
8578 * RequestsDispatcher constructor.
8579 * @param PhastUser $user
8580 * @param SDK $sdk
8581 */
8582 public function __construct(\Kibo\PhastPlugins\SDK\Security\PhastUser $user, \Kibo\PhastPlugins\SDK\SDK $sdk)
8583 {
8584 $this->user = $user;
8585 $this->sdk = $sdk;
8586 }
8587 /**
8588 * Handles ajax requests to plugin's admin AJAX end point
8589 *
8590 * @param array $request The $_POST data to the request
8591 * @return mixed Must be json encoded and returned to the client
8592 */
8593 public function dispatch(array $request)
8594 {
8595 if (!$this->user->mayModifySettings()) {
8596 return false;
8597 }
8598 $action = isset($request[self::KEY_ACTION]) ? $request[self::KEY_ACTION] : '';
8599 if ($action == 'save-settings') {
8600 $this->sdk->getPluginConfiguration()->save($request);
8601 return $this->makeResponse(true, $this->sdk->getAdminPanelData()->get());
8602 }
8603 if ($action == 'dismiss-notice') {
8604 $this->sdk->getPluginConfiguration()->hideActivationNotification();
8605 return $this->makeResponse(true);
8606 }
8607 return $this->makeResponse(false);
8608 }
8609 private function makeResponse($success, $data = null)
8610 {
8611 return ['phast-success' => $success, 'phast-data' => $data];
8612 }
8613 }
8614 namespace Kibo\PhastPlugins\SDK\Security;
8615
8616 /**
8617 * Represents the user currently viewing the website (either backend or frontend)
8618 *
8619 * Interface PhastUser
8620 */
8621 interface PhastUser
8622 {
8623 /**
8624 * Tells whether the user can access and manipulate
8625 * the plugin's settings
8626 *
8627 * @return bool
8628 */
8629 public function mayModifySettings();
8630 /**
8631 * Tells whether the user can access the website with Phast enabled in preview mode
8632 *
8633 * @return bool
8634 */
8635 public function seesPreviewMode();
8636 }
8637 namespace Kibo\PhastPlugins\SDK\Security;
8638
8639 /**
8640 * Checks whether posted data to the server
8641 * contains the expected nonce.
8642 *
8643 * Interface NonceChecker
8644 */
8645 interface NonceChecker
8646 {
8647 /**
8648 * Performs the check
8649 *
8650 * @param array $data The data posted to the server
8651 * @return bool TRUE if all is well, FALSE otherwise
8652 */
8653 public function checkNonce(array $data);
8654 }
8655 namespace Kibo\PhastPlugins\SDK\Common;
8656
8657 /**
8658 * Contains common implementations for methods
8659 * of the PluginHost interface
8660 *
8661 * @see PluginHost
8662 * Trait PluginHostTrait
8663 */
8664 trait PluginHostTrait
8665 {
8666 public function getPluginName()
8667 {
8668 return 'Phast';
8669 }
8670 public function isDev()
8671 {
8672 return $this->getPluginHostVersion() === '$VER' . 'SION$';
8673 }
8674 public function onPhastConfigurationLoad(array $config)
8675 {
8676 return $config;
8677 }
8678 public function getLocale()
8679 {
8680 return 'en';
8681 }
8682 public function getInstallNoticeRenderer()
8683 {
8684 return new \Kibo\PhastPlugins\SDK\AdminPanel\DefaultInstallNoticeRenderer();
8685 }
8686 }
8687 namespace Kibo\PhastPlugins\SDK\Common;
8688
8689 trait ServiceHostTrait
8690 {
8691 public function onServiceConfigurationLoad(array $config)
8692 {
8693 return $config;
8694 }
8695 }
8696 namespace Kibo\PhastPlugins\SDK\Common;
8697
8698 trait PreviewCookieTrait
8699 {
8700 public function seesPreviewMode()
8701 {
8702 return isset($_COOKIE['PHAST_PREVIEW']) && (bool) $_COOKIE['PHAST_PREVIEW'];
8703 }
8704 }
8705 namespace Kibo\Phast\Environment\Exceptions;
8706
8707 class PackageHasNoDiagnosticsException extends \Kibo\Phast\Exceptions\LogicException
8708 {
8709 }
8710 namespace Kibo\Phast\Environment\Exceptions;
8711
8712 class PackageHasNoFactoryException extends \Kibo\Phast\Exceptions\LogicException
8713 {
8714 }
8715 namespace Kibo\Phast\Cache\File;
8716
8717 class DiagnosticsLogWriter implements \Kibo\Phast\Logging\LogWriter
8718 {
8719 public function setLevelMask($mask)
8720 {
8721 }
8722 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry)
8723 {
8724 if ($entry->getLevel() > 2) {
8725 $needles = array_map(function ($key) {
8726 return '{' . $key . '}';
8727 }, array_keys($entry->getContext()));
8728 $message = str_replace($needles, $entry->getContext(), $entry->getMessage());
8729 throw new \Kibo\Phast\Exceptions\RuntimeException("Error: Level: {$entry->getLevel()}, Msg: {$message}");
8730 }
8731 }
8732 }
8733 namespace Kibo\Phast\Cache\File;
8734
8735 class Diagnostics implements \Kibo\Phast\Diagnostics\Diagnostics
8736 {
8737 public function diagnose(array $config)
8738 {
8739 \Kibo\Phast\Logging\Log::setLogger(new \Kibo\Phast\Logging\Logger(new \Kibo\Phast\Cache\File\DiagnosticsLogWriter()));
8740 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'cache-self-diagnosis');
8741 $v1 = $cache->get('test-key', function () {
8742 return 1;
8743 }, 2);
8744 $v2 = $cache->get('test-key', function () {
8745 return 2;
8746 }, 2);
8747 if ($v1 != $v2) {
8748 throw new \Kibo\Phast\Exceptions\RuntimeException('Cache failed, but no error was reported!');
8749 }
8750 }
8751 }
8752 namespace Kibo\Phast\Filters\HTML\CommentsRemoval;
8753
8754 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
8755 {
8756 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
8757 {
8758 foreach ($elements as $element) {
8759 if (!$element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Comment || $element->isIEConditional()) {
8760 (yield $element);
8761 }
8762 }
8763 }
8764 }
8765 namespace Kibo\Phast\Filters\HTML\ScriptsDeferring;
8766
8767 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
8768 {
8769 use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait;
8770 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
8771 {
8772 return $tag->getTagName() == 'script';
8773 }
8774 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
8775 {
8776 if ($this->isJSElement($script) && !$this->isDeferralDisabled($script)) {
8777 $this->rewrite($script);
8778 }
8779 (yield $script);
8780 }
8781 protected function afterLoop()
8782 {
8783 $this->context->addPhastJavaScript(\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/ScriptsDeferring/scripts-loader.js', "var Promise=phast.ES6Promise;phast.ScriptsLoader={};phast.ScriptsLoader.getScriptsInExecutionOrder=function(a,b){var c=a.querySelectorAll('script[type=\"text/phast\"]');var d=[],e=[];for(var f=0;f<c.length;f++){if(getSrc(c[f])!==undefined&&c[f].hasAttribute(\"defer\")){e.push(c[f])}else{d.push(c[f])}}return d.concat(e).map(function(g){return b.makeScriptFromElement(g)})};phast.ScriptsLoader.executeScripts=function(h){var i=h.map(function(k){return k.init()});var j=Promise.resolve();h.forEach(function(l){j=phast.ScriptsLoader.chainScript(j,l)});return j.then(function(){return Promise.all(i).catch(function(){})})};phast.ScriptsLoader.chainScript=function(m,n){var o;try{if(n.describe){o=n.describe()}else{o=\"unknown script\"}}catch(p){o=\"script.describe() failed\"}return m.then(function(){var q=n.execute();q.then(function(){console.debug(\"\342\234\223\",o)});return q}).catch(function(r){console.error(\"\342\234\230\",o);if(r){console.log(r)}})};var insertBefore=window.Element.prototype.insertBefore;phast.ScriptsLoader.Utilities=function(s){this._document=s;var t=0;function u(C){return new Promise(function(D){var E=\"PhastCompleteScript\"+ ++t;var F=s.createElement(\"script\");F.textContent=C;var G=s.createElement(\"script\");G.textContent=E+\"()\";window[E]=H;s.body.appendChild(F);s.body.appendChild(G);function H(){D();s.body.removeChild(F);s.body.removeChild(G);delete window[E]}})}function v(I){var J=s.createElement(I.nodeName);Array.prototype.forEach.call(I.attributes,function(K){J.setAttribute(K.nodeName,K.nodeValue)});return J}function w(L){L.removeAttribute(\"data-phast-params\");var M={};Array.prototype.map.call(L.attributes,function(N){return N.nodeName}).map(function(O){var P=O.match(/^data-phast-original-(.*)/i);if(P){M[P[1].toLowerCase()]=L.getAttribute(O);L.removeAttribute(O)}});Object.keys(M).sort().map(function(Q){L.setAttribute(Q,M[Q])});if(!(\"type\"in M)){L.removeAttribute(\"type\")}}function x(R,S){return new Promise(function(T,U){var V=S.getAttribute(\"src\");S.addEventListener(\"load\",T);S.addEventListener(\"error\",U);S.removeAttribute(\"src\");insertBefore.call(R.parentNode,S,R);R.parentNode.removeChild(R);if(V){S.setAttribute(\"src\",V)}})}function y(W,X){return A(W,function(){return u(X)})}function z(Y,Z){return A(Z,function(){return x(Y,Z)})}function A(\$,_){var aa=\$.nextElementSibling;var ba=Promise.resolve();s.write=function(fa){ca(fa)};s.writeln=function(ga){ca(ga+\"\\n\")};function ca(ha){var ia=s.createElement(\"div\");ia.innerHTML=ha;var ja=da(ia);if(aa&&aa.parentNode!==\$.parentNode){aa=\$.nextElementSibling}while(ia.firstChild){\$.parentNode.insertBefore(ia.firstChild,aa)}ja.map(ea)}function da(ka){return Array.prototype.slice.call(ka.getElementsByTagName(\"script\")).filter(function(la){var ma=la.getAttribute(\"type\");return!ma||/^(text|application)\\/javascript(;|\$)/i.test(ma)})}function ea(na){var oa=new phast.ScriptsLoader.Scripts.Factory(s);var pa=oa.makeScriptFromElement(na);ba=phast.ScriptsLoader.chainScript(ba,pa)}return _().then(function(){return ba}).finally(function(){delete s.write;delete s.writeln})}function B(qa){var ra=s.createElement(\"link\");ra.setAttribute(\"rel\",\"preload\");ra.setAttribute(\"as\",\"script\");ra.setAttribute(\"href\",qa);s.head.appendChild(ra)}this.executeString=u;this.copyElement=v;this.restoreOriginals=w;this.replaceElement=x;this.writeProtectAndExecuteString=y;this.writeProtectAndReplaceElement=z;this.addPreload=B};phast.ScriptsLoader.Scripts={};phast.ScriptsLoader.Scripts.InlineScript=function(sa,ta){this._utils=sa;this._element=ta;this.init=function(){return Promise.resolve()};this.execute=function(){var ua=ta.textContent.replace(/^\\s*<!--.*\\n/i,\"\");sa.restoreOriginals(ta);return sa.writeProtectAndExecuteString(ta,ua)};this.describe=function(){return\"inline script\"}};phast.ScriptsLoader.Scripts.AsyncBrowserScript=function(va,wa){var xa;this._utils=va;this._element=wa;this.init=function(){va.addPreload(getSrc(wa));return new Promise(function(ya){xa=ya})};this.execute=function(){var za=va.copyElement(wa);va.restoreOriginals(za);va.replaceElement(wa,za).then(xa).catch(xa);return Promise.resolve()};this.describe=function(){return\"async script at \"+getSrc(wa)}};phast.ScriptsLoader.Scripts.SyncBrowserScript=function(Aa,Ba){this._utils=Aa;this._element=Ba;this.init=function(){Aa.addPreload(getSrc(Ba));return Promise.resolve()};this.execute=function(){var Ca=Aa.copyElement(Ba);Aa.restoreOriginals(Ca);return Aa.writeProtectAndReplaceElement(Ba,Ca)};this.describe=function(){return\"sync script at \"+getSrc(Ba)}};phast.ScriptsLoader.Scripts.AsyncAJAXScript=function(Da,Ea,Fa,Ga){this._utils=Da;this._element=Ea;this._fetch=Fa;this._fallback=Ga;var Ha;var Ia;this.init=function(){Ha=Fa(Ea);return new Promise(function(Ja){Ia=Ja})};this.execute=function(){Ha.then(function(Ka){Da.restoreOriginals(Ea);return Da.executeString(Ka).then(Ia)}).catch(function(){Ga.init();return Ga.execute().then(Ia)});return Promise.resolve()};this.describe=function(){return\"bundled async script at \"+Ea.getAttribute(\"data-phast-original-src\")}};phast.ScriptsLoader.Scripts.SyncAJAXScript=function(La,Ma,Na,Oa){this._utils=La;this._element=Ma;this._fetch=Na;this._fallback=Oa;var Pa;this.init=function(){Pa=Na(Ma);return Pa};this.execute=function(){return Pa.then(function(Qa){La.restoreOriginals(Ma);return La.writeProtectAndExecuteString(Ma,Qa)}).catch(function(){Oa.init();return Oa.execute()})};this.describe=function(){return\"bundled sync script at \"+Ma.getAttribute(\"data-phast-original-src\")}};phast.ScriptsLoader.Scripts.Factory=function(Ra,Sa){var Ta=phast.ScriptsLoader.Scripts;var Ua=new phast.ScriptsLoader.Utilities(Ra);this.makeScriptFromElement=function(Ya){var Za;if(Va(Ya)){if(Xa(Ya)){Za=new Ta.AsyncBrowserScript(Ua,Ya);return Sa?new Ta.AsyncAJAXScript(Ua,Ya,Sa,Za):Za}Za=new Ta.SyncBrowserScript(Ua,Ya);return Sa?new Ta.SyncAJAXScript(Ua,Ya,Sa,Za):Za}if(Wa(Ya)){return new Ta.InlineScript(Ua,Ya)}if(Xa(Ya)){return new Ta.AsyncBrowserScript(Ua,Ya)}return new Ta.SyncBrowserScript(Ua,Ya)};function Va(\$a){return \$a.hasAttribute(\"data-phast-params\")}function Wa(_a){return!_a.hasAttribute(\"src\")}function Xa(ab){return ab.hasAttribute(\"async\")}};function getSrc(bb){if(bb.hasAttribute(\"data-phast-original-src\")){return bb.getAttribute(\"data-phast-original-src\")}else if(bb.hasAttribute(\"src\")){return bb.getAttribute(\"src\")}}\n"));
8784 $this->context->addPhastJavaScript(\Kibo\Phast\ValueObjects\PhastJavaScript::fromString('/home/albert/code/phast/src/Build/../../src/Filters/HTML/ScriptsDeferring/rewrite.js', "var Promise=phast.ES6Promise;var go=phast.once(loadScripts);phast.on(document,\"DOMContentLoaded\").then(function(){if(phast.stylesLoading){phast.onStylesLoaded=go;setTimeout(go,4e3)}else{Promise.resolve().then(go)}});var loadFiltered=false;window.addEventListener(\"load\",function(a){if(!loadFiltered){a.stopImmediatePropagation()}loadFiltered=true});function loadScripts(){var b=new phast.ScriptsLoader.Scripts.Factory(document,fetchScript);var c=phast.ScriptsLoader.getScriptsInExecutionOrder(document,b);if(c.length===0){return}try{Object.defineProperty(document,\"readyState\",{configurable:true,get:function(){return\"loading\"}})}catch(d){console.error(\"[Phast] Unable to override document.readyState on this browser: \",d)}phast.ScriptsLoader.executeScripts(c).then(restoreReadyState)}function restoreReadyState(){window.requestAnimationFrame(function(){delete document[\"readyState\"];triggerEvent(document,\"readystatechange\");triggerEvent(document,\"DOMContentLoaded\");window.requestAnimationFrame(function(){if(loadFiltered){triggerEvent(window,\"load\")}else{loadFiltered=true}})})}function triggerEvent(e,f){var g=document.createEvent(\"Event\");g.initEvent(f,true,true);e.dispatchEvent(g)}function fetchScript(h){return phast.ResourceLoader.instance.get(phast.ResourceLoader.RequestParams.fromString(h.getAttribute(\"data-phast-params\")))}\n"));
8785 }
8786 private function rewrite(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
8787 {
8788 if ($script->hasAttribute('type')) {
8789 $script->setAttribute('data-phast-original-type', $script->getAttribute('type'));
8790 }
8791 $script->setAttribute('type', 'text/phast');
8792 if ($script->hasAttribute('data-phast-params')) {
8793 $script->removeAttribute('src');
8794 }
8795 }
8796 private function isDeferralDisabled(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
8797 {
8798 return $script->hasAttribute('data-phast-no-defer') || $script->hasAttribute('data-pagespeed-no-defer') || $script->getAttribute('data-cfasync') === 'false';
8799 }
8800 }
8801 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags;
8802
8803 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8804 {
8805 public function make(array $config)
8806 {
8807 return new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags\Filter((new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriterFactory())->make($config, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags\Filter::class));
8808 }
8809 }
8810 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS;
8811
8812 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8813 {
8814 public function make(array $config)
8815 {
8816 return new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter((new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriterFactory())->make($config, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter::class));
8817 }
8818 }
8819 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS;
8820
8821 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
8822 {
8823 /**
8824 * @var ImageURLRewriter
8825 */
8826 protected $rewriter;
8827 /**
8828 * Filter constructor.
8829 * @param ImageURLRewriter $rewriter
8830 */
8831 public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter)
8832 {
8833 $this->rewriter = $rewriter;
8834 }
8835 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
8836 {
8837 if ($tag->hasAttribute('style')) {
8838 $tag->setAttribute('style', $this->rewriter->rewriteStyle($tag->getAttribute('style')));
8839 }
8840 (yield $tag);
8841 }
8842 }
8843 namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler;
8844
8845 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8846 {
8847 public function make(array $config)
8848 {
8849 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'phast-scripts');
8850 $compiler = new \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\PhastJavaScriptCompiler($cache, $config['servicesUrl'], $config['serviceRequestFormat']);
8851 return new \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\Filter($compiler);
8852 }
8853 }
8854 namespace Kibo\Phast\Filters\HTML\ScriptsProxyService;
8855
8856 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8857 {
8858 public function make(array $config)
8859 {
8860 if (!isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class]['serviceUrl'])) {
8861 $config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class]['serviceUrl'] = $config['servicesUrl'];
8862 }
8863 $filterConfig = $config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class];
8864 $filterConfig['match'] = $config['scripts']['whitelist'];
8865 return new \Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter($filterConfig, (new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']), (new \Kibo\Phast\Services\Bundler\TokenRefMakerFactory())->make($config));
8866 }
8867 }
8868 namespace Kibo\Phast\Filters\HTML\CSSInlining;
8869
8870 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8871 {
8872 public function make(array $config)
8873 {
8874 $localRetriever = new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']);
8875 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
8876 $retriever->addRetriever($localRetriever);
8877 $retriever->addRetriever(new \Kibo\Phast\Retrievers\CachingRetriever(new \Kibo\Phast\Cache\File\Cache($config['cache'], 'css')));
8878 if (!isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class]['serviceUrl'])) {
8879 $config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class]['serviceUrl'] = $config['servicesUrl'];
8880 }
8881 return new \Kibo\Phast\Filters\HTML\CSSInlining\Filter((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), \Kibo\Phast\ValueObjects\URL::fromString($config['documents']['baseUrl']), $config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class], $localRetriever, $retriever, new \Kibo\Phast\Filters\HTML\CSSInlining\OptimizerFactory($config), (new \Kibo\Phast\Filters\CSS\Composite\Factory())->make($config), (new \Kibo\Phast\Services\Bundler\TokenRefMakerFactory())->make($config));
8882 }
8883 }
8884 namespace Kibo\Phast\Filters\HTML\Diagnostics;
8885
8886 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8887 {
8888 public function make(array $config)
8889 {
8890 $url = isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\Diagnostics\Filter::class]['serviceUrl']) ? $config['documents']['filters'][\Kibo\Phast\Filters\HTML\Diagnostics\Filter::class]['serviceUrl'] : $config['servicesUrl'] . '?service=diagnostics';
8891 return new \Kibo\Phast\Filters\HTML\Diagnostics\Filter($url);
8892 }
8893 }
8894 namespace Kibo\Phast\Filters\Image\Exceptions;
8895
8896 class ImageProcessingException extends \Kibo\Phast\Exceptions\RuntimeException
8897 {
8898 }
8899 namespace Kibo\Phast\Filters\Image\ImageImplementations;
8900
8901 class DummyImage extends \Kibo\Phast\Filters\Image\ImageImplementations\BaseImage implements \Kibo\Phast\Filters\Image\Image
8902 {
8903 /**
8904 * @var string
8905 */
8906 private $imageString;
8907 private $transformationString;
8908 /**
8909 * DummyImage constructor.
8910 *
8911 * @param int $width
8912 * @param int $height
8913 */
8914 public function __construct($width = null, $height = null)
8915 {
8916 $this->width = $width;
8917 $this->height = $height;
8918 }
8919 /**
8920 * @return int
8921 */
8922 public function getWidth()
8923 {
8924 return $this->width;
8925 }
8926 /**
8927 * @return int
8928 */
8929 public function getHeight()
8930 {
8931 return $this->height;
8932 }
8933 /**
8934 * @return string
8935 */
8936 public function getType()
8937 {
8938 return $this->type;
8939 }
8940 /**
8941 * @param string $type
8942 */
8943 public function setType($type)
8944 {
8945 $this->type = $type;
8946 }
8947 /**
8948 * @return int
8949 */
8950 public function getCompression()
8951 {
8952 return $this->compression;
8953 }
8954 /**
8955 * @return string
8956 */
8957 public function getAsString()
8958 {
8959 return $this->imageString;
8960 }
8961 /**
8962 * @param string $imageString
8963 */
8964 public function setImageString($imageString)
8965 {
8966 $this->imageString = $imageString;
8967 }
8968 /**
8969 * @param mixed $transformationString
8970 */
8971 public function setTransformationString($transformationString)
8972 {
8973 $this->transformationString = $transformationString;
8974 }
8975 protected function __clone()
8976 {
8977 $this->imageString = $this->transformationString;
8978 }
8979 }
8980 namespace Kibo\Phast\Filters\Service;
8981
8982 class CachingServiceFilter implements \Kibo\Phast\Services\ServiceFilter
8983 {
8984 use \Kibo\Phast\Logging\LoggingTrait;
8985 /**
8986 * @var Cache
8987 */
8988 private $cache;
8989 /**
8990 * @var CachedResultServiceFilter
8991 */
8992 private $cachedFilter;
8993 /**
8994 * @var Retriever
8995 */
8996 private $retriever;
8997 /**
8998 * CachingServiceFilter constructor.
8999 * @param Cache $cache
9000 * @param CachedResultServiceFilter $cachedFilter
9001 * @param Retriever $retriever
9002 */
9003 public function __construct(\Kibo\Phast\Cache\Cache $cache, \Kibo\Phast\Filters\Service\CachedResultServiceFilter $cachedFilter, \Kibo\Phast\Retrievers\Retriever $retriever)
9004 {
9005 $this->cache = $cache;
9006 $this->cachedFilter = $cachedFilter;
9007 $this->retriever = $retriever;
9008 }
9009 /**
9010 * @param Resource $resource
9011 * @param array $request
9012 * @return Resource
9013 * @throws CachedExceptionException
9014 */
9015 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9016 {
9017 $key = $this->cachedFilter->getCacheSalt($resource, $request);
9018 $this->logger()->info('Trying to get {url} from cache', ['url' => (string) $resource->getUrl()]);
9019 $result = $this->cache->get($key);
9020 if (isset($result['encoding']) && $result['encoding'] != 'identity') {
9021 $result = null;
9022 }
9023 if ($result && $this->checkDependencies($result)) {
9024 return $this->deserializeCachedData($result);
9025 }
9026 try {
9027 $result = $this->cachedFilter->apply($resource, $request);
9028 $this->cache->set($key, $this->serializeResource($result));
9029 return $result;
9030 } catch (\Exception $e) {
9031 $cachingException = $this->serializeException($e);
9032 $this->cache->set($key, $cachingException);
9033 throw $this->deserializeException($cachingException);
9034 }
9035 }
9036 private function checkDependencies(array $data)
9037 {
9038 foreach ((array) @$data['dependencies'] as $dep) {
9039 $url = \Kibo\Phast\ValueObjects\URL::fromString($dep['url']);
9040 if ($this->retriever->getCacheSalt($url) >= $dep['cacheMarker']) {
9041 return false;
9042 }
9043 }
9044 return true;
9045 }
9046 private function deserializeCachedData(array $data)
9047 {
9048 if ($data['dataType'] == 'exception') {
9049 throw $this->deserializeException($data);
9050 }
9051 return $this->deserializeResource($data);
9052 }
9053 private function serializeResource(\Kibo\Phast\ValueObjects\Resource $resource)
9054 {
9055 return ['dataType' => 'resource', 'url' => $resource->getUrl()->toString(), 'mimeType' => $resource->getMimeType(), 'blob' => base64_encode($resource->getContent()), 'dependencies' => $this->serializeDependencies($resource)];
9056 }
9057 private function serializeDependencies(\Kibo\Phast\ValueObjects\Resource $resource)
9058 {
9059 return array_map(function (\Kibo\Phast\ValueObjects\Resource $dep) {
9060 return ['url' => $dep->getUrl()->toString(), 'cacheMarker' => $dep->getCacheSalt()];
9061 }, $resource->getDependencies());
9062 }
9063 private function deserializeResource(array $data)
9064 {
9065 $params = [\Kibo\Phast\ValueObjects\URL::fromString($data['url']), base64_decode($data['blob']), $data['mimeType']];
9066 return \Kibo\Phast\ValueObjects\Resource::makeWithContent(...$params);
9067 }
9068 private function serializeException(\Exception $e)
9069 {
9070 return ['dataType' => 'exception', 'class' => get_class($e), 'msg' => $e->getMessage(), 'code' => $e->getCode()];
9071 }
9072 private function deserializeException(array $data)
9073 {
9074 return new \Kibo\Phast\Exceptions\CachedExceptionException(sprintf('Phast: %s: Type: %s, Msg: %s, Code: %s', static::class, $data['class'], $data['msg'], $data['code']));
9075 }
9076 }
9077 namespace Kibo\Phast\Filters\Service;
9078
9079 interface CachedResultServiceFilter extends \Kibo\Phast\Services\ServiceFilter
9080 {
9081 /**
9082 * @param Resource $resource
9083 * @param array $request
9084 * @return string
9085 */
9086 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request);
9087 }
9088 namespace Kibo\Phast\Filters\Service;
9089
9090 class CompositeFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9091 {
9092 use \Kibo\Phast\Logging\LoggingTrait;
9093 /**
9094 * @var ServiceFilter[]
9095 */
9096 private $filters = array();
9097 public function addFilter(\Kibo\Phast\Services\ServiceFilter $filter)
9098 {
9099 $this->filters[] = $filter;
9100 }
9101 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9102 {
9103 $classes = array_map('get_class', $this->filters);
9104 $cached = array_filter($this->filters, function (\Kibo\Phast\Services\ServiceFilter $filter) {
9105 return $filter instanceof \Kibo\Phast\Filters\Service\CachedResultServiceFilter;
9106 });
9107 $salts = array_map(function (\Kibo\Phast\Filters\Service\CachedResultServiceFilter $filter) use($resource, $request) {
9108 return $filter->getCacheSalt($resource, $request);
9109 }, $cached);
9110 return join("\n", array_merge($classes, $salts, [$resource->getUrl(), $resource->getCacheSalt()]));
9111 }
9112 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9113 {
9114 $this->logger()->info('Starting filtering for resource {url}', ['url' => $resource->getUrl()]);
9115 $result = array_reduce($this->filters, function (\Kibo\Phast\ValueObjects\Resource $resource, \Kibo\Phast\Services\ServiceFilter $filter) use($request) {
9116 $this->logger()->info('Starting {filter}', ['filter' => get_class($filter)]);
9117 try {
9118 return $filter->apply($resource, $request);
9119 } catch (\Kibo\Phast\Exceptions\RuntimeException $e) {
9120 $message = 'Phast RuntimeException: Filter: {filter} Exception: {exceptionClass} Msg: {message} Code: {code} File: {file} Line: {line}';
9121 $this->logger()->critical($message, ['filter' => get_class($filter), 'exceptionClass' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
9122 return $resource;
9123 }
9124 }, $resource);
9125 $this->logger()->info('Done filtering for resource {url}', ['url' => $resource->getUrl()]);
9126 return $result;
9127 }
9128 }
9129 namespace Kibo\Phast\Filters\CSS\CSSMinifier;
9130
9131 class Filter implements \Kibo\Phast\Services\ServiceFilter
9132 {
9133 /**
9134 * @param Resource $resource
9135 * @param array $request
9136 * @return Resource
9137 */
9138 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9139 {
9140 $content = $resource->getContent();
9141 // Normalize whitespace
9142 $content = preg_replace('~\\s+~', ' ', $content);
9143 // Remove whitespace before and after operators
9144 $chars = [',', '{', '}', ';'];
9145 foreach ($chars as $char) {
9146 $content = str_replace("{$char} ", $char, $content);
9147 $content = str_replace(" {$char}", $char, $content);
9148 }
9149 // Remove whitespace after colons
9150 $content = str_replace(': ', ':', $content);
9151 return $resource->withContent(trim($content));
9152 }
9153 }
9154 namespace Kibo\Phast\Filters\CSS\CSSURLRewriter;
9155
9156 class Filter implements \Kibo\Phast\Services\ServiceFilter
9157 {
9158 /**
9159 * @param Resource $resource
9160 * @param array $request
9161 * @return Resource
9162 */
9163 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9164 {
9165 $baseUrl = $resource->getUrl();
9166 $callback = function ($match) use($baseUrl) {
9167 if (preg_match('~^[a-z]+:|^#~i', $match[3])) {
9168 return $match[0];
9169 }
9170 return $match[1] . \Kibo\Phast\ValueObjects\URL::fromString($match[3])->withBase($baseUrl) . $match[4];
9171 };
9172 $cssContent = preg_replace_callback('~
9173 \\b
9174 ( url\\( ([\'"]?) )
9175 ([A-Za-z0-9_/.:?&=+%,#@-]+)
9176 ( \\2 \\) )
9177 ~x', $callback, $resource->getContent());
9178 $cssContent = preg_replace_callback('~
9179 ( @import \\s+ ([\'"]) )
9180 ([A-Za-z0-9_/.:?&=+%,#@-]+)
9181 ( \\2 )
9182 ~x', $callback, $cssContent);
9183 return $resource->withContent($cssContent);
9184 }
9185 }
9186 namespace Kibo\Phast\Filters\CSS\ImageURLRewriter;
9187
9188 class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9189 {
9190 /**
9191 * @var ImageURLRewriter
9192 */
9193 private $rewriter;
9194 /**
9195 * Filter constructor.
9196 * @param ImageURLRewriter $rewriter
9197 */
9198 public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter)
9199 {
9200 $this->rewriter = $rewriter;
9201 }
9202 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9203 {
9204 return $this->rewriter->getCacheSalt();
9205 }
9206 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9207 {
9208 $content = $this->rewriter->rewriteStyle($resource->getContent());
9209 $dependencies = $this->rewriter->getInlinedResources();
9210 return $resource->withContent($content)->withDependencies($dependencies);
9211 }
9212 }
9213 namespace Kibo\Phast\Filters\CSS\Composite;
9214
9215 class Filter extends \Kibo\Phast\Filters\Service\CompositeFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9216 {
9217 public function __construct()
9218 {
9219 $this->addFilter(new \Kibo\Phast\Filters\CSS\CommentsRemoval\Filter());
9220 }
9221 }
9222 namespace Kibo\Phast\Filters\CSS\FontSwap;
9223
9224 class Filter implements \Kibo\Phast\Services\ServiceFilter
9225 {
9226 const FONT_FACE_REGEXP = '/(@font-face\\s*\\{)([^}]*)/i';
9227 const ICON_FONT_FAMILIES = array('Font Awesome', 'GeneratePress', 'Dashicons', 'Ionicons');
9228 private $fontDisplayBlockPattern;
9229 public function __construct()
9230 {
9231 $this->fontDisplayBlockPattern = $this->getFontDisplayBlockPattern();
9232 }
9233 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9234 {
9235 $css = $resource->getContent();
9236 $filtered = preg_replace_callback(self::FONT_FACE_REGEXP, function ($match) {
9237 list($block, $start, $contents) = $match;
9238 $mode = preg_match($this->fontDisplayBlockPattern, $contents) ? 'block' : 'swap';
9239 return $start . 'font-display:' . $mode . ';' . $contents;
9240 }, $css);
9241 return $resource->withContent($filtered);
9242 }
9243 private function getFontDisplayBlockPattern()
9244 {
9245 $patterns = [];
9246 foreach (self::ICON_FONT_FAMILIES as $family) {
9247 $chars = str_split($family);
9248 $chars = array_map(function ($char) {
9249 return preg_quote($char, '~');
9250 }, $chars);
9251 $patterns[] = implode('\\s*', $chars);
9252 }
9253 return '~' . implode('|', $patterns) . '~i';
9254 }
9255 }
9256 namespace Kibo\Phast\Filters\CSS\ImportsStripper;
9257
9258 class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9259 {
9260 use \Kibo\Phast\Logging\LoggingTrait;
9261 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9262 {
9263 return $this->shouldStripImports($request) ? 'strip-imports' : 'no-strip-imports';
9264 }
9265 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9266 {
9267 if (!$this->shouldStripImports($request)) {
9268 $this->logger()->info('No import stripping requested! Skipping!');
9269 return $resource;
9270 }
9271 $css = $resource->getContent();
9272 $stripped = preg_replace(\Kibo\Phast\Filters\HTML\CSSInlining\Filter::CSS_IMPORTS_REGEXP, '', $css);
9273 return $resource->withContent($stripped);
9274 }
9275 private function shouldStripImports(array $request)
9276 {
9277 return isset($request['strip-imports']);
9278 }
9279 }
9280 namespace Kibo\Phast\Filters\CSS\CommentsRemoval;
9281
9282 class Filter implements \Kibo\Phast\Services\ServiceFilter
9283 {
9284 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9285 {
9286 $content = preg_replace('~/\\*[^*]*\\*+([^/*][^*]*\\*+)*/~', '', $resource->getContent());
9287 return $resource->withContent($content);
9288 }
9289 }
9290 namespace Kibo\Phast\Filters\Text\Decode;
9291
9292 class Filter implements \Kibo\Phast\Services\ServiceFilter
9293 {
9294 const UTF8_BOM = "\357\273\277";
9295 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request = array())
9296 {
9297 $content = $resource->getContent();
9298 if (substr($content, 0, strlen(self::UTF8_BOM)) == self::UTF8_BOM) {
9299 $content = substr($content, strlen(self::UTF8_BOM));
9300 }
9301 return $resource->withContent($content);
9302 }
9303 }
9304 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
9305
9306 class ClosingTag extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
9307 {
9308 /**
9309 * @var string
9310 */
9311 private $tagName;
9312 /**
9313 * ClosingTag constructor.
9314 * @param string $tagName
9315 */
9316 public function __construct($tagName)
9317 {
9318 $this->tagName = strtolower($tagName);
9319 }
9320 /**
9321 * @return string
9322 */
9323 public function getTagName()
9324 {
9325 return $this->tagName;
9326 }
9327 public function appendChild(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element $element)
9328 {
9329 $this->stream->insertBeforeElement($this, $element);
9330 }
9331 public function dumpValue()
9332 {
9333 return $this->tagName;
9334 }
9335 }
9336 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
9337
9338 class Tag extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
9339 {
9340 /**
9341 * @var string
9342 */
9343 private $tagName;
9344 /**
9345 * @var array
9346 */
9347 private $attributes = array();
9348 /**
9349 * @var array
9350 */
9351 private $newAttributes = array();
9352 /**
9353 * @var \Iterator
9354 */
9355 private $attributeReader;
9356 /**
9357 * @var string
9358 */
9359 private $textContent = '';
9360 /**
9361 * @var string
9362 */
9363 private $closingTag = '';
9364 private $dirty = false;
9365 /**
9366 * Tag constructor.
9367 * @param $tagName
9368 * @param array|\Traversable $attributes
9369 */
9370 public function __construct($tagName, $attributes = array())
9371 {
9372 $this->tagName = strtolower($tagName);
9373 if ($attributes instanceof \Iterator) {
9374 $this->attributeReader = $attributes;
9375 } elseif (is_array($attributes)) {
9376 $this->attributeReader = new \ArrayIterator($attributes);
9377 } else {
9378 throw new \InvalidArgumentException('Attributes must be array or Iterator');
9379 }
9380 }
9381 /**
9382 * @return string
9383 */
9384 public function getTagName()
9385 {
9386 return $this->tagName;
9387 }
9388 /**
9389 * @param string $attrName
9390 * @return bool
9391 */
9392 public function hasAttribute($attrName)
9393 {
9394 return $this->getAttribute($attrName) !== null;
9395 }
9396 /**
9397 * @param string $attrName
9398 * @return mixed|null
9399 */
9400 public function getAttribute($attrName)
9401 {
9402 if (array_key_exists($attrName, $this->newAttributes)) {
9403 return $this->newAttributes[$attrName];
9404 }
9405 if (!array_key_exists($attrName, $this->attributes)) {
9406 $this->readUntilAttribute($attrName);
9407 }
9408 if (isset($this->attributes[$attrName])) {
9409 return $this->attributes[$attrName];
9410 }
9411 }
9412 /** @return string[] */
9413 public function getAttributes()
9414 {
9415 $this->readUntilAttribute(null);
9416 return array_filter($this->newAttributes + $this->attributes, function ($value) {
9417 return $value !== null;
9418 });
9419 }
9420 private function readUntilAttribute($attrName)
9421 {
9422 if (!$this->attributeReader) {
9423 return;
9424 }
9425 while ($this->attributeReader->valid()) {
9426 $name = strtolower($this->attributeReader->key());
9427 $value = $this->attributeReader->current();
9428 $this->attributeReader->next();
9429 if (!isset($this->attributes[$name])) {
9430 $this->attributes[$name] = $value;
9431 }
9432 if ($name == $attrName) {
9433 return;
9434 }
9435 }
9436 $this->attributeReader = null;
9437 }
9438 /**
9439 * @param string $attrName
9440 * @param string $value
9441 */
9442 public function setAttribute($attrName, $value)
9443 {
9444 if ($this->getAttribute($attrName) === $value) {
9445 return;
9446 }
9447 $this->dirty = true;
9448 $this->newAttributes[$attrName] = $value;
9449 }
9450 /**
9451 * @param string $attrName
9452 */
9453 public function removeAttribute($attrName)
9454 {
9455 $this->dirty = true;
9456 $this->newAttributes[$attrName] = null;
9457 }
9458 /**
9459 * @return string
9460 */
9461 public function getTextContent()
9462 {
9463 return $this->textContent;
9464 }
9465 /**
9466 * @param string $textContent
9467 */
9468 public function setTextContent($textContent)
9469 {
9470 $this->textContent = $textContent;
9471 }
9472 /**
9473 * @param $closingTag
9474 * @return Tag
9475 */
9476 public function withClosingTag($closingTag)
9477 {
9478 $new = clone $this;
9479 $new->closingTag = $closingTag;
9480 return $new;
9481 }
9482 /**
9483 * @return string
9484 */
9485 public function getClosingTag()
9486 {
9487 return $this->closingTag;
9488 }
9489 public function __toString()
9490 {
9491 return $this->getOpening() . $this->textContent . $this->getClosing();
9492 }
9493 private function getOpening()
9494 {
9495 if ($this->dirty || !isset($this->originalString)) {
9496 return $this->generateOpeningTag();
9497 }
9498 return parent::__toString();
9499 }
9500 private function getClosing()
9501 {
9502 if ($this->closingTag) {
9503 return $this->closingTag;
9504 }
9505 if ($this->mustHaveClosing() && !$this->isFromParser()) {
9506 return '</' . $this->tagName . '>';
9507 }
9508 return '';
9509 }
9510 private function generateOpeningTag()
9511 {
9512 $parts = ['<' . $this->tagName];
9513 foreach ($this->getAttributes() as $name => $value) {
9514 $parts[] = $this->generateAttribute($name, $value);
9515 }
9516 return join(' ', $parts) . '>';
9517 }
9518 private function generateAttribute($name, $value)
9519 {
9520 $result = $name;
9521 if ($value != '') {
9522 $result .= '=' . $this->quoteAttributeValue($value);
9523 }
9524 return $result;
9525 }
9526 private function quoteAttributeValue($value)
9527 {
9528 if (strpos($value, '"') === false) {
9529 return '"' . htmlspecialchars($value) . '"';
9530 }
9531 return "'" . str_replace(['&', "'"], ['&amp;', '&#039;'], $value) . "'";
9532 }
9533 private function mustHaveClosing()
9534 {
9535 return !\Kibo\Phast\Parsing\HTML\HTMLInfo::isA($this->tagName, \Kibo\Phast\Parsing\HTML\HTMLInfo::VOID_TAG);
9536 }
9537 private function isFromParser()
9538 {
9539 return isset($this->originalString);
9540 }
9541 public function dumpValue()
9542 {
9543 $o = $this->tagName;
9544 foreach ($this->attributes as $name => $_) {
9545 $o .= " {$name}=\"" . $this->getAttribute($name) . '"';
9546 }
9547 if ($this->textContent) {
9548 $o .= " content=[{$this->textContent}]";
9549 }
9550 return $o;
9551 }
9552 }
9553 namespace Kibo\Phast\Common;
9554
9555 class JSMinifier extends \JSMin\JSMin
9556 {
9557 protected $removeLicenseHeaders;
9558 public function __construct($input, $removeLicenseHeaders = false)
9559 {
9560 parent::__construct($input);
9561 $this->removeLicenseHeaders = $removeLicenseHeaders;
9562 }
9563 protected function consumeMultipleLineComment()
9564 {
9565 parent::consumeMultipleLineComment();
9566 if ($this->removeLicenseHeaders) {
9567 $this->keptComment = preg_replace('~/\\*!.*?\\*/~s', '', $this->keptComment);
9568 }
9569 }
9570 }
9571 namespace Kibo\Phast\Logging\LogWriters\Dummy;
9572
9573 class Writer implements \Kibo\Phast\Logging\LogWriter
9574 {
9575 public function setLevelMask($mask)
9576 {
9577 }
9578 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry)
9579 {
9580 }
9581 }
9582 namespace Kibo\Phast\Logging\LogWriters;
9583
9584 abstract class BaseLogWriter implements \Kibo\Phast\Logging\LogWriter
9585 {
9586 protected $levelMask = ~0;
9587 protected abstract function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry);
9588 public function setLevelMask($mask)
9589 {
9590 $this->levelMask = $mask;
9591 }
9592 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry)
9593 {
9594 if ($this->levelMask & $entry->getLevel()) {
9595 $this->doWriteEntry($entry);
9596 }
9597 }
9598 }
9599 namespace Kibo\Phast\Services\Css;
9600
9601 class Service extends \Kibo\Phast\Services\BaseService
9602 {
9603 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9604 {
9605 $response = parent::makeResponse($resource, $request);
9606 $response->setHeader('Content-Type', 'text/css');
9607 return $response;
9608 }
9609 }
9610 namespace Kibo\Phast\Services\Scripts;
9611
9612 class Service extends \Kibo\Phast\Services\BaseService
9613 {
9614 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9615 {
9616 $response = parent::makeResponse($resource, $request);
9617 $response->setHeader('Content-Type', 'application/javascript');
9618 return $response;
9619 }
9620 }
9621 namespace Kibo\Phast\Services\Images;
9622
9623 class Service extends \Kibo\Phast\Services\BaseService
9624 {
9625 protected function getParams(\Kibo\Phast\Services\ServiceRequest $request)
9626 {
9627 $params = parent::getParams($request);
9628 if ($this->proxySupportsAccept($request->getHTTPRequest())) {
9629 $params['varyAccept'] = true;
9630 if ($this->browserSupportsWebp($request->getHTTPRequest())) {
9631 $params['preferredType'] = \Kibo\Phast\Filters\Image\Image::TYPE_WEBP;
9632 \Kibo\Phast\Logging\Log::info('WebP will be served if possible!');
9633 }
9634 }
9635 return $params;
9636 }
9637 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9638 {
9639 $response = parent::makeResponse($resource, $request);
9640 $srcUrl = $resource->getUrl();
9641 $response->setHeader('Link', "<{$srcUrl}>; rel=\"canonical\"");
9642 $response->setHeader('Content-Type', $resource->getMimeType());
9643 if ($resource->getMimeType() != \Kibo\Phast\Filters\Image\Image::TYPE_PNG && @$request['varyAccept']) {
9644 $response->setHeader('Vary', 'Accept');
9645 }
9646 return $response;
9647 }
9648 protected function validateIntegrity(\Kibo\Phast\Services\ServiceRequest $request)
9649 {
9650 if (!$this->config['images']['api-mode']) {
9651 parent::validateIntegrity($request);
9652 }
9653 }
9654 protected function validateWhitelisted(\Kibo\Phast\Services\ServiceRequest $request)
9655 {
9656 if (!$this->config['images']['api-mode']) {
9657 parent::validateWhitelisted($request);
9658 }
9659 }
9660 private function browserSupportsWebp(\Kibo\Phast\HTTP\Request $request)
9661 {
9662 return strpos($request->getHeader('accept'), 'image/webp') !== false;
9663 }
9664 private function proxySupportsAccept(\Kibo\Phast\HTTP\Request $request)
9665 {
9666 return !$request->isCloudflare();
9667 }
9668 }
9669 namespace Kibo\PhastPlugins\SDK\AdminPanel;
9670
9671 class DefaultInstallNoticeRenderer implements \Kibo\PhastPlugins\SDK\AdminPanel\InstallNoticeRenderer
9672 {
9673 public function render($notice, $onCloseJSFunction)
9674 {
9675 return $notice;
9676 }
9677 }
9678 namespace Kibo\PhastPlugins\SDK;
9679
9680 interface PluginHost extends \Kibo\PhastPlugins\SDK\ServiceHost
9681 {
9682 /**
9683 * The name of the plugin used for displaying to the users
9684 *
9685 * @return string
9686 */
9687 public function getPluginName();
9688 /**
9689 * The name of the host system
9690 *
9691 * @return string
9692 */
9693 public function getPluginHostName();
9694 /**
9695 * The version of the plugin
9696 *
9697 * @return string
9698 */
9699 public function getPluginHostVersion();
9700 /**
9701 * Tells whether we are in production or development mode.
9702 * In development mode static files will be loaded from a dev server.
9703 * In production mode static files will be loaded from a prebuilt source.
9704 *
9705 * @return bool - TRUE for development, FALSE for production
9706 */
9707 public function isDev();
9708 /**
9709 * @return KeyValueStore
9710 */
9711 public function getKeyValueStore();
9712 /**
9713 * @return InstallNoticeRenderer
9714 */
9715 public function getInstallNoticeRenderer();
9716 /**
9717 * @return HostURLs
9718 */
9719 public function getHostURLs();
9720 /**
9721 * @return Nonce
9722 */
9723 public function getNonce();
9724 /**
9725 * @return NonceChecker
9726 */
9727 public function getNonceChecker();
9728 /**
9729 * @return PhastUser
9730 */
9731 public function getPhastUser();
9732 /**
9733 * Called right after phast's configuration
9734 * has been loaded. Use it to modify the config
9735 * and take any other needed action before
9736 * the filters are applied.
9737 *
9738 * @param array $config - The configuration that has been loaded
9739 * @return array - The configuration to use for phast
9740 */
9741 public function onPhastConfigurationLoad(array $config);
9742 /**
9743 * Returns the current system locale
9744 *
9745 * @return string
9746 */
9747 public function getLocale();
9748 }
9749 namespace Kibo\Phast\Filters\JavaScript\Minification;
9750
9751 class JSMinifierFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9752 {
9753 const VERSION = 2;
9754 private $removeLicenseHeaders = true;
9755 /**
9756 * JSMinifierFilter constructor.
9757 * @param bool $removeLicenseHeaders
9758 */
9759 public function __construct($removeLicenseHeaders)
9760 {
9761 $this->removeLicenseHeaders = (bool) $removeLicenseHeaders;
9762 }
9763 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9764 {
9765 return http_build_query(['v' => self::VERSION, 'removeLicenseHeaders' => $this->removeLicenseHeaders]);
9766 }
9767 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9768 {
9769 $minified = (new \Kibo\Phast\Common\JSMinifier($resource->getContent(), $this->removeLicenseHeaders))->min();
9770 return $resource->withContent($minified);
9771 }
9772 }
9773 namespace Kibo\Phast\Filters\Image\Composite;
9774
9775 class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9776 {
9777 use \Kibo\Phast\Logging\LoggingTrait;
9778 /**
9779 * @var ImageFactory
9780 */
9781 private $imageFactory;
9782 /**
9783 * @var ImageInliningManager
9784 */
9785 private $inliningManager;
9786 /**
9787 * @var ImageFilter[]
9788 */
9789 private $filters = array();
9790 /**
9791 * Filter constructor.
9792 * @param ImageFactory $imageFactory
9793 * @param ImageInliningManager $inliningManager
9794 */
9795 public function __construct(\Kibo\Phast\Filters\Image\ImageFactory $imageFactory, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManager $inliningManager)
9796 {
9797 $this->imageFactory = $imageFactory;
9798 $this->inliningManager = $inliningManager;
9799 }
9800 public function addImageFilter(\Kibo\Phast\Filters\Image\ImageFilter $filter)
9801 {
9802 $this->filters[] = $filter;
9803 }
9804 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9805 {
9806 $filters = array_map('get_class', $this->filters);
9807 $salts = array_map(function (\Kibo\Phast\Filters\Image\ImageFilter $filter) use($request) {
9808 return $filter->getCacheSalt($request);
9809 }, $this->filters);
9810 return implode("\n", array_merge($filters, $salts, [$this->inliningManager->getMaxImageInliningSize(), $resource->getUrl(), $resource->getCacheSalt()]));
9811 }
9812 /**
9813 * @param Resource $resource
9814 * @param array $request
9815 * @return Resource
9816 */
9817 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9818 {
9819 $image = $this->imageFactory->getForResource($resource);
9820 $filteredImage = $image;
9821 foreach ($this->filters as $filter) {
9822 $this->logger()->info('Applying {filter}', ['filter' => get_class($filter)]);
9823 try {
9824 $filteredImage = $filter->transformImage($filteredImage, $request);
9825 } catch (\Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException $e) {
9826 $message = 'Image filter exception: Filter: {filter} Exception: {exceptionClass} Msg: {message} Code: {code} File: {file} Line: {line}';
9827 $this->logger()->critical($message, ['filter' => get_class($filter), 'exceptionClass' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
9828 }
9829 }
9830 $sizeBefore = $filteredImage->getSizeAsString();
9831 $sizeAfter = $image->getSizeAsString();
9832 $sizeDifference = $sizeBefore - $sizeAfter;
9833 $this->logger()->info('Image processed. Size before/after: {sizeBefore}/{sizeAfter} ({sizeDifference})', ['sizeBefore' => $sizeBefore, 'sizeAfter' => $sizeAfter, 'sizeDifference' => $sizeDifference < 0 ? $sizeDifference : "+{$sizeDifference}"]);
9834 if ($sizeDifference < 0) {
9835 $this->logger()->info('Return filtered image and save {sizeDifference} bytes', ['sizeDifference' => -$sizeDifference]);
9836 $image = $filteredImage;
9837 } else {
9838 $this->logger()->info('Return original image');
9839 }
9840 $processedResource = $resource->withContent($image->getAsString(), $image->getType());
9841 $this->inliningManager->maybeStoreForInlining($processedResource);
9842 return $processedResource;
9843 }
9844 }
9845 namespace Kibo\Phast\Logging\LogWriters\JSONLFile;
9846
9847 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9848 {
9849 use \Kibo\Phast\Logging\Common\JSONLFileLogTrait;
9850 /**
9851 * @param LogEntry $entry
9852 */
9853 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
9854 {
9855 $encoded = @\Kibo\Phast\Common\JSON::encode($entry->toArray());
9856 if ($encoded) {
9857 $this->makeDirIfNotExists();
9858 @file_put_contents($this->filename, $encoded . "\n", FILE_APPEND | LOCK_EX);
9859 }
9860 }
9861 private function makeDirIfNotExists()
9862 {
9863 if (!@file_exists($this->dir)) {
9864 @mkdir($this->dir, 0777, true);
9865 }
9866 }
9867 }
9868 namespace Kibo\Phast\Logging\LogWriters\Composite;
9869
9870 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9871 {
9872 /**
9873 * @var Writer[]
9874 */
9875 private $writers = array();
9876 public function addWriter(\Kibo\Phast\Logging\LogWriter $writer)
9877 {
9878 $this->writers[] = $writer;
9879 }
9880 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
9881 {
9882 foreach ($this->writers as $writer) {
9883 $writer->writeEntry($entry);
9884 }
9885 }
9886 }
9887 namespace Kibo\Phast\Logging\LogWriters\RotatingTextFile;
9888
9889 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9890 {
9891 /** @var string */
9892 private $path = 'phast.log';
9893 /** @var int */
9894 private $maxFiles = 2;
9895 /** @var int */
9896 private $maxSize = 10 * 1024 * 1024;
9897 /** @var ObjectifiedFunctions */
9898 private $funcs;
9899 /**
9900 * @param array $config
9901 * @param ?ObjectifiedFunctions $funcs
9902 */
9903 public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
9904 {
9905 if (isset($config['path'])) {
9906 $this->path = (string) $config['path'];
9907 }
9908 if (isset($config['maxFiles'])) {
9909 $this->maxFiles = (int) $config['maxFiles'];
9910 }
9911 if (isset($config['maxSize'])) {
9912 $this->maxSize = (int) $config['maxSize'];
9913 }
9914 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
9915 }
9916 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
9917 {
9918 if (!($this->levelMask & $entry->getLevel())) {
9919 return;
9920 }
9921 $message = $this->interpolate($entry->getMessage(), $entry->getContext());
9922 $line = sprintf("%s %s %s\n", gmdate('Y-m-d\\TH:i:s\\Z', $this->funcs->time()), \Kibo\Phast\Logging\LogLevel::toString($entry->getLevel()), $message);
9923 clearstatcache(true, $this->path);
9924 $this->rotate(strlen($line));
9925 file_put_contents($this->path, $line, FILE_APPEND);
9926 }
9927 private function interpolate($message, $context)
9928 {
9929 $prefix = '';
9930 $prefixKeys = ['requestId', 'service', 'class', 'method', 'line'];
9931 foreach ($prefixKeys as $key) {
9932 if (isset($context[$key])) {
9933 $prefix .= '{' . $key . "}\t";
9934 }
9935 }
9936 return preg_replace_callback('/{(.+?)}/', function ($match) use($context) {
9937 return array_key_exists($match[1], $context) ? $context[$match[1]] : $match[0];
9938 }, $prefix . $message);
9939 }
9940 private function rotate($bufferSize)
9941 {
9942 if (!$this->shouldRotate($bufferSize)) {
9943 return;
9944 }
9945 if (!($fp = fopen($this->path, 'r+'))) {
9946 return;
9947 }
9948 try {
9949 if (!flock($fp, LOCK_EX | LOCK_NB)) {
9950 return;
9951 }
9952 if (!$this->shouldRotate($bufferSize)) {
9953 return;
9954 }
9955 for ($i = $this->maxFiles - 1; $i > 0; $i--) {
9956 @rename($this->getName($i - 1), $this->getName($i));
9957 }
9958 } finally {
9959 fclose($fp);
9960 }
9961 }
9962 private function getName($index)
9963 {
9964 if ($index <= 0) {
9965 return $this->path;
9966 }
9967 return $this->path . '.' . $index;
9968 }
9969 private function shouldRotate($bufferSize)
9970 {
9971 $currentSize = @filesize($this->path);
9972 if (!$currentSize) {
9973 return false;
9974 }
9975 $newSize = $currentSize + $bufferSize;
9976 return $newSize > $this->maxSize;
9977 }
9978 }
9979 namespace Kibo\Phast\Logging\LogWriters\PHPError;
9980
9981 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9982 {
9983 private $messageType = 0;
9984 private $destination = null;
9985 private $extraHeaders = null;
9986 /**
9987 * @var ObjectifiedFunctions
9988 */
9989 private $funcs;
9990 /**
9991 * PHPErrorLogWriter constructor.
9992 * @param array $config
9993 * @param ObjectifiedFunctions $funcs
9994 */
9995 public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
9996 {
9997 foreach (['messageType', 'destination', 'extraHeaders'] as $field) {
9998 if (isset($config[$field])) {
9999 $this->{$field} = $config[$field];
10000 }
10001 }
10002 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
10003 }
10004 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
10005 {
10006 if ($this->levelMask & $entry->getLevel()) {
10007 $this->funcs->error_log($this->interpolate($entry->getMessage(), $entry->getContext()), $this->messageType, $this->destination, $this->extraHeaders);
10008 }
10009 }
10010 private function interpolate($message, $context)
10011 {
10012 $prefix = '';
10013 $prefixKeys = ['requestId', 'service', 'class', 'method', 'line'];
10014 foreach ($prefixKeys as $key) {
10015 if (isset($context[$key])) {
10016 $prefix .= '{' . $key . "}\t";
10017 }
10018 }
10019 return preg_replace_callback('/{(.+?)}/', function ($match) use($context) {
10020 return array_key_exists($match[1], $context) ? $context[$match[1]] : $match[0];
10021 }, $prefix . $message);
10022 }
10023 }